From 5718b73470f3c79178b907fdd68d759591ea2a73 Mon Sep 17 00:00:00 2001 From: Chloe Date: Sat, 15 Aug 2026 13:44:30 -0700 Subject: [PATCH] my evil test --- app/build.gradle.kts | 2 + app/src/main/cpp/CMakeLists.txt | 7 +- app/src/main/cpp/pacing.cpp | 63 + .../geode/launcher/ExplicitGLSurfaceView.java | 1543 +++++++++++++++++ .../geode/launcher/GeometryDashActivity.kt | 66 +- .../java/com/geode/launcher/LauncherFix.kt | 8 + .../org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt | 18 +- .../java/org/cocos2dx/lib/Cocos2dxRenderer.kt | 13 +- 8 files changed, 1676 insertions(+), 44 deletions(-) create mode 100644 app/src/main/cpp/pacing.cpp create mode 100644 app/src/main/java/com/geode/launcher/ExplicitGLSurfaceView.java diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3fdde6f..7f3c42e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -84,6 +84,7 @@ android { buildFeatures { compose = true buildConfig = true + prefab = true } packaging { resources { @@ -112,6 +113,7 @@ dependencies { implementation ("androidx.activity:activity-ktx:1.13.0") implementation ("androidx.appcompat:appcompat:1.8.0") implementation ("androidx.documentfile:documentfile:1.1.0") + implementation ("androidx.games:games-frame-pacing:2.1.3") implementation ("com.squareup.okio:okio:3.18.2") implementation ("com.squareup.okhttp3:okhttp:5.5.0") diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index 12c579d..dba04e1 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -2,7 +2,9 @@ cmake_minimum_required(VERSION 3.21) project(launcherfix) -add_library(launcherfix SHARED launcher-fix.cpp patcher.cpp) +find_package(games-frame-pacing REQUIRED CONFIG) + +add_library(launcherfix SHARED launcher-fix.cpp patcher.cpp pacing.cpp) target_compile_features(launcherfix PUBLIC cxx_std_20) set_target_properties(launcherfix PROPERTIES CXX_EXTENSIONS OFF) @@ -14,4 +16,5 @@ set_target_properties(launcherfix PROPERTIES CXX_VISIBILITY_PRESET hidden ) -target_link_libraries(launcherfix PRIVATE log) +target_link_libraries(launcherfix PRIVATE log android) +target_link_libraries(launcherfix PRIVATE games-frame-pacing::swappy_static) \ No newline at end of file diff --git a/app/src/main/cpp/pacing.cpp b/app/src/main/cpp/pacing.cpp new file mode 100644 index 0000000..821674b --- /dev/null +++ b/app/src/main/cpp/pacing.cpp @@ -0,0 +1,63 @@ +#include +#include +#include + +extern "C" +JNIEXPORT void JNICALL Java_com_geode_launcher_LauncherFix_initFramePacing( + JNIEnv *env, + jobject, + jobject activity +) { + SwappyGL_init(env, activity); +} + +struct NativeWindowDeleter { + void operator()(ANativeWindow* p) const { + if (p != nullptr) ANativeWindow_release(p); + } +}; + +std::unique_ptr native_window = nullptr; + +extern "C" +JNIEXPORT void JNICALL Java_com_geode_launcher_LauncherFix_destroyFramePacing(JNIEnv*, jobject) { + SwappyGL_destroy(); +} + +extern "C" +JNIEXPORT jboolean JNICALL Java_com_geode_launcher_LauncherFix_swapFrame( + JNIEnv*, + jobject, + jlong display, + jlong surface +) { + return SwappyGL_swap( + reinterpret_cast(display), + reinterpret_cast(surface) + ); +} + +extern "C" +JNIEXPORT void JNICALL Java_com_geode_launcher_LauncherFix_setSurface( + JNIEnv* env, + jobject, + jobject surface +) { + if (surface == nullptr) { + native_window.reset(); + SwappyGL_setWindow(nullptr); + return; + } + + native_window.reset(ANativeWindow_fromSurface(env, surface)); + SwappyGL_setWindow(native_window.get()); +} + +extern "C" +JNIEXPORT void JNICALL Java_com_geode_launcher_LauncherFix_setSwapInterval( + JNIEnv* env, + jobject, + jlong interval_ns +) { + SwappyGL_setSwapIntervalNS(interval_ns); +} diff --git a/app/src/main/java/com/geode/launcher/ExplicitGLSurfaceView.java b/app/src/main/java/com/geode/launcher/ExplicitGLSurfaceView.java new file mode 100644 index 0000000..bc39546 --- /dev/null +++ b/app/src/main/java/com/geode/launcher/ExplicitGLSurfaceView.java @@ -0,0 +1,1543 @@ +package com.geode.launcher; + +import android.content.Context; +import android.opengl.EGL14; +import android.opengl.EGLConfig; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLExt; +import android.opengl.EGLSurface; +import android.util.AttributeSet; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import java.io.Writer; +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +public class ExplicitGLSurfaceView extends SurfaceView implements SurfaceHolder.Callback2 { + private final static String TAG = "ExplicitGLSurfaceView"; + private final static boolean LOG_ATTACH_DETACH = false; + private final static boolean LOG_THREADS = false; + private final static boolean LOG_PAUSE_RESUME = false; + private final static boolean LOG_SURFACE = false; + private final static boolean LOG_RENDERER = false; + private final static boolean LOG_RENDERER_DRAW_FRAME = false; + private final static boolean LOG_EGL = false; + /** + * The renderer only renders + * when the surface is created, or when {@link #requestRender} is called. + * + * @see #getRenderMode() + * @see #setRenderMode(int) + * @see #requestRender() + */ + public final static int RENDERMODE_WHEN_DIRTY = 0; + /** + * The renderer is called + * continuously to re-render the scene. + * + * @see #getRenderMode() + * @see #setRenderMode(int) + */ + public final static int RENDERMODE_CONTINUOUSLY = 1; + /** + * Check glError() after every GL call and throw an exception if glError indicates + * that an error has occurred. This can be used to help track down which OpenGL ES call + * is causing an error. + * + * @see #getDebugFlags + * @see #setDebugFlags + */ + public final static int DEBUG_CHECK_GL_ERROR = 1; + /** + * Log GL calls to the system log at "verbose" level with tag "GLSurfaceView". + * + * @see #getDebugFlags + * @see #setDebugFlags + */ + public final static int DEBUG_LOG_GL_CALLS = 2; + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ + public ExplicitGLSurfaceView(Context context) { + super(context); + init(); + } + /** + * Standard View constructor. In order to render something, you + * must call {@link #setRenderer} to register a renderer. + */ + public ExplicitGLSurfaceView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + @Override + protected void finalize() throws Throwable { + try { + if (mGLThread != null) { + // GLThread may still be running if this view was never + // attached to a window. + mGLThread.requestExitAndWait(); + } + } finally { + super.finalize(); + } + } + private void init() { + // Install a SurfaceHolder.Callback so we get notified when the + // underlying surface is created and destroyed + SurfaceHolder holder = getHolder(); + holder.addCallback(this); + // setFormat is done by SurfaceView in SDK 2.3 and newer. Uncomment + // this statement if back-porting to 2.2 or older: + // holder.setFormat(PixelFormat.RGB_565); + // + // setType is not needed for SDK 2.0 or newer. Uncomment this + // statement if back-porting this code to older SDKs. + // holder.setType(SurfaceHolder.SURFACE_TYPE_GPU); + } + + /** + * Set the debug flags to a new value. The value is + * constructed by OR-together zero or more + * of the DEBUG_CHECK_* constants. The debug flags take effect + * whenever a surface is created. The default value is zero. + * @param debugFlags the new debug flags + * @see #DEBUG_CHECK_GL_ERROR + * @see #DEBUG_LOG_GL_CALLS + */ + public void setDebugFlags(int debugFlags) { + mDebugFlags = debugFlags; + } + /** + * Get the current value of the debug flags. + * @return the current value of the debug flags. + */ + public int getDebugFlags() { + return mDebugFlags; + } + /** + * Control whether the EGL context is preserved when the GLSurfaceView is paused and + * resumed. + *

+ * If set to true, then the EGL context may be preserved when the GLSurfaceView is paused. + *

+ * Prior to API level 11, whether the EGL context is actually preserved or not + * depends upon whether the Android device can support an arbitrary number of + * EGL contexts or not. Devices that can only support a limited number of EGL + * contexts must release the EGL context in order to allow multiple applications + * to share the GPU. + *

+ * If set to false, the EGL context will be released when the GLSurfaceView is paused, + * and recreated when the GLSurfaceView is resumed. + *

+ * + * The default is false. + * + * @param preserveOnPause preserve the EGL context when paused + */ + public void setPreserveEGLContextOnPause(boolean preserveOnPause) { + mPreserveEGLContextOnPause = preserveOnPause; + } + /** + * @return true if the EGL context will be preserved when paused + */ + public boolean getPreserveEGLContextOnPause() { + return mPreserveEGLContextOnPause; + } + /** + * Set the renderer associated with this view. Also starts the thread that + * will call the renderer, which in turn causes the rendering to start. + *

This method should be called once and only once in the life-cycle of + * a GLSurfaceView. + *

The following GLSurfaceView methods can only be called before + * setRenderer is called: + *

    + *
  • {@link #setEGLConfigChooser(boolean)} + *
  • {@link #setEGLConfigChooser(EGLConfigChooser)} + *
  • {@link #setEGLConfigChooser(int, int, int, int, int, int)} + *
+ *

+ * The following GLSurfaceView methods can only be called after + * setRenderer is called: + *

    + *
  • {@link #getRenderMode()} + *
  • {@link #onPause()} + *
  • {@link #onResume()} + *
  • {@link #queueEvent(Runnable)} + *
  • {@link #requestRender()} + *
  • {@link #setRenderMode(int)} + *
+ * + * @param renderer the renderer to use to perform OpenGL drawing. + */ + public void setRenderer(Renderer renderer) { + checkRenderThreadState(); + if (mEGLConfigChooser == null) { + mEGLConfigChooser = new SimpleEGLConfigChooser(true); + } + if (mEGLContextFactory == null) { + mEGLContextFactory = new DefaultContextFactory(); + } + if (mEGLWindowSurfaceFactory == null) { + mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory(); + } + mRenderer = renderer; + mGLThread = new GLThread(mThisWeakRef); + mGLThread.start(); + } + /** + * Install a custom EGLContextFactory. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If this method is not called, then by default + * a context will be created with no shared context and + * with a null attribute list. + */ + public void setEGLContextFactory(EGLContextFactory factory) { + checkRenderThreadState(); + mEGLContextFactory = factory; + } + /** + * Install a custom EGLWindowSurfaceFactory. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If this method is not called, then by default + * a window surface will be created with a null attribute list. + */ + public void setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory factory) { + checkRenderThreadState(); + mEGLWindowSurfaceFactory = factory; + } + /** + * Install a custom EGLConfigChooser. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If no setEGLConfigChooser method is called, then by default the + * view will choose an EGLConfig that is compatible with the current + * android.view.Surface, with a depth buffer depth of + * at least 16 bits. + * @param configChooser + */ + public void setEGLConfigChooser(EGLConfigChooser configChooser) { + checkRenderThreadState(); + mEGLConfigChooser = configChooser; + } + /** + * Install a config chooser which will choose a config + * as close to 16-bit RGB as possible, with or without an optional depth + * buffer as close to 16-bits as possible. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If no setEGLConfigChooser method is called, then by default the + * view will choose an RGB_888 surface with a depth buffer depth of + * at least 16 bits. + * + * @param needDepth + */ + public void setEGLConfigChooser(boolean needDepth) { + setEGLConfigChooser(new SimpleEGLConfigChooser(needDepth)); + } + /** + * Install a config chooser which will choose a config + * with at least the specified depthSize and stencilSize, + * and exactly the specified redSize, greenSize, blueSize and alphaSize. + *

If this method is + * called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

+ * If no setEGLConfigChooser method is called, then by default the + * view will choose an RGB_888 surface with a depth buffer depth of + * at least 16 bits. + * + */ + public void setEGLConfigChooser(int redSize, int greenSize, int blueSize, + int alphaSize, int depthSize, int stencilSize) { + setEGLConfigChooser(new ComponentSizeChooser(redSize, greenSize, + blueSize, alphaSize, depthSize, stencilSize)); + } + /** + * Inform the default EGLContextFactory and default EGLConfigChooser + * which EGLContext client version to pick. + *

Use this method to create an OpenGL ES 2.0-compatible context. + * Example: + *

+     *     public MyView(Context context) {
+     *         super(context);
+     *         setEGLContextClientVersion(2); // Pick an OpenGL ES 2.0 context.
+     *         setRenderer(new MyRenderer());
+     *     }
+     * 
+ *

Note: Activities which require OpenGL ES 2.0 should indicate this by + * setting @lt;uses-feature android:glEsVersion="0x00020000" /> in the activity's + * AndroidManifest.xml file. + *

If this method is called, it must be called before {@link #setRenderer(Renderer)} + * is called. + *

This method only affects the behavior of the default EGLContexFactory and the + * default EGLConfigChooser. If + * {@link #setEGLContextFactory(EGLContextFactory)} has been called, then the supplied + * EGLContextFactory is responsible for creating an OpenGL ES 2.0-compatible context. + * If + * {@link #setEGLConfigChooser(EGLConfigChooser)} has been called, then the supplied + * EGLConfigChooser is responsible for choosing an OpenGL ES 2.0-compatible config. + * @param version The EGLContext client version to choose. Use 2 for OpenGL ES 2.0 + */ + public void setEGLContextClientVersion(int version) { + checkRenderThreadState(); + mEGLContextClientVersion = version; + } + /** + * Set the rendering mode. When renderMode is + * RENDERMODE_CONTINUOUSLY, the renderer is called + * repeatedly to re-render the scene. When renderMode + * is RENDERMODE_WHEN_DIRTY, the renderer only rendered when the surface + * is created, or when {@link #requestRender} is called. Defaults to RENDERMODE_CONTINUOUSLY. + *

+ * Using RENDERMODE_WHEN_DIRTY can improve battery life and overall system performance + * by allowing the GPU and CPU to idle when the view does not need to be updated. + *

+ * This method can only be called after {@link #setRenderer(Renderer)} + * + * @param renderMode one of the RENDERMODE_X constants + * @see #RENDERMODE_CONTINUOUSLY + * @see #RENDERMODE_WHEN_DIRTY + */ + public void setRenderMode(int renderMode) { + mGLThread.setRenderMode(renderMode); + } + /** + * Get the current rendering mode. May be called + * from any thread. Must not be called before a renderer has been set. + * @return the current rendering mode. + * @see #RENDERMODE_CONTINUOUSLY + * @see #RENDERMODE_WHEN_DIRTY + */ + public int getRenderMode() { + return mGLThread.getRenderMode(); + } + /** + * Request that the renderer render a frame. + * This method is typically used when the render mode has been set to + * {@link #RENDERMODE_WHEN_DIRTY}, so that frames are only rendered on demand. + * May be called + * from any thread. Must not be called before a renderer has been set. + */ + public void requestRender() { + mGLThread.requestRender(); + } + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + public void surfaceCreated(SurfaceHolder holder) { + LauncherFix.INSTANCE.setSurface(holder.getSurface()); + mGLThread.surfaceCreated(); + } + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + public void surfaceDestroyed(SurfaceHolder holder) { + LauncherFix.INSTANCE.setSurface(null); + // Surface will be destroyed when we return + mGLThread.surfaceDestroyed(); + } + /** + * This method is part of the SurfaceHolder.Callback interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { + LauncherFix.INSTANCE.setSurface(holder.getSurface()); + mGLThread.onWindowResize(w, h); + } + /** + * This method is part of the SurfaceHolder.Callback2 interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + @Override + public void surfaceRedrawNeededAsync(SurfaceHolder holder, Runnable finishDrawing) { + if (mGLThread != null) { + mGLThread.requestRenderAndNotify(finishDrawing); + } + } + /** + * This method is part of the SurfaceHolder.Callback2 interface, and is + * not normally called or subclassed by clients of GLSurfaceView. + */ + @Deprecated + @Override + public void surfaceRedrawNeeded(SurfaceHolder holder) { + // Since we are part of the framework we know only surfaceRedrawNeededAsync + // will be called. + } + + public void onPause() { + mGLThread.onPause(); + } + + public void onResume() { + mGLThread.onResume(); + } + /** + * Queue a runnable to be run on the GL rendering thread. This can be used + * to communicate with the Renderer on the rendering thread. + * Must not be called before a renderer has been set. + * @param r the runnable to be run on the GL rendering thread. + */ + public void queueEvent(Runnable r) { + mGLThread.queueEvent(r); + } + /** + * This method is used as part of the View class and is not normally + * called or subclassed by clients of GLSurfaceView. + */ + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onAttachedToWindow reattach =" + mDetached); + } + if (mDetached && (mRenderer != null)) { + int renderMode = RENDERMODE_CONTINUOUSLY; + if (mGLThread != null) { + renderMode = mGLThread.getRenderMode(); + } + mGLThread = new GLThread(mThisWeakRef); + if (renderMode != RENDERMODE_CONTINUOUSLY) { + mGLThread.setRenderMode(renderMode); + } + mGLThread.start(); + } + mDetached = false; + } + @Override + protected void onDetachedFromWindow() { + if (LOG_ATTACH_DETACH) { + Log.d(TAG, "onDetachedFromWindow"); + } + if (mGLThread != null) { + mGLThread.requestExitAndWait(); + } + mDetached = true; + super.onDetachedFromWindow(); + } + // ---------------------------------------------------------------------- + /** + * A generic renderer interface. + *

+ * The renderer is responsible for making OpenGL calls to render a frame. + *

+ * GLSurfaceView clients typically create their own classes that implement + * this interface, and then call {@link ExplicitGLSurfaceView#setRenderer} to + * register the renderer with the GLSurfaceView. + *

+ * + *

+ *

Developer Guides

+ *

For more information about how to use OpenGL, read the + * OpenGL developer guide.

+ *
+ * + *

Threading

+ * The renderer will be called on a separate thread, so that rendering + * performance is decoupled from the UI thread. Clients typically need to + * communicate with the renderer from the UI thread, because that's where + * input events are received. Clients can communicate using any of the + * standard Java techniques for cross-thread communication, or they can + * use the {@link ExplicitGLSurfaceView#queueEvent(Runnable)} convenience method. + *

+ *

EGL Context Lost

+ * There are situations where the EGL rendering context will be lost. This + * typically happens when device wakes up after going to sleep. When + * the EGL context is lost, all OpenGL resources (such as textures) that are + * associated with that context will be automatically deleted. In order to + * keep rendering correctly, a renderer must recreate any lost resources + * that it still needs. The {@link #onSurfaceCreated(EGLConfig)} method + * is a convenient place to do this. + * + * + * @see #setRenderer(Renderer) + */ + public interface Renderer { + /** + * Called when the surface is created or recreated. + *

+ * Called when the rendering thread + * starts and whenever the EGL context is lost. The EGL context will typically + * be lost when the Android device awakes after going to sleep. + *

+ * Since this method is called at the beginning of rendering, as well as + * every time the EGL context is lost, this method is a convenient place to put + * code to create resources that need to be created when the rendering + * starts, and that need to be recreated when the EGL context is lost. + * Textures are an example of a resource that you might want to create + * here. + *

+ * Note that when the EGL context is lost, all OpenGL resources associated + * with that context will be automatically deleted. You do not need to call + * the corresponding "glDelete" methods such as glDeleteTextures to + * manually delete these lost resources. + *

+ * @param config the EGLConfig of the created surface. Can be used + * to create matching pbuffers. + */ + void onSurfaceCreated(EGLConfig config); + /** + * Called when the surface changed size. + *

+ * Called after the surface is created and whenever + * the OpenGL ES surface size changes. + *

+ * Typically you will set your viewport here. If your camera + * is fixed then you could also set your projection matrix here: + *

+         * void onSurfaceChanged(GL10 gl, int width, int height) {
+         *     gl.glViewport(0, 0, width, height);
+         *     // for a fixed camera, set the projection too
+         *     float ratio = (float) width / height;
+         *     gl.glMatrixMode(GL10.GL_PROJECTION);
+         *     gl.glLoadIdentity();
+         *     gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
+         * }
+         * 
+ * @param width + * @param height + */ + void onSurfaceChanged(int width, int height); + /** + * Called to draw the current frame. + *

+ * This method is responsible for drawing the current frame. + *

+ * The implementation of this method typically looks like this: + *

+         * void onDrawFrame(GL10 gl) {
+         *     gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
+         *     //... other gl calls to render the scene ...
+         * }
+         * 
+ */ + void onDrawFrame(); + } + /** + * An interface for customizing the eglCreateContext and eglDestroyContext calls. + *

+ * This interface must be implemented by clients wishing to call + * {@link ExplicitGLSurfaceView#setEGLContextFactory(EGLContextFactory)} + */ + public interface EGLContextFactory { + EGLContext createContext(EGLDisplay display, EGLConfig eglConfig); + void destroyContext(EGLDisplay display, EGLContext context); + } + private class DefaultContextFactory implements EGLContextFactory { + private int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + public EGLContext createContext(EGLDisplay display, EGLConfig config) { + int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, + EGL14.EGL_NONE }; + return EGL14.eglCreateContext(display, config, EGL14.EGL_NO_CONTEXT, + mEGLContextClientVersion != 0 ? attrib_list : null, 0); + } + public void destroyContext(EGLDisplay display, + EGLContext context) { + if (!EGL14.eglDestroyContext(display, context)) { + Log.e("DefaultContextFactory", "display:" + display + " context: " + context); + if (LOG_THREADS) { + Log.i("DefaultContextFactory", "tid=" + Thread.currentThread().getId()); + } + EglHelper.throwEglException("eglDestroyContex", EGL14.eglGetError()); + } + } + } + /** + * An interface for customizing the eglCreateWindowSurface and eglDestroySurface calls. + *

+ * This interface must be implemented by clients wishing to call + * {@link ExplicitGLSurfaceView#setEGLWindowSurfaceFactory(EGLWindowSurfaceFactory)} + */ + public interface EGLWindowSurfaceFactory { + /** + * @return null if the surface cannot be constructed. + */ + EGLSurface createWindowSurface(EGLDisplay display, EGLConfig config, + Object nativeWindow); + void destroySurface(EGLDisplay display, EGLSurface surface); + } + private static class DefaultWindowSurfaceFactory implements EGLWindowSurfaceFactory { + public EGLSurface createWindowSurface(EGLDisplay display, + EGLConfig config, Object nativeWindow) { + EGLSurface result = null; + try { + result = EGL14.eglCreateWindowSurface(display, config, nativeWindow, null, 0); + } catch (IllegalArgumentException e) { + // This exception indicates that the surface flinger surface + // is not valid. This can happen if the surface flinger surface has + // been torn down, but the application has not yet been + // notified via SurfaceHolder.Callback.surfaceDestroyed. + // In theory the application should be notified first, + // but in practice sometimes it is not. See b/4588890 + Log.e(TAG, "eglCreateWindowSurface", e); + } + return result; + } + public void destroySurface(EGLDisplay display, + EGLSurface surface) { + EGL14.eglDestroySurface(display, surface); + } + } + /** + * An interface for choosing an EGLConfig configuration from a list of + * potential configurations. + *

+ * This interface must be implemented by clients wishing to call + * {@link ExplicitGLSurfaceView#setEGLConfigChooser(EGLConfigChooser)} + */ + public interface EGLConfigChooser { + /** + * Choose a configuration from the list. Implementors typically + * implement this method by calling + * {@link EGL14#eglChooseConfig} and iterating through the results. Please consult the + * EGL specification available from The Khronos Group to learn how to call eglChooseConfig. + * @param display the current display. + * @return the chosen configuration. + */ + EGLConfig chooseConfig(EGLDisplay display); + } + private abstract class BaseConfigChooser + implements EGLConfigChooser { + public BaseConfigChooser(int[] configSpec) { + mConfigSpec = filterConfigSpec(configSpec); + } + public EGLConfig chooseConfig(EGLDisplay display) { + int[] num_config = new int[1]; + if (!EGL14.eglChooseConfig(display, mConfigSpec, 0, null, 0, 0, + num_config, 0)) { + throw new IllegalArgumentException("eglChooseConfig failed"); + } + int numConfigs = num_config[0]; + if (numConfigs <= 0) { + throw new IllegalArgumentException( + "No configs match configSpec"); + } + EGLConfig[] configs = new EGLConfig[numConfigs]; + if (!EGL14.eglChooseConfig(display, mConfigSpec, 0, configs, 0, numConfigs, + num_config, 0)) { + throw new IllegalArgumentException("eglChooseConfig#2 failed"); + } + EGLConfig config = chooseConfig(display, configs); + if (config == null) { + throw new IllegalArgumentException("No config chosen"); + } + return config; + } + abstract EGLConfig chooseConfig(EGLDisplay display, + EGLConfig[] configs); + protected int[] mConfigSpec; + private int[] filterConfigSpec(int[] configSpec) { + if (mEGLContextClientVersion != 2 && mEGLContextClientVersion != 3) { + return configSpec; + } + /* We know none of the subclasses define EGL_RENDERABLE_TYPE. + * And we know the configSpec is well formed. + */ + int len = configSpec.length; + int[] newConfigSpec = new int[len + 2]; + System.arraycopy(configSpec, 0, newConfigSpec, 0, len-1); + newConfigSpec[len-1] = EGL14.EGL_RENDERABLE_TYPE; + if (mEGLContextClientVersion == 2) { + newConfigSpec[len] = EGL14.EGL_OPENGL_ES2_BIT; /* EGL_OPENGL_ES2_BIT */ + } else { + newConfigSpec[len] = EGLExt.EGL_OPENGL_ES3_BIT_KHR; /* EGL_OPENGL_ES3_BIT_KHR */ + } + newConfigSpec[len+1] = EGL14.EGL_NONE; + return newConfigSpec; + } + } + /** + * Choose a configuration with exactly the specified r,g,b,a sizes, + * and at least the specified depth and stencil sizes. + */ + private class ComponentSizeChooser extends BaseConfigChooser { + public ComponentSizeChooser(int redSize, int greenSize, int blueSize, + int alphaSize, int depthSize, int stencilSize) { + super(new int[] { + EGL14.EGL_RED_SIZE, redSize, + EGL14.EGL_GREEN_SIZE, greenSize, + EGL14.EGL_BLUE_SIZE, blueSize, + EGL14.EGL_ALPHA_SIZE, alphaSize, + EGL14.EGL_DEPTH_SIZE, depthSize, + EGL14.EGL_STENCIL_SIZE, stencilSize, + EGL14.EGL_NONE}); + mValue = new int[1]; + mRedSize = redSize; + mGreenSize = greenSize; + mBlueSize = blueSize; + mAlphaSize = alphaSize; + mDepthSize = depthSize; + mStencilSize = stencilSize; + } + @Override + public EGLConfig chooseConfig(EGLDisplay display, + EGLConfig[] configs) { + for (EGLConfig config : configs) { + int d = findConfigAttrib(display, config, + EGL14.EGL_DEPTH_SIZE, 0); + int s = findConfigAttrib(display, config, + EGL14.EGL_STENCIL_SIZE, 0); + if ((d >= mDepthSize) && (s >= mStencilSize)) { + int r = findConfigAttrib(display, config, + EGL14.EGL_RED_SIZE, 0); + int g = findConfigAttrib(display, config, + EGL14.EGL_GREEN_SIZE, 0); + int b = findConfigAttrib(display, config, + EGL14.EGL_BLUE_SIZE, 0); + int a = findConfigAttrib(display, config, + EGL14.EGL_ALPHA_SIZE, 0); + if ((r == mRedSize) && (g == mGreenSize) + && (b == mBlueSize) && (a == mAlphaSize)) { + return config; + } + } + } + return null; + } + private int findConfigAttrib(EGLDisplay display, + EGLConfig config, int attribute, int defaultValue) { + if (EGL14.eglGetConfigAttrib(display, config, attribute, mValue, 0)) { + return mValue[0]; + } + return defaultValue; + } + private int[] mValue; + // Subclasses can adjust these values: + protected int mRedSize; + protected int mGreenSize; + protected int mBlueSize; + protected int mAlphaSize; + protected int mDepthSize; + protected int mStencilSize; + } + /** + * This class will choose a RGB_888 surface with + * or without a depth buffer. + * + */ + private class SimpleEGLConfigChooser extends ComponentSizeChooser { + public SimpleEGLConfigChooser(boolean withDepthBuffer) { + super(8, 8, 8, 0, withDepthBuffer ? 16 : 0, 0); + } + } + /** + * An EGL helper class. + */ + private static class EglHelper { + public EglHelper(WeakReference glSurfaceViewWeakRef) { + mGLSurfaceViewWeakRef = glSurfaceViewWeakRef; + } + /** + * Initialize EGL for a given configuration spec. + */ + public void start() { + if (LOG_EGL) { + Log.w("EglHelper", "start() tid=" + Thread.currentThread().getId()); + } + /* + * Get to the default display. + */ + mEglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + if (mEglDisplay == EGL14.EGL_NO_DISPLAY) { + throw new RuntimeException("eglGetDisplay failed"); + } + /* + * We can now initialize EGL for that display + */ + int[] major = new int[1]; + int[] minor = new int[1]; + if(!EGL14.eglInitialize(mEglDisplay, major, 0, minor, 0)) { + throw new RuntimeException("eglInitialize failed"); + } + ExplicitGLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view == null) { + mEglConfig = null; + mEglContext = null; + } else { + mEglConfig = view.mEGLConfigChooser.chooseConfig(mEglDisplay); + /* + * Create an EGL context. We want to do this as rarely as we can, because an + * EGL context is a somewhat heavy object. + */ + mEglContext = view.mEGLContextFactory.createContext(mEglDisplay, mEglConfig); + } + if (mEglContext == null || mEglContext == EGL14.EGL_NO_CONTEXT) { + mEglContext = null; + throwEglException("createContext"); + } + if (LOG_EGL) { + Log.w("EglHelper", "createContext " + mEglContext + " tid=" + Thread.currentThread().getId()); + } + mEglSurface = null; + } + /** + * Create an egl surface for the current SurfaceHolder surface. If a surface + * already exists, destroy it before creating the new surface. + * + * @return true if the surface was created successfully. + */ + public boolean createSurface() { + if (LOG_EGL) { + Log.w("EglHelper", "createSurface() tid=" + Thread.currentThread().getId()); + } + /* + * Check preconditions. + */ + if (mEglDisplay == null) { + throw new RuntimeException("eglDisplay not initialized"); + } + if (mEglConfig == null) { + throw new RuntimeException("mEglConfig not initialized"); + } + /* + * The window size has changed, so we need to create a new + * surface. + */ + destroySurfaceImp(); + /* + * Create an EGL surface we can render into. + */ + ExplicitGLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + mEglSurface = view.mEGLWindowSurfaceFactory.createWindowSurface( + mEglDisplay, mEglConfig, view.getHolder()); + } else { + mEglSurface = null; + } + if (mEglSurface == null || mEglSurface == EGL14.EGL_NO_SURFACE) { + int error = EGL14.eglGetError(); + if (error == EGL14.EGL_BAD_NATIVE_WINDOW) { + Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW."); + } + return false; + } + /* + * Before we can issue GL commands, we need to make sure + * the context is current and bound to a surface. + */ + if (!EGL14.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) { + /* + * Could not make the context current, probably because the underlying + * SurfaceView surface has been destroyed. + */ + logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", EGL14.eglGetError()); + return false; + } + return true; + } + + /** + * Display the current render surface. + * @return the EGL error code from eglSwapBuffers. + */ + public int swap() { + // if (! EGL14.eglSwapBuffers(mEglDisplay, mEglSurface)) { + if (!LauncherFix.INSTANCE.swapFrame(mEglDisplay.getNativeHandle(), mEglSurface.getNativeHandle())) { + return EGL14.eglGetError(); + } + return EGL14.EGL_SUCCESS; + } + public void destroySurface() { + if (LOG_EGL) { + Log.w("EglHelper", "destroySurface() tid=" + Thread.currentThread().getId()); + } + destroySurfaceImp(); + } + private void destroySurfaceImp() { + if (mEglSurface != null && mEglSurface != EGL14.EGL_NO_SURFACE) { + EGL14.eglMakeCurrent(mEglDisplay, EGL14.EGL_NO_SURFACE, + EGL14.EGL_NO_SURFACE, + EGL14.EGL_NO_CONTEXT); + ExplicitGLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mEGLWindowSurfaceFactory.destroySurface(mEglDisplay, mEglSurface); + } + mEglSurface = null; + } + } + public void finish() { + if (LOG_EGL) { + Log.w("EglHelper", "finish() tid=" + Thread.currentThread().getId()); + } + if (mEglContext != null) { + ExplicitGLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mEGLContextFactory.destroyContext(mEglDisplay, mEglContext); + } + mEglContext = null; + } + if (mEglDisplay != null) { + EGL14.eglTerminate(mEglDisplay); + mEglDisplay = null; + } + } + private void throwEglException(String function) { + throwEglException(function, EGL14.eglGetError()); + } + public static void throwEglException(String function, int error) { + String message = formatEglError(function, error); + if (LOG_THREADS) { + Log.e("EglHelper", "throwEglException tid=" + Thread.currentThread().getId() + " " + + message); + } + throw new RuntimeException(message); + } + public static void logEglErrorAsWarning(String tag, String function, int error) { + Log.w(tag, formatEglError(function, error)); + } + public static String formatEglError(String function, int error) { + return function + " failed: " + error; + } + private WeakReference mGLSurfaceViewWeakRef; + EGLDisplay mEglDisplay; + EGLSurface mEglSurface; + EGLConfig mEglConfig; + EGLContext mEglContext; + } + /** + * A generic GL Thread. Takes care of initializing EGL and GL. Delegates + * to a Renderer instance to do the actual drawing. Can be configured to + * render continuously or on request. + * + * All potentially blocking synchronization is done through the + * sGLThreadManager object. This avoids multiple-lock ordering issues. + * + */ + static class GLThread extends Thread { + GLThread(WeakReference glSurfaceViewWeakRef) { + super(); + mWidth = 0; + mHeight = 0; + mRequestRender = true; + mRenderMode = RENDERMODE_CONTINUOUSLY; + mWantRenderNotification = false; + mGLSurfaceViewWeakRef = glSurfaceViewWeakRef; + } + @Override + public void run() { + setName("GLThread " + getId()); + if (LOG_THREADS) { + Log.i("GLThread", "starting tid=" + getId()); + } + try { + guardedRun(); + } catch (InterruptedException e) { + // fall thru and exit normally + } finally { + sGLThreadManager.threadExiting(this); + } + } + /* + * This private method should only be called inside a + * synchronized(sGLThreadManager) block. + */ + private void stopEglSurfaceLocked() { + if (mHaveEglSurface) { + mHaveEglSurface = false; + mEglHelper.destroySurface(); + } + } + /* + * This private method should only be called inside a + * synchronized(sGLThreadManager) block. + */ + private void stopEglContextLocked() { + if (mHaveEglContext) { + mEglHelper.finish(); + mHaveEglContext = false; + sGLThreadManager.releaseEglContextLocked(this); + } + } + private void guardedRun() throws InterruptedException { + mEglHelper = new EglHelper(mGLSurfaceViewWeakRef); + mHaveEglContext = false; + mHaveEglSurface = false; + mWantRenderNotification = false; + try { + // GL10 gl = null; + boolean createEglContext = false; + boolean createEglSurface = false; + // boolean createGlInterface = false; + boolean lostEglContext = false; + boolean sizeChanged = false; + boolean wantRenderNotification = false; + boolean doRenderNotification = false; + boolean askedToReleaseEglContext = false; + int w = 0; + int h = 0; + Runnable event = null; + Runnable finishDrawingRunnable = null; + while (true) { + synchronized (sGLThreadManager) { + while (true) { + if (mShouldExit) { + return; + } + if (! mEventQueue.isEmpty()) { + event = mEventQueue.remove(0); + break; + } + // Update the pause state. + boolean pausing = false; + if (mPaused != mRequestPaused) { + pausing = mRequestPaused; + mPaused = mRequestPaused; + sGLThreadManager.notifyAll(); + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "mPaused is now " + mPaused + " tid=" + getId()); + } + } + // Do we need to give up the EGL context? + if (mShouldReleaseEglContext) { + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL context because asked to tid=" + getId()); + } + stopEglSurfaceLocked(); + stopEglContextLocked(); + mShouldReleaseEglContext = false; + askedToReleaseEglContext = true; + } + // Have we lost the EGL context? + if (lostEglContext) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + lostEglContext = false; + } + // When pausing, release the EGL surface: + if (pausing && mHaveEglSurface) { + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL surface because paused tid=" + getId()); + } + stopEglSurfaceLocked(); + } + // When pausing, optionally release the EGL Context: + if (pausing && mHaveEglContext) { + ExplicitGLSurfaceView view = mGLSurfaceViewWeakRef.get(); + boolean preserveEglContextOnPause = view == null ? + false : view.mPreserveEGLContextOnPause; + if (!preserveEglContextOnPause) { + stopEglContextLocked(); + if (LOG_SURFACE) { + Log.i("GLThread", "releasing EGL context because paused tid=" + getId()); + } + } + } + // Have we lost the SurfaceView surface? + if ((! mHasSurface) && (! mWaitingForSurface)) { + if (LOG_SURFACE) { + Log.i("GLThread", "noticed surfaceView surface lost tid=" + getId()); + } + if (mHaveEglSurface) { + stopEglSurfaceLocked(); + } + mWaitingForSurface = true; + mSurfaceIsBad = false; + sGLThreadManager.notifyAll(); + } + // Have we acquired the surface view surface? + if (mHasSurface && mWaitingForSurface) { + if (LOG_SURFACE) { + Log.i("GLThread", "noticed surfaceView surface acquired tid=" + getId()); + } + mWaitingForSurface = false; + sGLThreadManager.notifyAll(); + } + if (doRenderNotification) { + if (LOG_SURFACE) { + Log.i("GLThread", "sending render notification tid=" + getId()); + } + mWantRenderNotification = false; + doRenderNotification = false; + mRenderComplete = true; + sGLThreadManager.notifyAll(); + } + if (mFinishDrawingRunnable != null) { + finishDrawingRunnable = mFinishDrawingRunnable; + mFinishDrawingRunnable = null; + } + // Ready to draw? + if (readyToDraw()) { + // If we don't have an EGL context, try to acquire one. + if (! mHaveEglContext) { + if (askedToReleaseEglContext) { + askedToReleaseEglContext = false; + } else { + try { + mEglHelper.start(); + } catch (RuntimeException t) { + sGLThreadManager.releaseEglContextLocked(this); + throw t; + } + mHaveEglContext = true; + createEglContext = true; + sGLThreadManager.notifyAll(); + } + } + if (mHaveEglContext && !mHaveEglSurface) { + mHaveEglSurface = true; + createEglSurface = true; + // createGlInterface = true; + sizeChanged = true; + } + if (mHaveEglSurface) { + if (mSizeChanged) { + sizeChanged = true; + w = mWidth; + h = mHeight; + mWantRenderNotification = true; + if (LOG_SURFACE) { + Log.i("GLThread", + "noticing that we want render notification tid=" + + getId()); + } + // Destroy and recreate the EGL surface. + createEglSurface = true; + mSizeChanged = false; + } + mRequestRender = false; + sGLThreadManager.notifyAll(); + if (mWantRenderNotification) { + wantRenderNotification = true; + } + break; + } + } else { + if (finishDrawingRunnable != null) { + Log.w(TAG, "Warning, !readyToDraw() but waiting for " + + "draw finished! Early reporting draw finished."); + finishDrawingRunnable.run(); + finishDrawingRunnable = null; + } + } + // By design, this is the only place in a GLThread thread where we wait(). + if (LOG_THREADS) { + Log.i("GLThread", "waiting tid=" + getId() + + " mHaveEglContext: " + mHaveEglContext + + " mHaveEglSurface: " + mHaveEglSurface + + " mFinishedCreatingEglSurface: " + mFinishedCreatingEglSurface + + " mPaused: " + mPaused + + " mHasSurface: " + mHasSurface + + " mSurfaceIsBad: " + mSurfaceIsBad + + " mWaitingForSurface: " + mWaitingForSurface + + " mWidth: " + mWidth + + " mHeight: " + mHeight + + " mRequestRender: " + mRequestRender + + " mRenderMode: " + mRenderMode); + } + sGLThreadManager.wait(); + } + } // end of synchronized(sGLThreadManager) + if (event != null) { + event.run(); + event = null; + continue; + } + if (createEglSurface) { + if (LOG_SURFACE) { + Log.w("GLThread", "egl createSurface"); + } + if (mEglHelper.createSurface()) { + synchronized(sGLThreadManager) { + mFinishedCreatingEglSurface = true; + sGLThreadManager.notifyAll(); + } + } else { + synchronized(sGLThreadManager) { + mFinishedCreatingEglSurface = true; + mSurfaceIsBad = true; + sGLThreadManager.notifyAll(); + } + continue; + } + createEglSurface = false; + } + /* + if (createGlInterface) { + gl = (GL10) mEglHelper.createGL(); + createGlInterface = false; + } + */ + if (createEglContext) { + if (LOG_RENDERER) { + Log.w("GLThread", "onSurfaceCreated"); + } + ExplicitGLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + try { + // Trace.traceBegin(Trace.TRACE_TAG_VIEW, "onSurfaceCreated"); + view.mRenderer.onSurfaceCreated(mEglHelper.mEglConfig); + } finally { + // Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } + } + createEglContext = false; + } + if (sizeChanged) { + if (LOG_RENDERER) { + Log.w("GLThread", "onSurfaceChanged(" + w + ", " + h + ")"); + } + ExplicitGLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + try { + // Trace.traceBegin(Trace.TRACE_TAG_VIEW, "onSurfaceChanged"); + view.mRenderer.onSurfaceChanged(w, h); + } finally { + // Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } + } + sizeChanged = false; + } + if (LOG_RENDERER_DRAW_FRAME) { + Log.w("GLThread", "onDrawFrame tid=" + getId()); + } + { + ExplicitGLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + try { + // Trace.traceBegin(Trace.TRACE_TAG_VIEW, "onDrawFrame"); + view.mRenderer.onDrawFrame(); + if (finishDrawingRunnable != null) { + finishDrawingRunnable.run(); + finishDrawingRunnable = null; + } + requestRender(); + } finally { + // Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } + } + } + int swapError = mEglHelper.swap(); + switch (swapError) { + case EGL14.EGL_SUCCESS: + break; + case EGL14.EGL_CONTEXT_LOST: + if (LOG_SURFACE) { + Log.i("GLThread", "egl context lost tid=" + getId()); + } + lostEglContext = true; + break; + default: + // Other errors typically mean that the current surface is bad, + // probably because the SurfaceView surface has been destroyed, + // but we haven't been notified yet. + // Log the error to help developers understand why rendering stopped. + EglHelper.logEglErrorAsWarning("GLThread", "eglSwapBuffers", swapError); + synchronized(sGLThreadManager) { + mSurfaceIsBad = true; + sGLThreadManager.notifyAll(); + } + break; + } + if (wantRenderNotification) { + doRenderNotification = true; + wantRenderNotification = false; + } + } + } finally { + /* + * clean-up everything... + */ + synchronized (sGLThreadManager) { + stopEglSurfaceLocked(); + stopEglContextLocked(); + } + } + } + public boolean ableToDraw() { + return mHaveEglContext && mHaveEglSurface && readyToDraw(); + } + private boolean readyToDraw() { + return (!mPaused) && mHasSurface && (!mSurfaceIsBad) + && (mWidth > 0) && (mHeight > 0) + && (mRequestRender || (mRenderMode == RENDERMODE_CONTINUOUSLY)); + } + public void setRenderMode(int renderMode) { + if ( !((RENDERMODE_WHEN_DIRTY <= renderMode) && (renderMode <= RENDERMODE_CONTINUOUSLY)) ) { + throw new IllegalArgumentException("renderMode"); + } + synchronized(sGLThreadManager) { + mRenderMode = renderMode; + sGLThreadManager.notifyAll(); + } + } + public int getRenderMode() { + synchronized(sGLThreadManager) { + return mRenderMode; + } + } + public void requestRender() { + synchronized(sGLThreadManager) { + mRequestRender = true; + sGLThreadManager.notifyAll(); + } + } + public void requestRenderAndNotify(Runnable finishDrawing) { + synchronized(sGLThreadManager) { + // If we are already on the GL thread, this means a client callback + // has caused reentrancy, for example via updating the SurfaceView parameters. + // We will return to the client rendering code, so here we don't need to + // do anything. + if (Thread.currentThread() == this) { + return; + } + mWantRenderNotification = true; + mRequestRender = true; + mRenderComplete = false; + final Runnable oldCallback = mFinishDrawingRunnable; + mFinishDrawingRunnable = () -> { + if (oldCallback != null) { + oldCallback.run(); + } + if (finishDrawing != null) { + finishDrawing.run(); + } + }; + sGLThreadManager.notifyAll(); + } + } + public void surfaceCreated() { + synchronized(sGLThreadManager) { + if (LOG_THREADS) { + Log.i("GLThread", "surfaceCreated tid=" + getId()); + } + mHasSurface = true; + mFinishedCreatingEglSurface = false; + sGLThreadManager.notifyAll(); + while (mWaitingForSurface + && !mFinishedCreatingEglSurface + && !mExited) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + public void surfaceDestroyed() { + synchronized(sGLThreadManager) { + if (LOG_THREADS) { + Log.i("GLThread", "surfaceDestroyed tid=" + getId()); + } + mHasSurface = false; + sGLThreadManager.notifyAll(); + while((!mWaitingForSurface) && (!mExited)) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + } + public void onPause() { + synchronized (sGLThreadManager) { + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "onPause tid=" + getId()); + } + mRequestPaused = true; + sGLThreadManager.notifyAll(); + while ((! mExited) && (! mPaused)) { + if (LOG_PAUSE_RESUME) { + Log.i("Main thread", "onPause waiting for mPaused."); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + public void onResume() { + synchronized (sGLThreadManager) { + if (LOG_PAUSE_RESUME) { + Log.i("GLThread", "onResume tid=" + getId()); + } + mRequestPaused = false; + mRequestRender = true; + mRenderComplete = false; + sGLThreadManager.notifyAll(); + while ((! mExited) && mPaused && (!mRenderComplete)) { + if (LOG_PAUSE_RESUME) { + Log.i("Main thread", "onResume waiting for !mPaused."); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + public void onWindowResize(int w, int h) { + synchronized (sGLThreadManager) { + mWidth = w; + mHeight = h; + mSizeChanged = true; + mRequestRender = true; + mRenderComplete = false; + // If we are already on the GL thread, this means a client callback + // has caused reentrancy, for example via updating the SurfaceView parameters. + // We need to process the size change eventually though and update our EGLSurface. + // So we set the parameters and return so they can be processed on our + // next iteration. + if (Thread.currentThread() == this) { + return; + } + sGLThreadManager.notifyAll(); + // Wait for thread to react to resize and render a frame + while (! mExited && !mPaused && !mRenderComplete + && ableToDraw()) { + if (LOG_SURFACE) { + Log.i("Main thread", "onWindowResize waiting for render complete from tid=" + getId()); + } + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + public void requestExitAndWait() { + // don't call this from GLThread thread or it is a guaranteed + // deadlock! + synchronized(sGLThreadManager) { + mShouldExit = true; + sGLThreadManager.notifyAll(); + while (! mExited) { + try { + sGLThreadManager.wait(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + public void requestReleaseEglContextLocked() { + mShouldReleaseEglContext = true; + sGLThreadManager.notifyAll(); + } + /** + * Queue an "event" to be run on the GL rendering thread. + * @param r the runnable to be run on the GL rendering thread. + */ + public void queueEvent(Runnable r) { + if (r == null) { + throw new IllegalArgumentException("r must not be null"); + } + synchronized(sGLThreadManager) { + mEventQueue.add(r); + sGLThreadManager.notifyAll(); + } + } + // Once the thread is started, all accesses to the following member + // variables are protected by the sGLThreadManager monitor + private boolean mShouldExit; + private boolean mExited; + private boolean mRequestPaused; + private boolean mPaused; + private boolean mHasSurface; + private boolean mSurfaceIsBad; + private boolean mWaitingForSurface; + private boolean mHaveEglContext; + private boolean mHaveEglSurface; + private boolean mFinishedCreatingEglSurface; + private boolean mShouldReleaseEglContext; + private int mWidth; + private int mHeight; + private int mRenderMode; + private boolean mRequestRender; + private boolean mWantRenderNotification; + private boolean mRenderComplete; + private ArrayList mEventQueue = new ArrayList(); + private boolean mSizeChanged = true; + private Runnable mFinishDrawingRunnable = null; + // End of member variables protected by the sGLThreadManager monitor. + private EglHelper mEglHelper; + /** + * Set once at thread construction time, nulled out when the parent view is garbage + * called. This weak reference allows the GLSurfaceView to be garbage collected while + * the GLThread is still alive. + */ + private WeakReference mGLSurfaceViewWeakRef; + } + static class LogWriter extends Writer { + @Override public void close() { + flushBuilder(); + } + @Override public void flush() { + flushBuilder(); + } + @Override public void write(char[] buf, int offset, int count) { + for(int i = 0; i < count; i++) { + char c = buf[offset + i]; + if ( c == '\n') { + flushBuilder(); + } + else { + mBuilder.append(c); + } + } + } + private void flushBuilder() { + if (mBuilder.length() > 0) { + Log.v("GLSurfaceView", mBuilder.toString()); + mBuilder.delete(0, mBuilder.length()); + } + } + private StringBuilder mBuilder = new StringBuilder(); + } + private void checkRenderThreadState() { + if (mGLThread != null) { + throw new IllegalStateException( + "setRenderer has already been called for this instance."); + } + } + private static class GLThreadManager { + private static String TAG = "GLThreadManager"; + public synchronized void threadExiting(GLThread thread) { + if (LOG_THREADS) { + Log.i("GLThread", "exiting tid=" + thread.getId()); + } + thread.mExited = true; + notifyAll(); + } + /* + * Releases the EGL context. Requires that we are already in the + * sGLThreadManager monitor when this is called. + */ + public void releaseEglContextLocked(GLThread thread) { + notifyAll(); + } + } + private static final GLThreadManager sGLThreadManager = new GLThreadManager(); + private final WeakReference mThisWeakRef = + new WeakReference(this); + private GLThread mGLThread; + private Renderer mRenderer; + private boolean mDetached; + private EGLConfigChooser mEGLConfigChooser; + private EGLContextFactory mEGLContextFactory; + private EGLWindowSurfaceFactory mEGLWindowSurfaceFactory; + private int mDebugFlags; + private int mEGLContextClientVersion; + private boolean mPreserveEGLContextOnPause; +} diff --git a/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt b/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt index 15b507c..f99ad2e 100644 --- a/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt +++ b/app/src/main/java/com/geode/launcher/GeometryDashActivity.kt @@ -5,7 +5,9 @@ import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo -import android.opengl.GLSurfaceView +import android.opengl.EGL14 +import android.opengl.EGLConfig +import android.opengl.EGLDisplay import android.os.Build import android.os.Bundle import android.os.Environment @@ -43,9 +45,6 @@ import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException -import javax.microedition.khronos.egl.EGL10 -import javax.microedition.khronos.egl.EGLConfig -import javax.microedition.khronos.egl.EGLDisplay enum class DisplayMode { DEFAULT, LIMITED, FULLSCREEN; @@ -131,6 +130,8 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL } }) mGLSurfaceView?.manualBackEvents = true + + LauncherFix.initFramePacing(this) } private fun createVersionFile() { @@ -474,7 +475,8 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL val frameRate = mLimitedRefreshRate if (frameRate != null) { - renderer.limitFrameRate(frameRate) + glSurfaceView.setMaxFramerate(frameRate) + // renderer.limitFrameRate(frameRate) } editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS @@ -545,6 +547,8 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL override fun onDestroy() { super.onDestroy() FMOD.close() + + LauncherFix.destroyFramePacing() } private fun resumeGame() { @@ -666,11 +670,7 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL } } - class EGLConfigChooser : GLSurfaceView.EGLConfigChooser { - // this comes from EGL14, but is unavailable on EGL10 - // also EGL14 is incompatible with EGL10. so whatever - private var EGL_OPENGL_ES2_BIT: Int = 0x04 - + class EGLConfigChooser : ExplicitGLSurfaceView.EGLConfigChooser { private data class ConfigValues( val redSize: Int, val greenSize: Int, @@ -680,29 +680,29 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL val stencilSize: Int, ) - private fun getAttribValue(egl: EGL10, display: EGLDisplay, config: EGLConfig, attrib: Int): Int { + private fun getAttribValue(display: EGLDisplay, config: EGLConfig, attrib: Int): Int { val value = IntArray(1) - egl.eglGetConfigAttrib(display, config, attrib, value) + EGL14.eglGetConfigAttrib(display, config, attrib, value, 0) return value[0] } - private fun testConfig(egl: EGL10, display: EGLDisplay, attrib: ConfigValues): EGLConfig? { + private fun testConfig(display: EGLDisplay, attrib: ConfigValues): EGLConfig? { val attribList = intArrayOf( - EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, - EGL10.EGL_RED_SIZE, attrib.redSize, - EGL10.EGL_GREEN_SIZE, attrib.greenSize, - EGL10.EGL_BLUE_SIZE, attrib.blueSize, - EGL10.EGL_ALPHA_SIZE, attrib.alphaSize, - EGL10.EGL_DEPTH_SIZE, attrib.depthSize, - EGL10.EGL_STENCIL_SIZE, attrib.stencilSize, - EGL10.EGL_NONE + EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, + EGL14.EGL_RED_SIZE, attrib.redSize, + EGL14.EGL_GREEN_SIZE, attrib.greenSize, + EGL14.EGL_BLUE_SIZE, attrib.blueSize, + EGL14.EGL_ALPHA_SIZE, attrib.alphaSize, + EGL14.EGL_DEPTH_SIZE, attrib.depthSize, + EGL14.EGL_STENCIL_SIZE, attrib.stencilSize, + EGL14.EGL_NONE ) val configs = arrayOfNulls(1) val numConfigs = IntArray(1) - val res = egl.eglChooseConfig(display, attribList, configs, configs.size, numConfigs) + val res = EGL14.eglChooseConfig(display, attribList, 0, configs, 0, configs.size, numConfigs, 0) if (res && numConfigs[0] > 0 && configs[0] != null) { return configs[0] } @@ -710,18 +710,18 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL return null } - private fun printConfig(egl: EGL10, display: EGLDisplay, config: EGLConfig) { - val id = getAttribValue(egl, display, config, EGL10.EGL_CONFIG_ID) - val red = getAttribValue(egl, display, config, EGL10.EGL_RED_SIZE) - val green = getAttribValue(egl, display, config, EGL10.EGL_GREEN_SIZE) - val blue = getAttribValue(egl, display, config, EGL10.EGL_BLUE_SIZE) - val alpha = getAttribValue(egl, display, config, EGL10.EGL_ALPHA_SIZE) - val depth = getAttribValue(egl, display, config, EGL10.EGL_DEPTH_SIZE) - val stencil = getAttribValue(egl, display, config, EGL10.EGL_STENCIL_SIZE) + private fun printConfig(display: EGLDisplay, config: EGLConfig) { + val id = getAttribValue(display, config, EGL14.EGL_CONFIG_ID) + val red = getAttribValue(display, config, EGL14.EGL_RED_SIZE) + val green = getAttribValue(display, config, EGL14.EGL_GREEN_SIZE) + val blue = getAttribValue(display, config, EGL14.EGL_BLUE_SIZE) + val alpha = getAttribValue(display, config, EGL14.EGL_ALPHA_SIZE) + val depth = getAttribValue(display, config, EGL14.EGL_DEPTH_SIZE) + val stencil = getAttribValue(display, config, EGL14.EGL_STENCIL_SIZE) println("EGLConfig $id: (red = $red, green = $green, blue = $blue, alpha = $alpha, depth = $depth, stencil = $stencil)") } - override fun chooseConfig(egl: EGL10, display: EGLDisplay): EGLConfig { + override fun chooseConfig(display: EGLDisplay): EGLConfig { val configs = listOf( ConfigValues(8, 8, 8, 8, 16, 8), ConfigValues(8, 8, 8, 0, 16, 8), @@ -730,9 +730,9 @@ class GeometryDashActivity : AppCompatActivity(), Cocos2dxHelper.Cocos2dxHelperL ) return configs.firstNotNullOf { - val config = testConfig(egl, display, it) + val config = testConfig(display, it) if (config != null) { - printConfig(egl, display, config) + printConfig(display, config) } config diff --git a/app/src/main/java/com/geode/launcher/LauncherFix.kt b/app/src/main/java/com/geode/launcher/LauncherFix.kt index e926e88..301e46d 100644 --- a/app/src/main/java/com/geode/launcher/LauncherFix.kt +++ b/app/src/main/java/com/geode/launcher/LauncherFix.kt @@ -1,5 +1,7 @@ package com.geode.launcher +import android.app.Activity +import android.view.Surface import androidx.annotation.Keep import com.geode.launcher.utils.Constants @@ -16,4 +18,10 @@ object LauncherFix { external fun enableExceptionsRenaming() external fun performPatches() + + external fun initFramePacing(activity: Activity) + external fun destroyFramePacing() + external fun swapFrame(display: Long, surface: Long): Boolean + external fun setSurface(surface: Surface?) + external fun setSwapInterval(intervalNs: Long) } \ No newline at end of file diff --git a/app/src/main/java/org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt b/app/src/main/java/org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt index 381029d..169eff3 100644 --- a/app/src/main/java/org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt +++ b/app/src/main/java/org/cocos2dx/lib/Cocos2dxGLSurfaceView.kt @@ -17,15 +17,18 @@ import android.view.inputmethod.InputMethodManager import androidx.annotation.RequiresApi import androidx.appcompat.widget.AppCompatEditText import com.customRobTop.BaseRobTopActivity +import com.geode.launcher.ExplicitGLSurfaceView +import com.geode.launcher.LauncherFix import com.geode.launcher.utils.GeodeUtils import kotlin.math.abs +import kotlin.math.roundToLong private const val HANDLER_OPEN_IME_KEYBOARD = 2 private const val HANDLER_CLOSE_IME_KEYBOARD = 3 private const val MS_TO_NS = 1_000_000 private const val SEC_TO_NS = 1_000_000_000.0 -class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { +class Cocos2dxGLSurfaceView(context: Context) : ExplicitGLSurfaceView(context) { companion object { lateinit var cocos2dxGLSurfaceView: Cocos2dxGLSurfaceView private lateinit var handler: Handler @@ -64,6 +67,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { var controllerEventsEnabled = false private set var supportsResizeEvents = false + var maxFrameRate: Int? = null var cocos2dxEditText: AppCompatEditText? = null set(value) { @@ -139,6 +143,8 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { println("updateRefreshRate: selecting refresh rate of ${chosenDisplay.refreshRate} (display ${chosenDisplay.modeId})") + LauncherFix.setSwapInterval((1_000_000_000L / chosenRefreshRate).roundToLong()) + holder.surface.setFrameRate(chosenRefreshRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT) if (isAttachedToWindow) { (context as Activity).window.attributes.preferredRefreshRate = chosenRefreshRate @@ -153,7 +159,8 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { override fun onResume() { super.onResume() - renderMode = RENDERMODE_CONTINUOUSLY + renderMode = RENDERMODE_WHEN_DIRTY + // renderMode = RENDERMODE_CONTINUOUSLY queueEvent { cocos2dxRenderer.handleOnResume() } } @@ -531,6 +538,7 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { fun setCocos2dxRenderer(renderer: Cocos2dxRenderer) { this.cocos2dxRenderer = renderer setRenderer(this.cocos2dxRenderer) + renderMode = RENDERMODE_WHEN_DIRTY } private fun getContentText(): String { @@ -567,6 +575,12 @@ class Cocos2dxGLSurfaceView(context: Context) : GLSurfaceView(context) { inputManager.unregisterInputDeviceListener(InputListener) } + fun setMaxFramerate(rate: Int) { + // maxFrameRate = rate + cocos2dxRenderer.limitFrameRate(rate) + // LauncherFix.setSwapInterval((1_000_000_000.0 / rate).roundToLong()) + } + object InputListener : InputManager.InputDeviceListener { override fun onInputDeviceAdded(deviceId: Int) { GeodeUtils.inputDeviceAdded(deviceId) diff --git a/app/src/main/java/org/cocos2dx/lib/Cocos2dxRenderer.kt b/app/src/main/java/org/cocos2dx/lib/Cocos2dxRenderer.kt index f81e5cb..3f43d99 100644 --- a/app/src/main/java/org/cocos2dx/lib/Cocos2dxRenderer.kt +++ b/app/src/main/java/org/cocos2dx/lib/Cocos2dxRenderer.kt @@ -1,15 +1,14 @@ package org.cocos2dx.lib -import android.opengl.GLSurfaceView +import android.opengl.EGLConfig import android.os.Build -import javax.microedition.khronos.egl.EGLConfig -import javax.microedition.khronos.opengles.GL10 +import com.geode.launcher.ExplicitGLSurfaceView private const val NANOSECONDS_PER_SECOND = 1000000000L private const val NANOSECONDS_PER_MICROSECOND = 1000000L @Suppress("unused", "KotlinJniMissingFunction") -class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceView.Renderer { +class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : ExplicitGLSurfaceView.Renderer { companion object { @JvmStatic fun setAnimationInterval(@Suppress("UNUSED_PARAMETER") animationInterval: Double) { @@ -80,12 +79,12 @@ class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceVi screenHeight = surfaceHeight } - override fun onSurfaceCreated(gl10: GL10?, eglConfig: EGLConfig?) { + override fun onSurfaceCreated(eglConfig: EGLConfig?) { nativeInit(screenWidth, screenHeight) lastTickInNanoSeconds = System.nanoTime() } - override fun onSurfaceChanged(gl10: GL10?, width: Int, height: Int) { + override fun onSurfaceChanged(width: Int, height: Int) { if (setFrameRate && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { handler.updateRefreshRate() } @@ -93,7 +92,7 @@ class Cocos2dxRenderer(private var handler: Cocos2dxGLSurfaceView) : GLSurfaceVi println("renderer surfaceChanged: ${width}x${height}@${handler.display.refreshRate}fps") } - override fun onDrawFrame(gl: GL10?) { + override fun onDrawFrame() { val animationInterval = mAnimationInterval if (animationInterval == null) { nativeRender()