Merge "Adding kseniia@ to Game SDK owners"
diff --git a/.gitignore b/.gitignore
index c4c1c51..6dfd570 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,6 @@
 .DS_Store
 
-**/.idea
-
+# Gradle outputs or settings:
 **/.gradle
 
 **/local.properties
@@ -13,4 +12,10 @@
 
 **/gradle/wrapper/
 
+# IntelliJ/Android Studio outputs or settings:
+**/.idea
 **/*.iml
+**/out
+
+# Protocol Buffers:
+third_party/protobuf-3.0.0/python/dist/
\ No newline at end of file
diff --git a/README b/README
deleted file mode 100644
index 5a8c828..0000000
--- a/README
+++ /dev/null
@@ -1,15 +0,0 @@
-
-In order to build using prebuild NDK versions, this project must be initialized from a custom repo using:
-mkdir android-games-sdk
-cd android-games-sdk
-Corp -> repo init -u persistent-https://googleplex-android.git.corp.google.com/platform/manifest -b android-games-sdk
-AOSP -> repo init -u https://android.googlesource.com/platform/manifest -b android-games-sdk
-repo sync -c -j8
-
-Then:
-cd gamesdk
-ANDROID_HOME=../prebuilts/sdk ./gradlew gamesdkZip
-will build static and dynamic libraries for several NDK versions.
-
-By default, the gradle script builds target archiveZip, which will use a locally installed SDK/NDK pointed
-to by ANDROID_HOME (and ANDROID_NDK, if the ndk isn't in ANDROID_HOME/ndk-bundle).
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2f89017
--- /dev/null
+++ b/README.md
@@ -0,0 +1,58 @@
+# Android Game SDK
+
+## Build the Game SDK
+
+In order to build using prebuild NDK versions, this project must be initialized from a custom repo using:
+
+```bash
+mkdir android-games-sdk
+cd android-games-sdk
+repo init -u https://android.googlesource.com/platform/manifest -b android-games-sdk
+# Or for Googlers:
+# repo init -u persistent-https://googleplex-android.git.corp.google.com/platform/manifest -b android-games-sdk
+repo sync -c -j8
+```
+
+### Build with prebuilt SDKs
+
+```bash
+cd gamesdk
+ANDROID_HOME=../prebuilts/sdk ./gradlew gamesdkZip
+```
+
+will build static and dynamic libraries for several SDK/NDK pairs.
+
+### Build with locally installed SDK/NDK
+
+By default, the gradle script builds target `archiveZip`.
+
+```bash
+./gradlew archiveZip # Without Tuning Fork
+./gradlew archiveTfZip # With Tuning Fork
+```
+
+This will use a locally installed SDK/NDK pointed to by `ANDROID_HOME` (and `ANDROID_NDK`, if the ndk isn't in `ANDROID_HOME/ndk-bundle`).
+
+## Samples
+
+Samples are classic Android projects, using CMake to build the native code. They are also all triggering the build of the Game SDK.
+
+### Using Grade command line:
+
+```bash
+cd samples/bouncyball && ./gradlew assemble
+cd samples/cube && ./gradlew assemble
+cd samples/tuningfork/tftestapp && ./gradlew assemble
+```
+
+The Android SDK/NDK exposed using environment variables (`ANDROID_HOME`) will be used for building both the sample project and the Game SDK.
+
+### Using Android Studio
+
+Open projects using Android Studio:
+
+* `samples/bouncyball`
+* `samples/cube`
+* `samples/tuningfork/tftestapp`
+
+and run them directly (`Shift + F10` on Linux, `Control + R` on macOS). The local Android SDK/NDK (configured in Android Studio) will be used for building both the sample project and the Game SDK.
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index b7979f5..fb7bbc2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -367,6 +367,8 @@
 def sdkNativeBuild(withTuningFork = true, buildType="Release") {
     def threadChecks = false
     return defaultAbis().collectMany {
+     [ buildNativeModules(it, "14", "r16", "c++_static", threadChecks, withTuningFork, buildType) ] +
+     [ buildNativeModules(it, "16", "r17", "c++_static", threadChecks, withTuningFork, buildType) ] +
      [ buildNativeModules(it, "26", "r16", "gnustl_static", threadChecks, withTuningFork, buildType) ] +
      [ buildNativeModules(it, "28", "r17", "gnustl_static", threadChecks, withTuningFork, buildType) ] +
      [ buildNativeModules(it, "28", "r17", "c++_static", threadChecks, withTuningFork, buildType) ]
@@ -379,12 +381,12 @@
     return new LocalToolchain(project, kLocalMinSdk)
 }
 
-def localNativeBuild(withTuningFork = false, subdir = "src") {
+def localNativeBuild(withTuningFork = false, subdir = "src", buildType="Release") {
     def toolchain = getLocalToolchain();
     def stl = "c++_static"
     def threadChecks = true
     return defaultAbis().collect {
-        buildNativeModules(it, toolchain , stl, threadChecks, withTuningFork, subdir)
+        buildNativeModules(it, toolchain , stl, threadChecks, withTuningFork, subdir, buildType)
     }
 }
 
@@ -451,7 +453,7 @@
     ext.withStaticLibs = true;
     ext.withFullBuildKey = false;
     ext.buildType = "Release";
-    ext.nativeBuild = { tf,bt -> localNativeBuild(tf,bt) }
+    ext.nativeBuild = { tf,bt -> localNativeBuild(tf, "src", bt) }
 }
 
 // Build using local SDK, with tuning fork
@@ -464,7 +466,7 @@
     ext.withStaticLibs = true;
     ext.withFullBuildKey = false;
     ext.buildType = "Release";
-    ext.nativeBuild = { tf,bt -> localNativeBuild(tf,bt) }
+    ext.nativeBuild = { tf,bt -> localNativeBuild(tf, "src", bt) }
 }
 
 tasks.withType(BuildTask) {
diff --git a/include/swappy/swappyGL.h b/include/swappy/swappyGL.h
index 1d4c5d7..b777968 100644
--- a/include/swappy/swappyGL.h
+++ b/include/swappy/swappyGL.h
@@ -30,10 +30,11 @@
 #endif
 
 // Internal init function. Do not call directly.
-void SwappyGL_init_internal(JNIEnv *env, jobject jactivity);
+bool SwappyGL_init_internal(JNIEnv *env, jobject jactivity);
 
 // Initialize Swappy, getting the required Android parameters from the display subsystem via JNI
-static inline void SwappyGL_init(JNIEnv *env, jobject jactivity)  {
+// Returns false if swappy failed to initialize
+static inline bool SwappyGL_init(JNIEnv *env, jobject jactivity)  {
     // This call ensures that the header and the linked library are from the same version
     // (if not, a linker error will be triggered because of an undefined symbolP).
     SWAPPY_VERSION_SYMBOL();
@@ -53,7 +54,6 @@
 bool SwappyGL_swap(EGLDisplay display, EGLSurface surface);
 
 // Parameter setters
-void SwappyGL_setRefreshPeriod(uint64_t period_ns);
 void SwappyGL_setUseAffinity(bool tf);
 void SwappyGL_setSwapIntervalNS(uint64_t swap_ns);
 void SwappyGL_setFenceTimeoutNS(uint64_t fence_timeout_ns);
diff --git a/include/swappy/swappyGL_extra.h b/include/swappy/swappyGL_extra.h
index 3f52279..345c19f 100644
--- a/include/swappy/swappyGL_extra.h
+++ b/include/swappy/swappyGL_extra.h
@@ -46,6 +46,11 @@
 // dynamically, so the swap interval may change.
 void SwappyGL_setAutoSwapInterval(bool enabled);
 
+// Sets the maximal duration for auto-swap interval in milliseconds.
+// If swappy is operating in auto-swap interval and the frame duration is longer than 'max_swap_ns',
+// Swappy will not do any pacing and just submit the frame as soon as possible.
+void SwappyGL_setMaxAutoSwapIntervalNS(uint64_t max_swap_ns);
+
 // Toggle auto-pipeline mode on/off
 // By default, if auto-swap interval is on, auto-pipelining is on and Swappy will try to reduce
 // latency by scheduling cpu and gpu work in the same pipeline stage, if it fits.
diff --git a/include/swappy/swappyVk.h b/include/swappy/swappyVk.h
index a8f6996..2a11b89 100644
--- a/include/swappy/swappyVk.h
+++ b/include/swappy/swappyVk.h
@@ -241,6 +241,19 @@
 void SwappyVk_setAutoPipelineMode(bool enabled);
 
 /**
+ * Sets the maximal swap duration for all instances.
+ *
+ * Sets the maximal duration for Auto-Swap-Interval in milliseconds.
+ * If SwappyVk is operating in Auto-Swap-Interval and the frame duration is longer
+ * than the provided duration, SwappyVk will not do any pacing and just submit the
+ * frame as soon as possible.
+ * Parameters:
+ *
+ *  (IN)  max_swap_ns - maximal swap duration in milliseconds.
+ */
+void SwappyVk_setMaxAutoSwapIntervalNS(uint64_t max_swap_ns);
+
+/**
  * The fence timeout parameter can be set for devices with faulty
  * drivers. Its default value is 50,000,000.
  */
diff --git a/include/swappy/swappy_common.h b/include/swappy/swappy_common.h
index da26171..7f83f8b 100644
--- a/include/swappy/swappy_common.h
+++ b/include/swappy/swappy_common.h
@@ -27,7 +27,7 @@
 
 // Internal macros to track Swappy version, do not use directly.
 #define SWAPPY_MAJOR_VERSION 0
-#define SWAPPY_MINOR_VERSION 1
+#define SWAPPY_MINOR_VERSION 3
 #define SWAPPY_PACKED_VERSION ((SWAPPY_MAJOR_VERSION<<16)|(SWAPPY_MINOR_VERSION))
 
 // Internal macros to generate a symbol to track Swappy version, do not use directly.
diff --git a/samples/bouncyball/app/src/main/cpp/Settings.cpp b/samples/bouncyball/app/src/main/cpp/Settings.cpp
index 76823a1..6314b32 100644
--- a/samples/bouncyball/app/src/main/cpp/Settings.cpp
+++ b/samples/bouncyball/app/src/main/cpp/Settings.cpp
@@ -36,10 +36,8 @@
 }
 
 void Settings::setPreference(std::string key, std::string value) {
-    if (key == "refresh_period") {
-        SwappyGL_setRefreshPeriod(std::stoll(value));
-    } else if (key == "swap_interval") {
-        SwappyGL_setSwapIntervalNS(std::stoi(value) * 1e6);
+    if (key == "swap_interval") {
+        SwappyGL_setSwapIntervalNS(std::stod(value) * 1e6);
     } else if (key == "use_affinity") {
         SwappyGL_setUseAffinity(value == "true");
     } else if (key == "hot_pocket") {
diff --git a/samples/bouncyball/app/src/main/java/com/prefabulated/bouncyball/OrbitActivity.java b/samples/bouncyball/app/src/main/java/com/prefabulated/bouncyball/OrbitActivity.java
index aa663f5..347c744 100644
--- a/samples/bouncyball/app/src/main/java/com/prefabulated/bouncyball/OrbitActivity.java
+++ b/samples/bouncyball/app/src/main/java/com/prefabulated/bouncyball/OrbitActivity.java
@@ -229,8 +229,6 @@
         // Initialize the native renderer
 
         nInit();
-
-        nSetPreference("refresh_period", String.valueOf(refreshPeriodNanos));
     }
 
     private void infoOverlayToggle() {
diff --git a/samples/bouncyball/app/src/main/java/com/prefabulated/bouncyball/SettingsFragment.java b/samples/bouncyball/app/src/main/java/com/prefabulated/bouncyball/SettingsFragment.java
index 5fbab36..70620cf 100644
--- a/samples/bouncyball/app/src/main/java/com/prefabulated/bouncyball/SettingsFragment.java
+++ b/samples/bouncyball/app/src/main/java/com/prefabulated/bouncyball/SettingsFragment.java
@@ -44,10 +44,9 @@
     private ListPreference mSwapIntervalPreference;
     private String mSwapIntervalKey;
     private Display.Mode mCurrentMode;
-    private int mDisplayWidth;
 
     @TargetApi(Build.VERSION_CODES.M)
-    private boolean isModeValid(Display.Mode mode) {
+    private boolean modeMatchesCurrentResolution(Display.Mode mode) {
         return mode.getPhysicalHeight() == mCurrentMode.getPhysicalHeight() &&
                 mode.getPhysicalWidth() == mCurrentMode.getPhysicalWidth();
     }
@@ -76,11 +75,12 @@
             Display.Mode[] supportedModes =
                     getActivity().getWindowManager().getDefaultDisplay().getSupportedModes();
             for (Display.Mode mode : supportedModes) {
-                if (isModeValid(mode)) {
-                    float refreshRate = mode.getRefreshRate();
-                    for (int interval = 1; refreshRate / interval >= 20; interval++) {
-                        fpsSet.add((int) refreshRate / interval);
-                    }
+                if (!modeMatchesCurrentResolution(mode)) {
+                    continue;
+                }
+                float refreshRate = mode.getRefreshRate();
+                for (int interval = 1; refreshRate / interval >= 20; interval++) {
+                    fpsSet.add((int) refreshRate / interval);
                 }
             }
         } else {
diff --git a/samples/bouncyball/build.gradle b/samples/bouncyball/build.gradle
index 4699ea7..15b2a1c 100644
--- a/samples/bouncyball/build.gradle
+++ b/samples/bouncyball/build.gradle
@@ -6,7 +6,7 @@
         jcenter()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.3.2'
+        classpath 'com.android.tools.build:gradle:3.2.1'
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
     }
@@ -21,4 +21,4 @@
 
 task clean(type: Delete) {
     delete rootProject.buildDir
-}
+}
\ No newline at end of file
diff --git a/samples/bouncyball/gradle b/samples/bouncyball/gradle
new file mode 120000
index 0000000..1ce6c4c
--- /dev/null
+++ b/samples/bouncyball/gradle
@@ -0,0 +1 @@
+../../gradle
\ No newline at end of file
diff --git a/samples/bouncyball/gradle/wrapper/gradle-wrapper.jar b/samples/bouncyball/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index 13372ae..0000000
--- a/samples/bouncyball/gradle/wrapper/gradle-wrapper.jar
+++ /dev/null
Binary files differ
diff --git a/samples/bouncyball/gradle/wrapper/gradle-wrapper.properties b/samples/bouncyball/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index 92f5b28..0000000
--- a/samples/bouncyball/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-#Thu Mar 07 15:44:58 GMT 2019
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
diff --git a/samples/bouncyball/settings.gradle b/samples/bouncyball/settings.gradle
index a0abcdb..36c073e 100644
--- a/samples/bouncyball/settings.gradle
+++ b/samples/bouncyball/settings.gradle
@@ -1,3 +1,3 @@
 include ':app'
 include ':extras'
-project(':extras').projectDir = new File('../../src/extras')
+project(':extras').projectDir = new File('../../src/extras')
\ No newline at end of file
diff --git a/samples/cube/build.gradle b/samples/cube/build.gradle
index 4699ea7..51b175b 100644
--- a/samples/cube/build.gradle
+++ b/samples/cube/build.gradle
@@ -6,7 +6,7 @@
         jcenter()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.3.2'
+        classpath 'com.android.tools.build:gradle:3.2.1'
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
     }
diff --git a/samples/cube/gradle b/samples/cube/gradle
new file mode 120000
index 0000000..1ce6c4c
--- /dev/null
+++ b/samples/cube/gradle
@@ -0,0 +1 @@
+../../gradle
\ No newline at end of file
diff --git a/samples/cube/settings.gradle b/samples/cube/settings.gradle
index 9dde883..7e6eddb 100644
--- a/samples/cube/settings.gradle
+++ b/samples/cube/settings.gradle
@@ -1,2 +1,4 @@
 include ':app'
 project(':app').projectDir = new File('../../third_party/cube/app')
+include ':extras'
+project(':extras').projectDir = new File('../../src/extras')
diff --git a/samples/gamesdk.cmake b/samples/gamesdk.cmake
index 7ea6740..ffdfbd9 100644
--- a/samples/gamesdk.cmake
+++ b/samples/gamesdk.cmake
@@ -2,8 +2,9 @@
 
 # This function will create a static library target called 'gamesdk'.
 # The location of the library is set according to your ANDROID_NDK_REVISION
-# and ANDROID_PLATFORM, unless you explicitly set GAMESDK_NDK_VERSION
+# and ANDROID_PLATFORM, unless you explicitly set GAMESDK_NDK_VERSION,
 #   GAMESDK_ANDROID_SDK_VERSION or pass these as 4th and 5th arguments.
+#
 # Optional arguments, in order:
 #  PACKAGE_DIR: where the packaged version of the library is, relative to the gamesdk root dir
 #    default value: package/localtf
@@ -55,8 +56,11 @@
     if (NOT DEFINED GAMESDK_ANDROID_SDK_VERSION)
 		string(REGEX REPLACE "^android-([^.]+)" "\\1" GAMESDK_ANDROID_SDK_VERSION ${ANDROID_PLATFORM} )
     endif()
-    set(GAMESDK_PACKAGE_DIR "${_MY_DIR}/../../${GAMESDK_PACKAGE_DIR}")
-    set(BUILD_NAME ${ANDROID_ABI}_SDK${GAMESDK_ANDROID_SDK_VERSION}_NDK${GAMESDK_NDK_VERSION}_${ANDROID_STL})
+    string(REPLACE "+" "p" GAMESDK_ANDROID_STL ${ANDROID_STL}) # Game SDK build names use a sanitized STL name (c++ => cpp)
+
+    set(GAMESDK_ROOT_DIR "${_MY_DIR}/..")
+    set(GAMESDK_PACKAGE_DIR "${GAMESDK_ROOT_DIR}/../${GAMESDK_PACKAGE_DIR}")
+    set(BUILD_NAME ${ANDROID_ABI}_SDK${GAMESDK_ANDROID_SDK_VERSION}_NDK${GAMESDK_NDK_VERSION}_${GAMESDK_ANDROID_STL})
     set(GAMESDK_LIB_DIR "${GAMESDK_PACKAGE_DIR}/libs/${BUILD_NAME}")
 
     include_directories( "${GAMESDK_PACKAGE_DIR}/include" ) # Games SDK Public Includes
@@ -66,11 +70,27 @@
         set(GAMESDK_LIB ${DEP_LIB} PARENT_SCOPE)
     endif()
 
-    add_library( gamesdk STATIC IMPORTED GLOBAL)
+    # If building from a project containing local.properties, generated by Android Studio with
+    # the local Android SDK and NDK paths, copy it to gamesdk to allow it to build with the local
+    # toolchain.
+    if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../local.properties")
+        file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/../local.properties
+             DESTINATION ${GAMESDK_ROOT_DIR})
+    endif()
+
+    add_library(gamesdk STATIC IMPORTED GLOBAL)
     if(GAMESDK_DO_BUILD)
-        add_custom_command(OUTPUT ${DEP_LIB}
-            COMMAND ./gradlew ${GAMESDK_GEN_TASK} -PGAMESDK_ANDROID_SDK_VERSION=${GAMESDK_ANDROID_SDK_VERSION} VERBATIM
-            WORKING_DIRECTORY "${_MY_DIR}/.." )
+        # Build Game SDK (Gradle will use local.properties to find the Android SDK/NDK,
+        # or the environment variables if no local.properties - i.e: if compiling from command line).
+        add_custom_command(
+            OUTPUT
+                ${DEP_LIB}
+            COMMAND
+                ./gradlew ${GAMESDK_GEN_TASK} -PGAMESDK_ANDROID_SDK_VERSION=${GAMESDK_ANDROID_SDK_VERSION}
+            VERBATIM
+            WORKING_DIRECTORY
+                "${GAMESDK_ROOT_DIR}"
+        )
         add_custom_target(gamesdk_lib DEPENDS ${DEP_LIB})
         add_dependencies(gamesdk gamesdk_lib)
     endif()
diff --git a/samples/tuningfork/tftestapp/app/build.gradle b/samples/tuningfork/tftestapp/app/build.gradle
index 39f5e58..8c1711c 100644
--- a/samples/tuningfork/tftestapp/app/build.gradle
+++ b/samples/tuningfork/tftestapp/app/build.gradle
@@ -35,6 +35,8 @@
     implementation "com.google.android.gms:play-services-clearcut:16.0.0"
 
     testImplementation 'junit:junit:4.12'
+
+    implementation project(':extras')
 }
 
 task createJar(type: GradleBuild) {
diff --git a/samples/tuningfork/tftestapp/build.gradle b/samples/tuningfork/tftestapp/build.gradle
index 798ee14..df75d6a 100644
--- a/samples/tuningfork/tftestapp/build.gradle
+++ b/samples/tuningfork/tftestapp/build.gradle
@@ -7,7 +7,7 @@
         jcenter()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.3.2'
+        classpath 'com.android.tools.build:gradle:3.2.1'
 
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
diff --git a/samples/tuningfork/tftestapp/gradle b/samples/tuningfork/tftestapp/gradle
new file mode 120000
index 0000000..84b694e
--- /dev/null
+++ b/samples/tuningfork/tftestapp/gradle
@@ -0,0 +1 @@
+../../../gradle
\ No newline at end of file
diff --git a/samples/tuningfork/tftestapp/settings.gradle b/samples/tuningfork/tftestapp/settings.gradle
index e7b4def..752f3ad 100644
--- a/samples/tuningfork/tftestapp/settings.gradle
+++ b/samples/tuningfork/tftestapp/settings.gradle
@@ -1 +1,3 @@
 include ':app'
+include ':extras'
+project(':extras').projectDir = new File('../../../src/extras')
diff --git a/src/common/Trace.h b/src/common/Trace.h
index 78d6f70..1b0c460 100644
--- a/src/common/Trace.h
+++ b/src/common/Trace.h
@@ -100,7 +100,7 @@
     }
 
     void setCounter(const char *name, int64_t value) {
-        if (!ATrace_endSection || !isEnabled()) {
+        if (!ATrace_setCounter || !isEnabled()) {
             return;
         }
 
@@ -149,3 +149,4 @@
 #define PASTE_HELPER(a, b) PASTE_HELPER_HELPER(a, b)
 #define TRACE_CALL() gamesdk::ScopedTrace PASTE_HELPER(scopedTrace, __LINE__)(__PRETTY_FUNCTION__)
 #define TRACE_INT(name, value) gamesdk::Trace::getInstance()->setCounter(name, value)
+#define TRACE_ENABLED() gamesdk::Trace::getInstance()->isEnabled()
diff --git a/src/extras/src/main/java/com/google/androidgamesdk/SwappyDisplayManager.java b/src/extras/src/main/java/com/google/androidgamesdk/SwappyDisplayManager.java
new file mode 100644
index 0000000..94dba72
--- /dev/null
+++ b/src/extras/src/main/java/com/google/androidgamesdk/SwappyDisplayManager.java
@@ -0,0 +1,161 @@
+package com.google.androidgamesdk;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.hardware.display.DisplayManager;
+import android.os.Build;
+import android.util.Log;
+import android.view.Display;
+import android.view.Window;
+import android.view.WindowManager;
+
+import static android.app.NativeActivity.META_DATA_LIB_NAME;
+
+public class SwappyDisplayManager implements DisplayManager.DisplayListener {
+    final private String LOG_TAG = "SwappyDisplayManager";
+    final private boolean DEBUG = false;
+    final private long ONE_MS_IN_NS = 1000000;
+    final private long ONE_S_IN_NS = ONE_MS_IN_NS * 1000;
+
+    private long mCookie;
+    private Activity mActivity;
+    private WindowManager mWindowManager;
+    private Display.Mode mCurrentMode;
+
+    @TargetApi(Build.VERSION_CODES.M)
+    private boolean modeMatchesCurrentResolution(Display.Mode mode) {
+        return mode.getPhysicalHeight() == mCurrentMode.getPhysicalHeight() &&
+                mode.getPhysicalWidth() == mCurrentMode.getPhysicalWidth();
+
+    }
+
+    public SwappyDisplayManager(long cookie, Activity activity) {
+        // Load the native library for cases where an NDK application is running
+        // without a java componenet
+        try {
+            ActivityInfo ai = activity.getPackageManager().getActivityInfo(
+                    activity.getIntent().getComponent(), PackageManager.GET_META_DATA);
+            if (ai.metaData != null) {
+                String nativeLibName = ai.metaData.getString(META_DATA_LIB_NAME);
+                if (nativeLibName != null) {
+                    System.loadLibrary(nativeLibName);
+                }
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(LOG_TAG, e.getMessage());
+        }
+
+        mCookie = cookie;
+        mActivity = activity;
+
+        mWindowManager = mActivity.getSystemService(WindowManager.class);
+        Display display = mWindowManager.getDefaultDisplay();
+        mCurrentMode = display.getMode();
+        updateSupportedRefreshRates(display);
+
+        // Register display listener callbacks
+        DisplayManager dm = mActivity.getSystemService(DisplayManager.class);
+
+        synchronized(this) {
+            dm.registerDisplayListener(this, null);
+        }
+    }
+
+    private void updateSupportedRefreshRates(Display display) {
+        Display.Mode[] supportedModes = display.getSupportedModes();
+        int totalModes = 0;
+        for (int i = 0; i < supportedModes.length; i++) {
+            if (!modeMatchesCurrentResolution(supportedModes[i])) {
+                continue;
+            }
+            totalModes++;
+        }
+
+        long[] supportedRefreshRates = new long[totalModes];
+        int[] supportedRefreshRatesIds = new int[totalModes];
+        totalModes = 0;
+        for (int i = 0; i < supportedModes.length; i++) {
+            if (!modeMatchesCurrentResolution(supportedModes[i])) {
+                continue;
+            }
+            supportedRefreshRates[totalModes] =
+                    (long) (ONE_S_IN_NS / supportedModes[i].getRefreshRate());
+            supportedRefreshRatesIds[totalModes] = supportedModes[i].getModeId();
+            totalModes++;
+
+        }
+        // Call down to native to set the supported refresh rates
+        nSetSupportedRefreshRates(mCookie, supportedRefreshRates, supportedRefreshRatesIds);
+    }
+
+    public void setPreferredRefreshRate(final int modeId) {
+        mActivity.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                Window w = mActivity.getWindow();
+                WindowManager.LayoutParams l = w.getAttributes();
+                if (DEBUG) {
+                    Log.v(LOG_TAG, "set preferredDisplayModeId to " + modeId);
+                }
+                l.preferredDisplayModeId = modeId;
+
+
+                w.setAttributes(l);
+            }
+        });
+    }
+
+    @Override
+    public void onDisplayAdded(int displayId) {
+
+    }
+
+    @Override
+    public void onDisplayRemoved(int displayId) {
+
+    }
+
+    @Override
+    public void onDisplayChanged(int displayId) {
+        synchronized(this) {
+            Display display = mWindowManager.getDefaultDisplay();
+            float newRefreshRate = display.getRefreshRate();
+            Display.Mode newMode = display.getMode();
+            boolean resolutionChanged =
+                    (newMode.getPhysicalWidth() != mCurrentMode.getPhysicalWidth()) |
+                    (newMode.getPhysicalHeight() != mCurrentMode.getPhysicalHeight());
+            boolean refreshRateChanged = (newRefreshRate != mCurrentMode.getRefreshRate());
+            mCurrentMode = newMode;
+
+            if (resolutionChanged) {
+                updateSupportedRefreshRates(display);
+            }
+
+            if (refreshRateChanged) {
+                final long appVsyncOffsetNanos = display.getAppVsyncOffsetNanos();
+                final long vsyncPresentationDeadlineNanos =
+                        mWindowManager.getDefaultDisplay().getPresentationDeadlineNanos();
+
+                final long vsyncPeriodNanos = (long)(ONE_S_IN_NS / newRefreshRate);
+                final long sfVsyncOffsetNanos =
+                        vsyncPeriodNanos - (vsyncPresentationDeadlineNanos - ONE_MS_IN_NS);
+
+                nOnRefreshRateChanged(mCookie,
+                                     vsyncPeriodNanos,
+                                     appVsyncOffsetNanos,
+                                     sfVsyncOffsetNanos);
+            }
+        }
+    }
+
+    private native void nSetSupportedRefreshRates(long cookie,
+                                                  long[] refreshRates,
+                                                  int[] modeIds);
+    private native void nOnRefreshRateChanged(long cookie,
+                                              long refreshPeriod,
+                                              long appOffset,
+                                              long sfOffset);
+}
diff --git a/src/swappy/CMakeLists.txt b/src/swappy/CMakeLists.txt
index 958eb79..367ced1 100644
--- a/src/swappy/CMakeLists.txt
+++ b/src/swappy/CMakeLists.txt
@@ -38,6 +38,8 @@
              ${SOURCE_LOCATION_COMMON}/Thread.cpp
              ${SOURCE_LOCATION_COMMON}/SwappyCommon.cpp
              ${SOURCE_LOCATION_COMMON}/swappy_c.cpp
+             ${SOURCE_LOCATION_COMMON}/SwappyDisplayManager.cpp
+             ${SOURCE_LOCATION_COMMON}/CPUTracer.cpp
              ${SOURCE_LOCATION_OPENGL}/EGL.cpp
              ${SOURCE_LOCATION_OPENGL}/swappyGL_c.cpp
              ${SOURCE_LOCATION_OPENGL}/SwappyGL.cpp
diff --git a/src/swappy/common/CPUTracer.cpp b/src/swappy/common/CPUTracer.cpp
new file mode 100644
index 0000000..bffd5b8
--- /dev/null
+++ b/src/swappy/common/CPUTracer.cpp
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <memory>
+
+#include "CPUTracer.h"
+#include "../../common/Trace.h"
+#include "../../common/Log.h"
+
+namespace swappy {
+
+CPUTracer::CPUTracer() {}
+
+CPUTracer::~CPUTracer() {
+    joinThread();
+}
+
+void CPUTracer::joinThread() {
+    bool join = false;
+    if (mThread && mThread->joinable()) {
+        std::lock_guard<std::mutex> lock(mMutex);
+        mTrace = false;
+        mRunning = false;
+        mCond.notify_one();
+        join = true;
+    }
+    if (join) {
+        mThread->join();
+    }
+    mThread.reset();
+}
+
+void CPUTracer::startTrace() {
+    if (TRACE_ENABLED()) {
+        std::lock_guard<std::mutex> lock(mMutex);
+        if (!mThread) {
+            mRunning = true;
+            mThread = std::make_unique<std::thread>(&CPUTracer::threadMain, this);
+        }
+        mTrace = true;
+        mCond.notify_one();
+    } else {
+        joinThread();
+    }
+}
+
+void CPUTracer::endTrace() {
+    if (TRACE_ENABLED()) {
+        std::lock_guard<std::mutex> lock(mMutex);
+        mTrace = false;
+        mCond.notify_one();
+    } else {
+        joinThread();
+    }
+}
+
+void CPUTracer::threadMain() NO_THREAD_SAFETY_ANALYSIS {
+    std::unique_lock<std::mutex> lock(mMutex);
+    while (mRunning) {
+        if (mTrace) {
+            gamesdk::ScopedTrace trace("Swappy: CPU frame time");
+            mCond.wait(lock);
+        } else {
+            mCond.wait(lock);
+        }
+    }
+}
+
+} // namespace swappy
\ No newline at end of file
diff --git a/src/swappy/common/CPUTracer.h b/src/swappy/common/CPUTracer.h
new file mode 100644
index 0000000..99d1e98
--- /dev/null
+++ b/src/swappy/common/CPUTracer.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <condition_variable>
+#include <memory>
+#include <mutex>
+#include <thread>
+#include "Thread.h"
+
+namespace swappy {
+
+class CPUTracer {
+public:
+    CPUTracer();
+    ~CPUTracer();
+
+    CPUTracer(CPUTracer&) = delete;
+
+    void startTrace();
+    void endTrace();
+
+private:
+    void threadMain();
+    void joinThread();
+
+    std::mutex mMutex;
+    std::condition_variable_any mCond GUARDED_BY(mMutex);
+    std::unique_ptr<std::thread> mThread;
+    bool mRunning GUARDED_BY(mMutex) = true;
+    bool mTrace GUARDED_BY(mMutex) = false;
+};
+
+} // namespace swappy
\ No newline at end of file
diff --git a/src/swappy/common/ChoreographerFilter.cpp b/src/swappy/common/ChoreographerFilter.cpp
index f10f639..d15a78f 100644
--- a/src/swappy/common/ChoreographerFilter.cpp
+++ b/src/swappy/common/ChoreographerFilter.cpp
@@ -68,7 +68,8 @@
         }
 
         // TODO: 0.2 weighting factor for exponential smoothing is completely arbitrary
-        mBaseTime += mRefreshPeriod + delta * 2 / 10;
+        mRefreshPeriod += delta * 2 / 10;
+        mBaseTime += mRefreshPeriod;
 
         return true;
     }
@@ -88,7 +89,7 @@
     }
 
   private:
-    const std::chrono::nanoseconds mRefreshPeriod;
+    std::chrono::nanoseconds mRefreshPeriod;
     const std::chrono::nanoseconds mAppToSfDelay;
     time_point mBaseTime = std::chrono::steady_clock::now();
 
@@ -119,7 +120,7 @@
 }
 
 void ChoreographerFilter::onChoreographer() {
-    std::unique_lock<std::mutex> lock(mMutex);
+    std::lock_guard<std::mutex> lock(mMutex);
     mLastTimestamp = std::chrono::steady_clock::now();
     ++mSequenceNumber;
     mCondition.notify_all();
@@ -152,13 +153,20 @@
 
 void ChoreographerFilter::onSettingsChanged() {
     const bool useAffinity = Settings::getInstance()->getUseAffinity();
+    const Settings::DisplayTimings& displayTimings = Settings::getInstance()->getDisplayTimings();
     std::lock_guard<std::mutex> lock(mThreadPoolMutex);
-    if (useAffinity == mUseAffinity) {
+    if (useAffinity == mUseAffinity && mRefreshPeriod == displayTimings.refreshPeriod) {
         return;
     }
 
     terminateThreadsLocked();
     mUseAffinity = useAffinity;
+    mRefreshPeriod = displayTimings.refreshPeriod;
+    mAppToSfDelay = displayTimings.sfOffset - displayTimings.appOffset;
+    ALOGV("onSettingsChanged(): refreshPeriod=%lld, appOffset=%lld, sfOffset=%lld",
+          (long long)displayTimings.refreshPeriod.count(),
+          (long long)displayTimings.appOffset.count(),
+          (long long)displayTimings.sfOffset.count());
     launchThreadsLocked();
 }
 
diff --git a/src/swappy/common/ChoreographerFilter.h b/src/swappy/common/ChoreographerFilter.h
index 4f6c892..3d95937 100644
--- a/src/swappy/common/ChoreographerFilter.h
+++ b/src/swappy/common/ChoreographerFilter.h
@@ -21,6 +21,7 @@
 #include <vector>
 #include <mutex>
 #include <condition_variable>
+#include "Settings.h"
 
 namespace swappy {
 
@@ -57,8 +58,8 @@
     std::chrono::steady_clock::time_point mLastWorkRun;
     std::chrono::nanoseconds mWorkDuration;
 
-    const std::chrono::nanoseconds mRefreshPeriod;
-    const std::chrono::nanoseconds mAppToSfDelay;
+    std::chrono::nanoseconds mRefreshPeriod;
+    std::chrono::nanoseconds mAppToSfDelay;
     const Worker mDoWork;
 };
 
diff --git a/src/swappy/common/ChoreographerThread.cpp b/src/swappy/common/ChoreographerThread.cpp
index a8f3ede..e413cfb 100644
--- a/src/swappy/common/ChoreographerThread.cpp
+++ b/src/swappy/common/ChoreographerThread.cpp
@@ -54,6 +54,8 @@
 
 class NDKChoreographerThread : public ChoreographerThread {
 public:
+    static constexpr int MIN_SDK_VERSION = 24;
+
     NDKChoreographerThread(Callback onChoreographer);
     ~NDKChoreographerThread() override;
 
@@ -78,7 +80,7 @@
     mLibAndroid = dlopen("libandroid.so", RTLD_NOW | RTLD_LOCAL);
     if (mLibAndroid == nullptr) {
         ALOGE("FATAL: cannot open libandroid.so: %s", strerror(errno));
-        abort();
+        return;
     }
 
     mAChoreographer_getInstance =
@@ -97,7 +99,7 @@
         !mAChoreographer_postFrameCallback ||
         !mAChoreographer_postFrameCallbackDelayed) {
         ALOGE("FATAL: cannot get AChoreographer symbols");
-        abort();
+        return;
     }
 
     std::unique_lock<std::mutex> lock(mWaitingMutex);
@@ -107,6 +109,8 @@
     mWaitingCondition.wait(lock, [&]() REQUIRES(mWaitingMutex) {
         return mChoreographer != nullptr;
     });
+
+    mInitialized = true;
 }
 
 NDKChoreographerThread::~NDKChoreographerThread()
@@ -238,6 +242,8 @@
             env->NewObject(choreographerCallbackClass, constructor, reinterpret_cast<jlong>(this));
 
     mJobj = env->NewGlobalRef(choreographerCallback);
+
+    mInitialized = true;
 }
 
 JavaChoreographerThread::~JavaChoreographerThread()
@@ -289,7 +295,9 @@
 };
 
 NoChoreographerThread::NoChoreographerThread(Callback onChoreographer) :
-    ChoreographerThread(onChoreographer) {}
+    ChoreographerThread(onChoreographer) {
+    mInitialized = true;
+}
 
 
 void NoChoreographerThread::postFrameCallbacks() {
@@ -332,36 +340,6 @@
     mCallback();
 }
 
-int ChoreographerThread::getSDKVersion(JavaVM *vm)
-{
-    JNIEnv *env;
-    vm->AttachCurrentThread(&env, nullptr);
-
-    const jclass buildClass = env->FindClass("android/os/Build$VERSION");
-    if (env->ExceptionCheck()) {
-        env->ExceptionClear();
-        ALOGE("Failed to get Build.VERSION class");
-        return 0;
-    }
-
-    const jfieldID sdk_int = env->GetStaticFieldID(buildClass, "SDK_INT", "I");
-    if (env->ExceptionCheck()) {
-        env->ExceptionClear();
-        ALOGE("Failed to get Build.VERSION.SDK_INT field");
-        return 0;
-    }
-
-    const jint sdk = env->GetStaticIntField(buildClass, sdk_int);
-    if (env->ExceptionCheck()) {
-        env->ExceptionClear();
-        ALOGE("Failed to get SDK version");
-        return 0;
-    }
-
-    ALOGI("SDK version = %d", sdk);
-    return sdk;
-}
-
 bool ChoreographerThread::isChoreographerCallbackClassLoaded(JavaVM *vm)
 {
     JNIEnv *env;
@@ -378,13 +356,13 @@
 
 std::unique_ptr<ChoreographerThread>
     ChoreographerThread::createChoreographerThread(
-                Type type, JavaVM *vm, Callback onChoreographer) {
+                Type type, JavaVM *vm, Callback onChoreographer, int sdkVersion) {
     if (type == Type::App) {
         ALOGI("Using Application's Choreographer");
         return std::make_unique<NoChoreographerThread>(onChoreographer);
     }
 
-    if (vm == nullptr || getSDKVersion(vm) >= 24) {
+    if (vm == nullptr || sdkVersion >= NDKChoreographerThread::MIN_SDK_VERSION) {
         ALOGI("Using NDK Choreographer");
         return std::make_unique<NDKChoreographerThread>(onChoreographer);
     }
diff --git a/src/swappy/common/ChoreographerThread.h b/src/swappy/common/ChoreographerThread.h
index d06fb08..cf4383d 100644
--- a/src/swappy/common/ChoreographerThread.h
+++ b/src/swappy/common/ChoreographerThread.h
@@ -37,12 +37,14 @@
     using Callback = std::function<void()>;
 
     static std::unique_ptr<ChoreographerThread> createChoreographerThread(
-            Type type, JavaVM *vm, Callback onChoreographer);
+            Type type, JavaVM *vm, Callback onChoreographer, int sdkVersion);
 
     virtual ~ChoreographerThread() = 0;
 
     virtual void postFrameCallbacks();
 
+    bool isInitialized() { return mInitialized; }
+
 protected:
     ChoreographerThread(Callback onChoreographer);
     virtual void scheduleNextFrameCallback() REQUIRES(mWaitingMutex) = 0;
@@ -51,11 +53,11 @@
     std::mutex mWaitingMutex;
     int mCallbacksBeforeIdle GUARDED_BY(mWaitingMutex) = 0;
     Callback mCallback;
+    bool mInitialized = false;
 
     static constexpr int MAX_CALLBACKS_BEFORE_IDLE = 10;
 
 private:
-    static int getSDKVersion(JavaVM *vm);
     static bool isChoreographerCallbackClassLoaded(JavaVM *vm);
 };
 
diff --git a/src/swappy/common/FrameStatistics.cpp b/src/swappy/common/FrameStatistics.cpp
index c3ea474..1017c9d 100644
--- a/src/swappy/common/FrameStatistics.cpp
+++ b/src/swappy/common/FrameStatistics.cpp
@@ -35,7 +35,7 @@
 void FrameStatistics::updateFrames(EGLnsecsANDROID start, EGLnsecsANDROID end, uint64_t stat[]) {
     const uint64_t deltaTimeNano = end - start;
 
-    uint32_t numFrames = deltaTimeNano / mRefreshPeriod.count();
+    uint32_t numFrames = deltaTimeNano / mSwappyCommon.getRefreshPeriod().count();
     numFrames = std::min(numFrames, static_cast<uint32_t>(MAX_FRAME_BUCKETS));
     stat[numFrames]++;
 }
@@ -73,7 +73,7 @@
     const TimePoint frameStartTime = std::chrono::steady_clock::now();
 
     // first get the next frame id
-    std::pair<bool,EGLuint64KHR> nextFrameId = mEgl->getNextFrameId(dpy, surface);
+    std::pair<bool,EGLuint64KHR> nextFrameId = mEgl.getNextFrameId(dpy, surface);
     if (nextFrameId.first) {
         mPendingFrames.push_back({dpy, surface, nextFrameId.second, frameStartTime});
     }
@@ -93,7 +93,7 @@
     }
 
     std::unique_ptr<EGL::FrameTimestamps> frameStats =
-            mEgl->getFrameTimestamps(frame.dpy, frame.surface, frame.id);
+            mEgl.getFrameTimestamps(frame.dpy, frame.surface, frame.id);
 
     if (!frameStats) {
         return;
diff --git a/src/swappy/common/FrameStatistics.h b/src/swappy/common/FrameStatistics.h
index 7c5e996..e8f1b05 100644
--- a/src/swappy/common/FrameStatistics.h
+++ b/src/swappy/common/FrameStatistics.h
@@ -17,6 +17,7 @@
 #pragma once
 
 #include "EGL.h"
+#include "SwappyCommon.h"
 #include "Thread.h"
 
 #include <array>
@@ -33,9 +34,8 @@
 
 class FrameStatistics {
 public:
-    explicit FrameStatistics(std::shared_ptr<EGL> egl,
-                             std::chrono::nanoseconds refreshPeriod) :
-            mEgl(egl), mRefreshPeriod(refreshPeriod) {};
+    FrameStatistics(const EGL& egl, const SwappyCommon& swappyCommon)
+        : mEgl(egl), mSwappyCommon(swappyCommon) {};
     ~FrameStatistics() = default;
 
     void capture(EGLDisplay dpy, EGLSurface surface);
@@ -54,8 +54,8 @@
                              TimePoint frameStartTime) REQUIRES(mMutex);
     void logFrames() REQUIRES(mMutex);
 
-    std::shared_ptr<EGL> mEgl;
-    const std::chrono::nanoseconds mRefreshPeriod;
+    const EGL& mEgl;
+    const SwappyCommon& mSwappyCommon;
 
     struct EGLFrame {
         EGLDisplay dpy;
diff --git a/src/swappy/common/Settings.cpp b/src/swappy/common/Settings.cpp
index 4c5a7ee..7f0f36b 100644
--- a/src/swappy/common/Settings.cpp
+++ b/src/swappy/common/Settings.cpp
@@ -40,10 +40,10 @@
     mListeners.emplace_back(std::move(listener));
 }
 
-void Settings::setRefreshPeriod(std::chrono::nanoseconds period) {
+void Settings::setDisplayTimings(const DisplayTimings& displayTimings) {
     {
         std::lock_guard<std::mutex> lock(mMutex);
-        mRefreshPeriod = period;
+        mDisplayTimings = displayTimings;
     }
     // Notify the listeners without the lock held
     notifyListeners();
@@ -66,10 +66,9 @@
     notifyListeners();
 }
 
-
-std::chrono::nanoseconds Settings::getRefreshPeriod() const {
+const Settings::DisplayTimings& Settings::getDisplayTimings() const {
     std::lock_guard<std::mutex> lock(mMutex);
-    return mRefreshPeriod;
+    return mDisplayTimings;
 }
 
 uint64_t Settings::getSwapIntervalNS() const {
diff --git a/src/swappy/common/Settings.h b/src/swappy/common/Settings.h
index 857fe53..84a39ea 100644
--- a/src/swappy/common/Settings.h
+++ b/src/swappy/common/Settings.h
@@ -34,6 +34,12 @@
     struct ConstructorTag {
     };
   public:
+    struct DisplayTimings {
+        std::chrono::nanoseconds refreshPeriod{0};
+        std::chrono::nanoseconds appOffset{0};
+        std::chrono::nanoseconds sfOffset{0};
+    };
+
     explicit Settings(ConstructorTag) {};
 
     static Settings *getInstance();
@@ -43,11 +49,11 @@
     using Listener = std::function<void()>;
     void addListener(Listener listener);
 
-    void setRefreshPeriod(std::chrono::nanoseconds period);
+    void setDisplayTimings(const DisplayTimings& displayTimings);
     void setSwapIntervalNS(uint64_t swap_ns);
     void setUseAffinity(bool);
 
-    std::chrono::nanoseconds getRefreshPeriod() const;
+    const DisplayTimings& getDisplayTimings() const;
     uint64_t getSwapIntervalNS() const;
     bool getUseAffinity() const;
 
@@ -59,8 +65,7 @@
     mutable std::mutex mMutex;
     std::vector<Listener> mListeners GUARDED_BY(mMutex);
 
-    std::chrono::nanoseconds
-        mRefreshPeriod GUARDED_BY(mMutex) = std::chrono::nanoseconds{12'345'678};
+    DisplayTimings mDisplayTimings GUARDED_BY(mMutex);
     uint64_t mSwapIntervalNS GUARDED_BY(mMutex) = 16666667L;
     bool mUseAffinity GUARDED_BY(mMutex) = true;
 };
diff --git a/src/swappy/common/SwappyCommon.cpp b/src/swappy/common/SwappyCommon.cpp
index a9a4954..cdde397 100644
--- a/src/swappy/common/SwappyCommon.cpp
+++ b/src/swappy/common/SwappyCommon.cpp
@@ -33,12 +33,14 @@
 using std::chrono::nanoseconds;
 
 // NB These are only needed for C++14
-constexpr std::chrono::nanoseconds SwappyCommon::FrameDuration::MAX_DURATION;
-constexpr std::chrono::nanoseconds SwappyCommon::FRAME_HYSTERESIS;
+constexpr nanoseconds SwappyCommon::FrameDuration::MAX_DURATION;
+constexpr nanoseconds SwappyCommon::FRAME_MARGIN;
+constexpr nanoseconds SwappyCommon::EDGE_HYSTERESIS;
+constexpr nanoseconds SwappyCommon::REFRESH_RATE_MARGIN;
 
 SwappyCommon::SwappyCommon(JNIEnv *env, jobject jactivity)
-        : mSwapDuration(std::chrono::nanoseconds(0)),
-          mSwapInterval(1),
+        : mSdkVersion(getSDKVersion(env)),
+          mSwapDuration(nanoseconds(0)),
           mAutoSwapInterval(1),
           mValid(false) {
     jclass activityClass = env->FindClass("android/app/NativeActivity");
@@ -103,36 +105,56 @@
     env->GetJavaVM(&vm);
 
     using std::chrono::nanoseconds;
-    mRefreshPeriod  = nanoseconds(vsyncPeriodNanos);
-    mAppVsyncOffset = nanoseconds(appVsyncOffsetNanos);
-    mSfVsyncOffset  = nanoseconds(sfVsyncOffsetNanos);
+    mRefreshPeriod = nanoseconds(vsyncPeriodNanos);
+    nanoseconds appVsyncOffset = nanoseconds(appVsyncOffsetNanos);
+    nanoseconds sfVsyncOffset  = nanoseconds(sfVsyncOffsetNanos);
 
     mChoreographerFilter = std::make_unique<ChoreographerFilter>(mRefreshPeriod,
-                                                                 mSfVsyncOffset - mAppVsyncOffset,
+                                                                 sfVsyncOffset - appVsyncOffset,
                                                                  [this]() { return wakeClient(); });
 
-   mChoreographerThread = ChoreographerThread::createChoreographerThread(
+    mChoreographerThread = ChoreographerThread::createChoreographerThread(
                                    ChoreographerThread::Type::Swappy,
                                    vm,
-                                   [this]{ mChoreographerFilter->onChoreographer(); });
+                                   [this]{ mChoreographerFilter->onChoreographer(); },
+                                   mSdkVersion);
+    if (!mChoreographerThread->isInitialized()) {
+        ALOGE("failed to initialize ChoreographerThread");
+        return;
+    }
+
+    if (USE_DISPLAY_MANAGER && mSdkVersion >= SwappyDisplayManager::MIN_SDK_VERSION) {
+        mDisplayManager = std::make_unique<SwappyDisplayManager>(vm, jactivity);
+        if (!mDisplayManager->isInitialized()) {
+            ALOGE("failed to initialize DisplayManager");
+            return;
+        }
+    }
 
     Settings::getInstance()->addListener([this]() { onSettingsChanged(); });
+    Settings::getInstance()->setDisplayTimings({mRefreshPeriod, appVsyncOffset, sfVsyncOffset});
 
-    mAutoSwapIntervalThreshold = (1e9f / mRefreshPeriod.count()) / 20; // 20FPS
+    std::lock_guard<std::mutex> lock(mFrameDurationsMutex);
     mFrameDurations.reserve(mFrameDurationSamples);
 
+    ALOGI("Initialized Swappy with vsyncPeriod=%lld, appOffset=%lld, sfOffset=%lld",
+          (long long)vsyncPeriodNanos,
+          (long long)appVsyncOffset.count(),
+          (long long)sfVsyncOffset.count()
+    );
+
     mValid = true;
 }
 
 SwappyCommon::~SwappyCommon() {
     // destroy all threads first before the other members of this class
-    mChoreographerFilter.reset();
     mChoreographerThread.reset();
+    mChoreographerFilter.reset();
 
     Settings::reset();
 }
 
-std::chrono::nanoseconds SwappyCommon::wakeClient() {
+nanoseconds SwappyCommon::wakeClient() {
     std::lock_guard<std::mutex> lock(mWaitingMutex);
     ++mCurrentFrame;
 
@@ -153,7 +175,8 @@
                 ChoreographerThread::createChoreographerThread(
                         ChoreographerThread::Type::App,
                         nullptr,
-                        [this] { mChoreographerFilter->onChoreographer(); });
+                        [this] { mChoreographerFilter->onChoreographer(); },
+                        mSdkVersion);
     }
 
     mChoreographerThread->postFrameCallbacks();
@@ -163,13 +186,14 @@
     int lateFrames = 0;
     bool presentationTimeIsNeeded;
 
-    const std::chrono::nanoseconds cpuTime = std::chrono::steady_clock::now() - mStartFrameTime;
+    const nanoseconds cpuTime = std::chrono::steady_clock::now() - mStartFrameTime;
+    mCPUTracer.endTrace();
 
     preWaitCallbacks();
 
     // if we are running slower than the threshold there is no point to sleep, just let the
     // app run as fast as it can
-    if (mAutoSwapInterval <= mAutoSwapIntervalThreshold) {
+    if (mRefreshPeriod * mAutoSwapInterval <= mAutoSwapIntervalThresholdNS.load()) {
         waitUntilTargetFrame();
 
         // wait for the previous frame to be rendered
@@ -184,13 +208,62 @@
         presentationTimeIsNeeded = false;
     }
 
-    const std::chrono::nanoseconds gpuTime = h.getPrevFrameGpuTime();
+    const nanoseconds gpuTime = h.getPrevFrameGpuTime();
     addFrameDuration({cpuTime, gpuTime});
     postWaitCallbacks();
 
     return presentationTimeIsNeeded;
 }
 
+void SwappyCommon::updateDisplayTimings() {
+    // grab a pointer to the latest supported refresh rates
+    if (mDisplayManager) {
+        mSupportedRefreshRates = mDisplayManager->getSupportedRefreshRates();
+    }
+
+    std::lock_guard<std::mutex> lock(mFrameDurationsMutex);
+    if (!mTimingSettingsNeedUpdate) {
+        return;
+    }
+
+    mTimingSettingsNeedUpdate = false;
+
+    if (mRefreshPeriod == mNextTimingSettings.refreshPeriod &&
+        mSwapIntervalNS == mNextTimingSettings.swapIntervalNS) {
+        return;
+    }
+
+    mAutoSwapInterval = mSwapIntervalForNewRefresh;
+    mPipelineMode = mPipelineModeForNewRefresh;
+    mSwapIntervalForNewRefresh = 0;
+
+    const bool swapIntervalValid = mNextTimingSettings.refreshPeriod * mAutoSwapInterval >=
+                                   mNextTimingSettings.swapIntervalNS;
+    const bool swapIntervalChangedBySettings = mSwapIntervalNS !=
+                                               mNextTimingSettings.swapIntervalNS;
+
+    mRefreshPeriod = mNextTimingSettings.refreshPeriod;
+    mSwapIntervalNS = mNextTimingSettings.swapIntervalNS;
+    if (!mAutoSwapIntervalEnabled || swapIntervalChangedBySettings ||
+            mAutoSwapInterval == 0 || !swapIntervalValid) {
+        mAutoSwapInterval = calculateSwapInterval(mSwapIntervalNS, mRefreshPeriod);
+        mPipelineMode = mAutoSwapIntervalEnabled ? PipelineMode::Off : PipelineMode::On;
+        setPreferredRefreshRate(mSwapIntervalNS);
+    }
+
+    if (mNextModeId == -1) {
+        setPreferredRefreshRate(mSwapIntervalNS);
+    }
+
+    mFrameDurations.clear();
+    mFrameDurationsSum = {};
+
+    TRACE_INT("mSwapIntervalNS", int(mSwapIntervalNS.count()));
+    TRACE_INT("mAutoSwapInterval", mAutoSwapInterval);
+    TRACE_INT("mRefreshPeriod", mRefreshPeriod.count());
+    TRACE_INT("mPipelineMode", static_cast<int>(mPipelineMode));
+}
+
 void SwappyCommon::onPreSwap(const SwapHandlers& h) {
     if (!mUsingExternalChoreographer) {
         mChoreographerThread->postFrameCallbacks();
@@ -198,10 +271,11 @@
 
     // for non pipeline mode where both cpu and gpu work is done at the same stage
     // wait for next frame will happen after swap
-    if (mPipelineMode) {
+    if (mPipelineMode == PipelineMode::On) {
         mPresentationTimeNeeded = waitForNextFrame(h);
     } else {
-        mPresentationTimeNeeded = mAutoSwapInterval <= mAutoSwapIntervalThreshold;
+        mPresentationTimeNeeded =
+                (mRefreshPeriod * mAutoSwapInterval <= mAutoSwapIntervalThresholdNS.load());
     }
 
     mSwapTime = std::chrono::steady_clock::now();
@@ -211,22 +285,25 @@
 void SwappyCommon::onPostSwap(const SwapHandlers& h) {
     postSwapBuffersCallbacks();
 
-    if (updateSwapInterval()) {
-        swapIntervalChangedCallbacks();
-        TRACE_INT("mPipelineMode", mPipelineMode);
-        TRACE_INT("mAutoSwapInterval", mAutoSwapInterval);
-    }
 
     updateSwapDuration(std::chrono::steady_clock::now() - mSwapTime);
 
-    if (!mPipelineMode) {
+    if (mPipelineMode == PipelineMode::Off) {
         waitForNextFrame(h);
     }
 
+    if (updateSwapInterval()) {
+        swapIntervalChangedCallbacks();
+        TRACE_INT("mPipelineMode", static_cast<int>(mPipelineMode));
+        TRACE_INT("mAutoSwapInterval", mAutoSwapInterval);
+    }
+
+    updateDisplayTimings();
+
     startFrame();
 }
 
-void SwappyCommon::updateSwapDuration(std::chrono::nanoseconds duration) {
+void SwappyCommon::updateSwapDuration(nanoseconds duration) {
     // TODO: The exponential smoothing factor here is arbitrary
     mSwapDuration = (mSwapDuration.load() * 4 / 5) + duration / 5;
 
@@ -240,7 +317,7 @@
 
 uint64_t SwappyCommon::getSwapIntervalNS() {
     std::lock_guard<std::mutex> lock(mFrameDurationsMutex);
-    return mAutoSwapInterval.load() * mRefreshPeriod.count();
+    return mAutoSwapInterval * mRefreshPeriod.count();
 };
 
 void SwappyCommon::addFrameDuration(FrameDuration duration) {
@@ -258,52 +335,71 @@
     mFrameDurationsSum += duration;
 }
 
+bool SwappyCommon::pipelineModeNotNeeded(const nanoseconds& averageFrameTime,
+                                         const nanoseconds& upperBound) {
+    return (mPipelineModeAutoMode && averageFrameTime < upperBound);
+}
+
 void SwappyCommon::swapSlower(const FrameDuration& averageFrameTime,
-                        const std::chrono::nanoseconds& upperBound,
-                        const std::chrono::nanoseconds& lowerBound,
+                        const nanoseconds& upperBound,
                         const int32_t& newSwapInterval) {
     ALOGV("Rendering takes too much time for the given config");
 
-    if (!mPipelineMode && averageFrameTime.getTime(true) <= upperBound) {
+    if (mPipelineMode == PipelineMode::Off &&
+            averageFrameTime.getTime(PipelineMode::On) <= upperBound) {
         ALOGV("turning on pipelining");
-        mPipelineMode = true;
+        mPipelineMode = PipelineMode::On;
     } else {
         mAutoSwapInterval = newSwapInterval;
-        ALOGV("Changing Swap interval to %d", mAutoSwapInterval.load());
+        ALOGV("Changing Swap interval to %d", mAutoSwapInterval);
 
         // since we changed the swap interval, we may be able to turn off pipeline mode
-        nanoseconds newBound = mRefreshPeriod * mAutoSwapInterval.load();
-        newBound -= (FRAME_HYSTERESIS * 2);
-        if (mPipelineModeAutoMode && averageFrameTime.getTime(false) < newBound) {
+        const nanoseconds newUpperBound = mRefreshPeriod * mAutoSwapInterval;
+        if (pipelineModeNotNeeded(averageFrameTime.getTime(PipelineMode::Off) + FRAME_MARGIN,
+                                  newUpperBound)) {
             ALOGV("Turning off pipelining");
-            mPipelineMode = false;
+            mPipelineMode = PipelineMode::Off;
         } else {
             ALOGV("Turning on pipelining");
-            mPipelineMode = true;
+            mPipelineMode = PipelineMode::On;
         }
     }
 }
 
 void SwappyCommon::swapFaster(const FrameDuration& averageFrameTime,
-                        const std::chrono::nanoseconds& upperBound,
-                        const std::chrono::nanoseconds& lowerBound,
+                        const nanoseconds& lowerBound,
                         const int32_t& newSwapInterval) {
+
+
     ALOGV("Rendering is much shorter for the given config");
     mAutoSwapInterval = newSwapInterval;
-    ALOGV("Changing Swap interval to %d", mAutoSwapInterval.load());
+    ALOGV("Changing Swap interval to %d", mAutoSwapInterval);
 
     // since we changed the swap interval, we may need to turn on pipeline mode
-    nanoseconds newBound = mRefreshPeriod * mAutoSwapInterval.load();
-    newBound -= FRAME_HYSTERESIS;
-    if (!mPipelineModeAutoMode || averageFrameTime.getTime(false) > newBound) {
-        ALOGV("Turning on pipelining");
-        mPipelineMode = true;
-    } else {
+    const nanoseconds newUpperBound = mRefreshPeriod * mAutoSwapInterval;
+    if (pipelineModeNotNeeded(averageFrameTime.getTime(PipelineMode::Off) + FRAME_MARGIN,
+                              newUpperBound)) {
         ALOGV("Turning off pipelining");
-        mPipelineMode = false;
+        mPipelineMode = PipelineMode::Off;
+    } else {
+        ALOGV("Turning on pipelining");
+        mPipelineMode = PipelineMode::On;
     }
 }
 
+bool SwappyCommon::isSameDuration(std::chrono::nanoseconds period1, int interval1,
+                                  std::chrono::nanoseconds period2, int interval2) {
+    static constexpr std::chrono::nanoseconds MARGIN = 1ms;
+
+    auto duration1 = period1 * interval1;
+    auto duration2 = period2 * interval2;
+
+    if (std::max(duration1, duration2) - std::min(duration1, duration2) < MARGIN) {
+        return true;
+    }
+    return false;
+}
+
 bool SwappyCommon::updateSwapInterval() {
     std::lock_guard<std::mutex> lock(mFrameDurationsMutex);
     if (!mAutoSwapIntervalEnabled)
@@ -313,43 +409,54 @@
         return false;
 
     const auto averageFrameTime = mFrameDurationsSum / mFrameDurations.size();
-    // define lower and upper bound based on the swap duration
-    nanoseconds upperBound = mRefreshPeriod * mAutoSwapInterval.load();
-    nanoseconds lowerBound = mRefreshPeriod * (mAutoSwapInterval - 1);
 
-    // to be on the conservative side, lower bounds by FRAME_HYSTERESIS
-    upperBound -= FRAME_HYSTERESIS;
-    lowerBound -= FRAME_HYSTERESIS;
+    const auto pipelineFrameTime = averageFrameTime.getTime(PipelineMode::On) + FRAME_MARGIN;
+    const auto nonPipelineFrameTime = averageFrameTime.getTime(PipelineMode::Off) + FRAME_MARGIN;
+    const auto currentConfigFrameTime = mPipelineMode == PipelineMode::On ?
+                                        pipelineFrameTime :
+                                        nonPipelineFrameTime;
+
+    // calculate the new swap interval based on average frame time assume we are in pipeline mode
+    // (prefer higher swap interval rather than turning off pipeline mode)
+    const int newSwapInterval = calculateSwapInterval(pipelineFrameTime, mRefreshPeriod);
+
+    // Define upper and lower bounds based on the swap duration
+    nanoseconds upperBoundForThisRefresh = mRefreshPeriod * mAutoSwapInterval;
+    nanoseconds lowerBoundForThisRefresh = mRefreshPeriod * (mAutoSwapInterval - 1);
 
     // add the hysteresis to one of the bounds to avoid going back and forth when frames
     // are exactly at the edge.
-    lowerBound -= FRAME_HYSTERESIS;
+    lowerBoundForThisRefresh -= EDGE_HYSTERESIS;
 
-    auto div_result = div((averageFrameTime.getTime(true) + FRAME_HYSTERESIS).count(),
-                               mRefreshPeriod.count());
-    auto framesPerRefresh = div_result.quot;
-    auto framesPerRefreshRemainder = div_result.rem;
 
-    const int32_t newSwapInterval = framesPerRefresh + (framesPerRefreshRemainder ? 1 : 0);
-
-    ALOGV("mPipelineMode = %d", mPipelineMode);
+    ALOGV("mPipelineMode = %d", static_cast<int>(mPipelineMode));
     ALOGV("Average cpu frame time = %.2f", (averageFrameTime.getCpuTime().count()) / 1e6f);
     ALOGV("Average gpu frame time = %.2f", (averageFrameTime.getGpuTime().count()) / 1e6f);
-    ALOGV("upperBound = %.2f", upperBound.count() / 1e6f);
-    ALOGV("lowerBound = %.2f", lowerBound.count() / 1e6f);
+    ALOGV("upperBound = %.2f", upperBoundForThisRefresh.count() / 1e6f);
+    ALOGV("lowerBound = %.2f", lowerBoundForThisRefresh.count() / 1e6f);
 
     bool configChanged = false;
-    if (averageFrameTime.getTime(mPipelineMode) > upperBound) {
-        swapSlower(averageFrameTime, upperBound, lowerBound, newSwapInterval);
+
+    // Make sure the frame time fits in the current config to avoid missing frames
+    if (currentConfigFrameTime > upperBoundForThisRefresh) {
+        swapSlower(averageFrameTime, upperBoundForThisRefresh, newSwapInterval);
         configChanged = true;
-    } else if (mSwapInterval < mAutoSwapInterval &&
-               (averageFrameTime.getTime(true) < lowerBound)) {
-        swapFaster(averageFrameTime, upperBound, lowerBound, newSwapInterval);
+    }
+
+    // So we shouldn't miss any frames with this config but maybe we can go faster ?
+    // we check the pipeline frame time here as we prefer lower swap interval than no pipelining
+    else if (mSwapIntervalNS <= mRefreshPeriod * (mAutoSwapInterval - 1) &&
+            pipelineFrameTime < lowerBoundForThisRefresh) {
+        swapFaster(averageFrameTime, lowerBoundForThisRefresh, newSwapInterval);
         configChanged = true;
-    } else if (mPipelineModeAutoMode && mPipelineMode &&
-               averageFrameTime.getTime(false) < upperBound - FRAME_HYSTERESIS) {
+    }
+
+    // If we reached to this condition it means that we fit into the boundaries.
+    // However we might be in pipeline mode and we could turn it off if we still fit.
+    else if (mPipelineMode == PipelineMode::On &&
+             pipelineModeNotNeeded(nonPipelineFrameTime, upperBoundForThisRefresh)) {
         ALOGV("Rendering time fits the current swap interval without pipelining");
-        mPipelineMode = false;
+        mPipelineMode = PipelineMode::Off;
         configChanged = true;
     }
 
@@ -357,6 +464,61 @@
         mFrameDurationsSum = {};
         mFrameDurations.clear();
     }
+
+    // Loop across all supported refresh rate to see if we can find a better refresh rate.
+    // Better refresh rate means:
+    //      Shorter swap period that can still accommodate the frame time can be achieved
+    //      Or,
+    //      Same swap period can be achieved with a lower refresh rate to optimize power
+    //      consumption.
+    nanoseconds minSwapPeriod = mRefreshPeriod * mAutoSwapInterval;
+    bool betterRefreshFound = false;
+    std::pair<std::chrono::nanoseconds, int> betterRefreshConfig;
+    int betterRefreshSwapInterval = 0;
+    if (mSupportedRefreshRates) {
+        for (auto i : *mSupportedRefreshRates) {
+            const auto period = i.first;
+            const int swapIntervalForPeriod = calculateSwapInterval(pipelineFrameTime, period);
+            const nanoseconds duration = period * swapIntervalForPeriod;
+            const nanoseconds lowerBound = duration - EDGE_HYSTERESIS;
+            if (pipelineFrameTime < lowerBound && duration < minSwapPeriod && duration >= mSwapIntervalNS) {
+                minSwapPeriod = duration;
+                betterRefreshConfig = i;
+                betterRefreshSwapInterval = swapIntervalForPeriod;
+                betterRefreshFound = true;
+                ALOGV("Found better refresh %.2f", 1e9f / period.count());
+            }
+        }
+
+        if (!betterRefreshFound) {
+            for (auto i : *mSupportedRefreshRates) {
+                const auto period = i.first;
+                const int swapIntervalForPeriod =
+                        calculateSwapInterval(pipelineFrameTime, period);
+                const nanoseconds duration = period * swapIntervalForPeriod;
+                if (isSameDuration(period, swapIntervalForPeriod,
+                                   mRefreshPeriod, mAutoSwapInterval) && period > mRefreshPeriod) {
+                    betterRefreshFound = true;
+                    betterRefreshConfig = i;
+                    betterRefreshSwapInterval = swapIntervalForPeriod;
+                    ALOGV("Found better refresh %.2f", 1e9f / period.count());
+                }
+            }
+        }
+    }
+
+    // Check if we there is a potential better refresh rate
+    if (betterRefreshFound) {
+        TRACE_INT("preferredRefreshPeriod", betterRefreshConfig.first.count());
+        setPreferredRefreshRate(betterRefreshConfig.second);
+        mSwapIntervalForNewRefresh = betterRefreshSwapInterval;
+
+        nanoseconds upperBoundForNewRefresh = betterRefreshConfig.first * betterRefreshSwapInterval;
+        mPipelineModeForNewRefresh =
+                pipelineModeNotNeeded(nonPipelineFrameTime, upperBoundForNewRefresh) ?
+                                      PipelineMode::Off :  PipelineMode::On;
+    }
+
     return configChanged;
 }
 
@@ -416,8 +578,8 @@
 
     // non pipeline mode is not supported when auto mode is disabled
     if (!enabled) {
-        mPipelineMode = true;
-        TRACE_INT("mPipelineMode", mPipelineMode);
+        mPipelineMode = PipelineMode::On;
+        TRACE_INT("mPipelineMode", static_cast<int>(mPipelineMode));
     }
 }
 
@@ -426,23 +588,74 @@
     mPipelineModeAutoMode = enabled;
     TRACE_INT("mPipelineModeAutoMode", mPipelineModeAutoMode);
     if (!enabled) {
-        mPipelineMode = true;
-        TRACE_INT("mPipelineMode", mPipelineMode);
+        mPipelineMode = PipelineMode::On;
+        TRACE_INT("mPipelineMode", static_cast<int>(mPipelineMode));
     }
 }
 
+void SwappyCommon::setPreferredRefreshRate(int modeId) {
+    if (!mDisplayManager || modeId < 0 || mNextModeId == modeId) {
+        return;
+    }
+
+    mNextModeId = modeId;
+    mDisplayManager->setPreferredRefreshRate(modeId);
+}
+
+int SwappyCommon::calculateSwapInterval(nanoseconds frameTime, nanoseconds refreshPeriod) {
+
+    if (frameTime < refreshPeriod) {
+        return 1;
+    }
+
+    auto div_result = div(frameTime.count(), refreshPeriod.count());
+    auto framesPerRefresh = div_result.quot;
+    auto framesPerRefreshRemainder = div_result.rem;
+
+    return (framesPerRefresh + (framesPerRefreshRemainder > REFRESH_RATE_MARGIN.count() ? 1 : 0));
+}
+
+void SwappyCommon::setPreferredRefreshRate(nanoseconds frameTime) {
+    if (!mDisplayManager) {
+        return;
+    }
+
+    int bestModeId = -1;
+    nanoseconds bestPeriod = 0ns;
+    nanoseconds swapIntervalNSMin = 100ms;
+    for (auto i = mSupportedRefreshRates->crbegin(); i != mSupportedRefreshRates->crend(); ++i) {
+        const auto period = i->first;
+        const int modeId = i->second;
+
+        // Make sure we don't cross the swap interval set by the app
+        if (frameTime < mSwapIntervalNS) {
+            frameTime = mSwapIntervalNS;
+        }
+
+        int swapIntervalForPeriod = calculateSwapInterval(frameTime, period);
+        const auto swapIntervalNS = (period * swapIntervalForPeriod);
+        if (swapIntervalNS < swapIntervalNSMin) {
+            swapIntervalNSMin = swapIntervalNS;
+            bestModeId = modeId;
+            bestPeriod = period;
+        }
+    }
+
+    TRACE_INT("preferredRefreshPeriod", bestPeriod.count());
+    setPreferredRefreshRate(bestModeId);
+}
+
 void SwappyCommon::onSettingsChanged() {
     std::lock_guard<std::mutex> lock(mFrameDurationsMutex);
-    int32_t newSwapInterval = round(float(Settings::getInstance()->getSwapIntervalNS()) /
-                                         float(mRefreshPeriod.count()));
-    if (mSwapInterval != newSwapInterval || mAutoSwapInterval != newSwapInterval) {
-        mSwapInterval = newSwapInterval;
-        mAutoSwapInterval = mSwapInterval.load();
-        mFrameDurations.clear();
-        mFrameDurationsSum = {};
+
+    TimingSettings timingSettings = TimingSettings::from(*Settings::getInstance());
+
+    // If display timings has changed, cache the update and apply them on the next frame
+    if (timingSettings != mNextTimingSettings) {
+        mNextTimingSettings = timingSettings;
+        mTimingSettingsNeedUpdate = true;
     }
-    TRACE_INT("mSwapInterval", mSwapInterval);
-    TRACE_INT("mAutoSwapInterval", mAutoSwapInterval);
+
 }
 
 void SwappyCommon::startFrame() {
@@ -460,13 +673,14 @@
 
     mTargetFrame = currentFrame + mAutoSwapInterval;
 
-    const int intervals = (mPipelineMode) ? 2 : 1;
+    const int intervals = (mPipelineMode == PipelineMode::On) ? 2 : 1;
 
     // We compute the target time as now
     //   + the time the buffer will be on the GPU and in the queue to the compositor (1 swap period)
     mPresentationTime = currentFrameTimestamp + (mAutoSwapInterval * intervals) * mRefreshPeriod;
 
     mStartFrameTime = std::chrono::steady_clock::now();
+    mCPUTracer.startTrace();
 }
 
 void SwappyCommon::waitUntilTargetFrame() {
@@ -482,4 +696,31 @@
     mWaitingCondition.wait(lock, [&]() { return mCurrentFrame >= target; });
 }
 
+int SwappyCommon::getSDKVersion(JNIEnv *env)
+{
+    const jclass buildClass = env->FindClass("android/os/Build$VERSION");
+    if (env->ExceptionCheck()) {
+        env->ExceptionClear();
+        ALOGE("Failed to get Build.VERSION class");
+        return 0;
+    }
+
+    const jfieldID sdk_int = env->GetStaticFieldID(buildClass, "SDK_INT", "I");
+    if (env->ExceptionCheck()) {
+        env->ExceptionClear();
+        ALOGE("Failed to get Build.VERSION.SDK_INT field");
+        return 0;
+    }
+
+    const jint sdk = env->GetStaticIntField(buildClass, sdk_int);
+    if (env->ExceptionCheck()) {
+        env->ExceptionClear();
+        ALOGE("Failed to get SDK version");
+        return 0;
+    }
+
+    ALOGI("SDK version = %d", sdk);
+    return sdk;
+}
+
 } // namespace swappy
diff --git a/src/swappy/common/SwappyCommon.h b/src/swappy/common/SwappyCommon.h
index 0d0f75a..292e59c 100644
--- a/src/swappy/common/SwappyCommon.h
+++ b/src/swappy/common/SwappyCommon.h
@@ -29,6 +29,8 @@
 #include "Thread.h"
 #include "ChoreographerFilter.h"
 #include "ChoreographerThread.h"
+#include "SwappyDisplayManager.h"
+#include "CPUTracer.h"
 
 namespace swappy {
 
@@ -37,6 +39,8 @@
 // Common part between OpenGL and Vulkan implementations.
 class SwappyCommon final {
 public:
+    enum class PipelineMode { Off, On };
+
     // callbacks to be called during pre/post swap
     struct SwapHandlers {
         std::function<bool()> lastFrameIsComplete;
@@ -56,6 +60,8 @@
 
     void onPostSwap(const SwapHandlers& h);
 
+    PipelineMode getCurrentPipelineMode() { return mPipelineMode; }
+
     template <typename ...T>
     using Tracer = std::function<void (T...)>;
     void addTracerCallbacks(SwappyTracer tracer);
@@ -63,10 +69,12 @@
     void setAutoSwapInterval(bool enabled);
     void setAutoPipelineMode(bool enabled);
 
+    void setMaxAutoSwapIntervalNS(std::chrono::nanoseconds swapIntervalNS) {
+        mAutoSwapIntervalThresholdNS = swapIntervalNS;
+    }
+
     std::chrono::steady_clock::time_point getPresentationTime() { return mPresentationTime; }
-    std::chrono::nanoseconds getRefreshPeriod()    { return mRefreshPeriod; }
-    std::chrono::nanoseconds getAppVsyncOffset() { return mAppVsyncOffset; }
-    std::chrono::nanoseconds getSfVsyncOffset()  { return mSfVsyncOffset; }
+    std::chrono::nanoseconds getRefreshPeriod() const { return mRefreshPeriod; }
 
     bool isValid() { return mValid; }
 
@@ -85,8 +93,9 @@
 
         std::chrono::nanoseconds getCpuTime() const { return mCpuTime; }
         std::chrono::nanoseconds getGpuTime() const { return mGpuTime; }
-        std::chrono::nanoseconds getTime(bool pipeline) const {
-            if (pipeline) {
+
+        std::chrono::nanoseconds getTime(PipelineMode pipeline) const {
+            if (pipeline == PipelineMode::On) {
                 return std::max(mCpuTime, mGpuTime);
             }
 
@@ -122,13 +131,11 @@
     std::chrono::nanoseconds wakeClient();
 
     void swapFaster(const FrameDuration& averageFrameTime,
-                    const std::chrono::nanoseconds& upperBound,
                     const std::chrono::nanoseconds& lowerBound,
                     const int32_t& newSwapInterval) REQUIRES(mFrameDurationsMutex);
 
     void swapSlower(const FrameDuration& averageFrameTime,
                     const std::chrono::nanoseconds& upperBound,
-                    const std::chrono::nanoseconds& lowerBound,
                     const int32_t& newSwapInterval) REQUIRES(mFrameDurationsMutex);
     bool updateSwapInterval();
     void preSwapBuffersCallbacks();
@@ -142,10 +149,27 @@
     void startFrame();
     void waitUntilTargetFrame();
     void waitOneFrame();
+    void setPreferredRefreshRate(int index);
+    void setPreferredRefreshRate(std::chrono::nanoseconds frameTime);
+    int calculateSwapInterval(std::chrono::nanoseconds frameTime,
+                              std::chrono::nanoseconds refreshPeriod);
+    bool pipelineModeNotNeeded(const std::chrono::nanoseconds& averageFrameTime,
+                               const std::chrono::nanoseconds& upperBound)
+                               REQUIRES(mFrameDurationsMutex);
+    void updateDisplayTimings();
 
     // Waits for the next frame, considering both Choreographer and the prior frame's completion
     bool waitForNextFrame(const SwapHandlers& h);
 
+    bool isSameDuration(std::chrono::nanoseconds period1,
+                        int interval1,
+                        std::chrono::nanoseconds period2,
+                        int interval2);
+
+    int getSDKVersion(JNIEnv *env);
+
+    const int mSdkVersion;
+
     std::unique_ptr<ChoreographerFilter> mChoreographerFilter;
 
     bool mUsingExternalChoreographer = false;
@@ -160,20 +184,23 @@
     std::chrono::steady_clock::time_point mSwapTime;
 
     std::chrono::nanoseconds mRefreshPeriod;
-    std::chrono::nanoseconds mAppVsyncOffset;
-    std::chrono::nanoseconds mSfVsyncOffset;
 
     std::mutex mFrameDurationsMutex;
     std::vector<FrameDuration> mFrameDurations GUARDED_BY(mFrameDurationsMutex);
     FrameDuration mFrameDurationsSum GUARDED_BY(mFrameDurationsMutex);
-    static constexpr int mFrameDurationSamples = 10;
+    static constexpr int mFrameDurationSamples = 300; // 5 Seconds in 60Hz
     bool mAutoSwapIntervalEnabled GUARDED_BY(mFrameDurationsMutex) = true;
     bool mPipelineModeAutoMode GUARDED_BY(mFrameDurationsMutex) = true;
-    static constexpr std::chrono::nanoseconds FRAME_HYSTERESIS = 3ms;
 
-    std::atomic<int32_t> mSwapInterval;
-    std::atomic<int32_t> mAutoSwapInterval;
-    int mAutoSwapIntervalThreshold = 0;
+    static constexpr std::chrono::nanoseconds FRAME_MARGIN = 3ms;
+    static constexpr std::chrono::nanoseconds EDGE_HYSTERESIS = 4ms;
+
+    std::chrono::nanoseconds mSwapIntervalNS;
+    int32_t mAutoSwapInterval;
+    std::atomic<std::chrono::nanoseconds> mAutoSwapIntervalThresholdNS = {50ms}; // 20FPS
+    int mSwapIntervalForNewRefresh = 0;
+    PipelineMode mPipelineModeForNewRefresh;
+    static constexpr std::chrono::nanoseconds REFRESH_RATE_MARGIN = 500ns;
 
     std::chrono::steady_clock::time_point mStartFrameTime;
 
@@ -191,11 +218,44 @@
     int32_t mTargetFrame = 0;
     std::chrono::steady_clock::time_point mPresentationTime = std::chrono::steady_clock::now();
     bool mPresentationTimeNeeded;
-    bool mPipelineMode = false;
+    PipelineMode mPipelineMode = PipelineMode::Off;
 
     bool mValid;
 
     std::chrono::nanoseconds mFenceTimeout = std::chrono::nanoseconds(50ms);
+
+    constexpr static bool USE_DISPLAY_MANAGER = true;
+    std::unique_ptr<SwappyDisplayManager> mDisplayManager;
+    int mNextModeId = -1;
+
+    std::shared_ptr<SwappyDisplayManager::RefreshRateMap> mSupportedRefreshRates;
+
+    struct TimingSettings {
+        std::chrono::nanoseconds refreshPeriod = {};
+        std::chrono::nanoseconds swapIntervalNS = {};
+
+        static TimingSettings from(const Settings& settings) {
+            TimingSettings timingSettings;
+
+            timingSettings.refreshPeriod = settings.getDisplayTimings().refreshPeriod;
+            timingSettings.swapIntervalNS =
+                    std::chrono::nanoseconds(settings.getSwapIntervalNS());
+            return timingSettings;
+        }
+
+        bool operator != (const TimingSettings& other) const {
+            return (refreshPeriod  != other.refreshPeriod)  ||
+                   (swapIntervalNS != other.swapIntervalNS);
+        }
+
+        bool operator == (const TimingSettings& other) const {
+            return !(*this != other);
+        }
+    };
+    TimingSettings mNextTimingSettings GUARDED_BY(mFrameDurationsMutex) = {};
+    bool mTimingSettingsNeedUpdate GUARDED_BY(mFrameDurationsMutex) = false;
+
+    CPUTracer mCPUTracer;
 };
 
 } //namespace swappy
diff --git a/src/swappy/common/SwappyDisplayManager.cpp b/src/swappy/common/SwappyDisplayManager.cpp
new file mode 100644
index 0000000..19f374e
--- /dev/null
+++ b/src/swappy/common/SwappyDisplayManager.cpp
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <jni.h>
+#include <Log.h>
+#include <map>
+#include "SwappyDisplayManager.h"
+#include "Settings.h"
+
+#define LOG_TAG "SwappyDisplayManager"
+
+namespace swappy {
+
+SwappyDisplayManager::SwappyDisplayManager(JavaVM* vm, jobject mainActivity) : mJVM(vm) {
+    JNIEnv *env;
+    mJVM->AttachCurrentThread(&env, nullptr);
+
+    // Since we may call this from ANativeActivity we cannot use env->FindClass as the classpath
+    // will be empty. Instead we need to work a bit harder
+    jclass activity = env->GetObjectClass(mainActivity);
+    jclass classLoader = env->FindClass("java/lang/ClassLoader");
+    jmethodID getClassLoader = env->GetMethodID(activity,
+                                                "getClassLoader",
+                                                "()Ljava/lang/ClassLoader;");
+    jmethodID loadClass = env->GetMethodID(classLoader,
+                                           "loadClass",
+                                           "(Ljava/lang/String;)Ljava/lang/Class;");
+    jobject classLoaderObj = env->CallObjectMethod(mainActivity, getClassLoader);
+    jstring swappyDisplayManagerName =
+            env->NewStringUTF("com/google/androidgamesdk/SwappyDisplayManager");
+
+    jclass swappyDisplayManagerClass = static_cast<jclass>(
+            env->CallObjectMethod(classLoaderObj, loadClass, swappyDisplayManagerName));
+    env->DeleteLocalRef(swappyDisplayManagerName);
+    if (env->ExceptionCheck()) {
+        env->ExceptionDescribe();
+        ALOGE("Unable to find com.google.androidgamesdk.SwappyDisplayManager class");
+        ALOGE("Did you integrate extras ?");
+        return;
+    }
+
+    jmethodID constructor = env->GetMethodID(
+            swappyDisplayManagerClass,
+            "<init>",
+            "(JLandroid/app/Activity;)V");
+    mSetPreferredRefreshRate = env->GetMethodID(
+            swappyDisplayManagerClass,
+            "setPreferredRefreshRate",
+            "(I)V");
+    jobject swappyDisplayManager = env->NewObject(swappyDisplayManagerClass,
+                                                  constructor,
+                                                  (jlong)this,
+                                                  mainActivity);
+    mJthis = env->NewGlobalRef(swappyDisplayManager);
+
+    mInitialized = true;
+}
+
+SwappyDisplayManager::~SwappyDisplayManager() {
+    JNIEnv *env;
+    mJVM->AttachCurrentThread(&env, nullptr);
+
+    env->DeleteGlobalRef(mJthis);
+}
+
+std::shared_ptr<SwappyDisplayManager::RefreshRateMap>
+SwappyDisplayManager::getSupportedRefreshRates() {
+    std::unique_lock<std::mutex> lock(mMutex);
+
+    mCondition.wait(lock, [&]() { return mSupportedRefreshRates.get() != nullptr; });
+    return mSupportedRefreshRates;
+}
+
+void SwappyDisplayManager::setPreferredRefreshRate(int index) {
+    JNIEnv *env;
+    mJVM->AttachCurrentThread(&env, nullptr);
+
+    env->CallVoidMethod(mJthis, mSetPreferredRefreshRate, index);
+}
+
+// Helper class to wrap JNI entry points to SwappyDisplayManager
+class SwappyDisplayManagerJNI {
+public:
+    static void onSetSupportedRefreshRates(jlong,
+                                           std::shared_ptr<SwappyDisplayManager::RefreshRateMap>);
+    static void onRefreshRateChanged(jlong, long, long, long);
+};
+
+void SwappyDisplayManagerJNI::onSetSupportedRefreshRates(jlong cookie,
+        std::shared_ptr<SwappyDisplayManager::RefreshRateMap> refreshRates) {
+    auto *sDM = reinterpret_cast<SwappyDisplayManager*>(cookie);
+
+    std::lock_guard<std::mutex> lock(sDM->mMutex);
+    sDM->mSupportedRefreshRates = std::move(refreshRates);
+    sDM->mCondition.notify_one();
+}
+
+void SwappyDisplayManagerJNI::onRefreshRateChanged(jlong /*cookie*/,
+                                                   long refreshPeriod,
+                                                   long appOffset,
+                                                   long sfOffset) {
+    using std::chrono::nanoseconds;
+    Settings::DisplayTimings displayTimings;
+    displayTimings.refreshPeriod = nanoseconds(refreshPeriod);
+    displayTimings.appOffset = nanoseconds(appOffset);
+    displayTimings.sfOffset = nanoseconds(sfOffset);
+    Settings::getInstance()->setDisplayTimings(displayTimings);
+}
+
+extern "C" {
+
+JNIEXPORT void JNICALL
+Java_com_google_androidgamesdk_SwappyDisplayManager_nSetSupportedRefreshRates(
+                                                                            JNIEnv *env,
+                                                                            jobject /* this */,
+                                                                            jlong cookie,
+                                                                            jlongArray refreshRates,
+                                                                            jintArray modeIds) {
+    int length = env->GetArrayLength(refreshRates);
+    auto refreshRatesMap =
+            std::make_shared<SwappyDisplayManager::RefreshRateMap>();
+
+    jlong *refreshRatesArr = env->GetLongArrayElements(refreshRates, 0);
+    jint *modeIdsArr = env->GetIntArrayElements(modeIds, 0);
+    for (int i = 0; i < length; i++) {
+        (*refreshRatesMap)[std::chrono::nanoseconds(refreshRatesArr[i])] = modeIdsArr[i];
+    }
+    env->ReleaseLongArrayElements(refreshRates, refreshRatesArr, 0);
+    env->ReleaseIntArrayElements(modeIds, modeIdsArr, 0);
+
+    SwappyDisplayManagerJNI::onSetSupportedRefreshRates(cookie, refreshRatesMap);
+}
+
+JNIEXPORT void JNICALL
+Java_com_google_androidgamesdk_SwappyDisplayManager_nOnRefreshRateChanged(JNIEnv *env,
+                                                                          jobject /* this */,
+                                                                          jlong cookie,
+                                                                          jlong refreshPeriod,
+                                                                          jlong appOffset,
+                                                                          jlong sfOffset) {
+    SwappyDisplayManagerJNI::onRefreshRateChanged(cookie, refreshPeriod, appOffset, sfOffset);
+}
+
+} // extern "C"
+
+} // namespace swappy
diff --git a/src/swappy/common/SwappyDisplayManager.h b/src/swappy/common/SwappyDisplayManager.h
new file mode 100644
index 0000000..bec9736
--- /dev/null
+++ b/src/swappy/common/SwappyDisplayManager.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <condition_variable>
+#include <functional>
+#include <map>
+#include <memory>
+#include <mutex>
+
+
+namespace swappy {
+
+class SwappyDisplayManager {
+public:
+    static constexpr int MIN_SDK_VERSION = 23;
+
+    SwappyDisplayManager(JavaVM*, jobject);
+    ~SwappyDisplayManager();
+
+    bool isInitialized() { return mInitialized; }
+
+    using RefreshRateMap = std::map<std::chrono::nanoseconds, int>;
+
+    std::shared_ptr<RefreshRateMap> getSupportedRefreshRates();
+
+    void setPreferredRefreshRate(int index);
+
+private:
+    JavaVM* mJVM;
+    std::mutex mMutex;
+    std::condition_variable mCondition;
+    std::shared_ptr<RefreshRateMap> mSupportedRefreshRates;
+    jobject mJthis;
+    jmethodID mSetPreferredRefreshRate;
+    bool mInitialized = false;
+
+    friend class SwappyDisplayManagerJNI;
+};
+
+} // namespace swappy
\ No newline at end of file
diff --git a/src/swappy/common/Thread.h b/src/swappy/common/Thread.h
index 18557b8..27a7e4e 100644
--- a/src/swappy/common/Thread.h
+++ b/src/swappy/common/Thread.h
@@ -36,9 +36,13 @@
 
     #define REQUIRES(...) \
         THREAD_ANNOTATION_ATTRIBUTE__(requires_capability(__VA_ARGS__))
+
+    #define NO_THREAD_SAFETY_ANALYSIS \
+        THREAD_ANNOTATION_ATTRIBUTE__(no_thread_safety_analysis)
 #else
     #define GUARDED_BY(x)
     #define REQUIRES(...)
+    #define NO_THREAD_SAFETY_ANALYSIS
 #endif
 
 namespace swappy {
diff --git a/src/swappy/opengl/EGL.cpp b/src/swappy/opengl/EGL.cpp
index 7ecc9fc..198a6c0 100644
--- a/src/swappy/opengl/EGL.cpp
+++ b/src/swappy/opengl/EGL.cpp
@@ -17,6 +17,7 @@
 #include "EGL.h"
 
 #include <vector>
+#include <Trace.h>
 
 #define LOG_TAG "Swappy::EGL"
 
@@ -26,8 +27,7 @@
 
 namespace swappy {
 
-std::unique_ptr<EGL> EGL::create(std::chrono::nanoseconds refreshPeriod,
-                                 std::chrono::nanoseconds fenceTimeout) {
+std::unique_ptr<EGL> EGL::create(std::chrono::nanoseconds fenceTimeout) {
     auto eglPresentationTimeANDROID = reinterpret_cast<eglPresentationTimeANDROID_type>(
         eglGetProcAddress("eglPresentationTimeANDROID"));
     if (eglPresentationTimeANDROID == nullptr) {
@@ -83,7 +83,7 @@
         ALOGI("Failed to load eglGetFrameTimestampsANDROID");
     }
 
-    auto egl = std::make_unique<EGL>(refreshPeriod, fenceTimeout, ConstructorTag{});
+    auto egl = std::make_unique<EGL>(fenceTimeout, ConstructorTag{});
     egl->eglPresentationTimeANDROID = eglPresentationTimeANDROID;
     egl->eglCreateSyncKHR = eglCreateSyncKHR;
     egl->eglDestroySyncKHR = eglDestroySyncKHR;
@@ -148,7 +148,7 @@
     return (eglGetNextFrameIdANDROID != nullptr && eglGetFrameTimestampsANDROID != nullptr);
 }
 
-std::pair<bool,EGLuint64KHR> EGL::getNextFrameId(EGLDisplay dpy, EGLSurface surface) {
+std::pair<bool,EGLuint64KHR> EGL::getNextFrameId(EGLDisplay dpy, EGLSurface surface) const {
     if (eglGetNextFrameIdANDROID == nullptr) {
         ALOGE("stats are not supported on this platform");
         return {false, 0};
@@ -166,7 +166,7 @@
 
 std::unique_ptr<EGL::FrameTimestamps> EGL::getFrameTimestamps(EGLDisplay dpy,
                                                               EGLSurface surface,
-                                                              EGLuint64KHR frameId) {
+                                                              EGLuint64KHR frameId) const {
     if (eglGetFrameTimestampsANDROID == nullptr) {
         ALOGE("stats are not supported on this platform");
         return nullptr;
@@ -259,6 +259,7 @@
             break;
         }
 
+        gamesdk::ScopedTrace tracer("Swappy: GPU frame time");
         const auto startTime = std::chrono::steady_clock::now();
         EGLBoolean result = eglClientWaitSyncKHR(mDisplay, mSyncFence, 0,
                                                  mFenceTimeout.count());
@@ -282,7 +283,7 @@
     }
 }
 
-std::chrono::nanoseconds EGL::FenceWaiter::getFencePendingTime() {
+std::chrono::nanoseconds EGL::FenceWaiter::getFencePendingTime() const {
     // return mFencePendingTime without a lock to avoid blocking the main thread
     // worst case, the time will be of some previous frame
     return mFencePendingTime.load();
diff --git a/src/swappy/opengl/EGL.h b/src/swappy/opengl/EGL.h
index de05a4f..4450bd3 100644
--- a/src/swappy/opengl/EGL.h
+++ b/src/swappy/opengl/EGL.h
@@ -45,31 +45,28 @@
         EGLnsecsANDROID presented;
     };
 
-    explicit EGL(std::chrono::nanoseconds refreshPeriod,
-                 std::chrono::nanoseconds fenceTimeout, ConstructorTag)
-        : mRefreshPeriod(refreshPeriod), mFenceWaiter(fenceTimeout) {}
+    explicit EGL(std::chrono::nanoseconds fenceTimeout, ConstructorTag)
+        : mFenceWaiter(fenceTimeout) {}
 
-  static std::unique_ptr<EGL> create(std::chrono::nanoseconds refreshPeriod,
-                                     std::chrono::nanoseconds fenceTimeout);
+    static std::unique_ptr<EGL> create(std::chrono::nanoseconds fenceTimeout);
 
     void resetSyncFence(EGLDisplay display);
     bool lastFrameIsComplete(EGLDisplay display);
     bool setPresentationTime(EGLDisplay display,
                              EGLSurface surface,
                              std::chrono::steady_clock::time_point time);
-    std::chrono::nanoseconds getFencePendingTime() { return mFenceWaiter.getFencePendingTime(); }
+    std::chrono::nanoseconds getFencePendingTime() const
+        { return mFenceWaiter.getFencePendingTime(); }
 
     // for stats
     bool statsSupported();
     std::pair<bool,EGLuint64KHR> getNextFrameId(EGLDisplay dpy,
-                                                EGLSurface surface);
+                                                EGLSurface surface) const;
     std::unique_ptr<FrameTimestamps> getFrameTimestamps(EGLDisplay dpy,
                                                         EGLSurface surface,
-                                                        EGLuint64KHR frameId);
+                                                        EGLuint64KHR frameId) const;
 
   private:
-    const std::chrono::nanoseconds mRefreshPeriod;
-
     using eglPresentationTimeANDROID_type = EGLBoolean (*)(EGLDisplay, EGLSurface, EGLnsecsANDROID);
     eglPresentationTimeANDROID_type eglPresentationTimeANDROID = nullptr;
     using eglCreateSyncKHR_type = EGLSyncKHR (*)(EGLDisplay, EGLenum, const EGLint *);
@@ -99,7 +96,7 @@
 
         void onFenceCreation(EGLDisplay display, EGLSyncKHR syncFence);
         void waitForIdle();
-        std::chrono::nanoseconds getFencePendingTime();
+        std::chrono::nanoseconds getFencePendingTime() const;
 
     private:
         using eglClientWaitSyncKHR_type = EGLBoolean (*)(EGLDisplay, EGLSyncKHR, EGLint, EGLTimeKHR);
diff --git a/src/swappy/opengl/SwappyGL.cpp b/src/swappy/opengl/SwappyGL.cpp
index fe6aac9..7c76f49 100644
--- a/src/swappy/opengl/SwappyGL.cpp
+++ b/src/swappy/opengl/SwappyGL.cpp
@@ -37,13 +37,20 @@
 std::mutex SwappyGL::sInstanceMutex;
 std::unique_ptr<SwappyGL> SwappyGL::sInstance;
 
-void SwappyGL::init(JNIEnv *env, jobject jactivity) {
+bool SwappyGL::init(JNIEnv *env, jobject jactivity) {
     std::lock_guard<std::mutex> lock(sInstanceMutex);
     if (sInstance) {
         ALOGE("Attempted to initialize SwappyGL twice");
-        return;
+        return false;
     }
     sInstance = std::make_unique<SwappyGL>(env, jactivity, ConstructorTag{});
+    if (!sInstance->mEnableSwappy) {
+        ALOGE("Failed to initialize SwappyGL");
+        sInstance = nullptr;
+        return false;
+    }
+
+    return true;
 }
 
 void SwappyGL::onChoreographer(int64_t frameTimeNanos) {
@@ -145,6 +152,15 @@
     swappy->mCommonBase.setAutoPipelineMode(enabled);
 }
 
+void SwappyGL::setMaxAutoSwapIntervalNS(std::chrono::nanoseconds maxSwapNS) {
+    SwappyGL *swappy = getInstance();
+    if (!swappy) {
+        ALOGE("Failed to get SwappyGL instance in setMaxAutoSwapIntervalNS");
+        return;
+    }
+    swappy->mCommonBase.setMaxAutoSwapIntervalNS(maxSwapNS);
+}
+
 void SwappyGL::enableStats(bool enabled) {
     SwappyGL *swappy = getInstance();
     if (!swappy) {
@@ -163,7 +179,7 @@
 
     if (enabled && swappy->mFrameStatistics == nullptr) {
         swappy->mFrameStatistics = std::make_unique<FrameStatistics>(
-                swappy->mEgl, swappy->mCommonBase.getRefreshPeriod());
+                *swappy->mEgl, swappy->mCommonBase);
         ALOGI("Enabling stats");
     } else {
         swappy->mFrameStatistics = nullptr;
@@ -259,18 +275,14 @@
     }
 
     std::lock_guard<std::mutex> lock(mEglMutex);
-    mEgl = EGL::create(mCommonBase.getRefreshPeriod(), mCommonBase.getFenceTimeout());
+    mEgl = EGL::create(mCommonBase.getFenceTimeout());
     if (!mEgl) {
         ALOGE("Failed to load EGL functions");
         mEnableSwappy = false;
         return;
     }
 
-    ALOGI("Initialized Swappy with vsyncPeriod=%lld, appOffset=%lld, sfOffset=%lld",
-        (long long)mCommonBase.getRefreshPeriod().count(),
-        (long long)mCommonBase.getAppVsyncOffset().count(),
-        (long long)mCommonBase.getSfVsyncOffset().count()
-    );
+    ALOGI("SwappyGL initialized successfully");
 }
 
 void SwappyGL::resetSyncFence(EGLDisplay display) {
@@ -280,9 +292,11 @@
 bool SwappyGL::setPresentationTime(EGLDisplay display, EGLSurface surface) {
     TRACE_CALL();
 
+    auto displayTimings = Settings::getInstance()->getDisplayTimings();
+
     // if we are too close to the vsync, there is no need to set presentation time
     if ((mCommonBase.getPresentationTime() - std::chrono::steady_clock::now()) <
-            (mCommonBase.getRefreshPeriod() - mCommonBase.getSfVsyncOffset())) {
+            (mCommonBase.getRefreshPeriod() - displayTimings.sfOffset)) {
         return EGL_TRUE;
     }
 
diff --git a/src/swappy/opengl/SwappyGL.h b/src/swappy/opengl/SwappyGL.h
index 253e6cd..c185a0a 100644
--- a/src/swappy/opengl/SwappyGL.h
+++ b/src/swappy/opengl/SwappyGL.h
@@ -41,7 +41,7 @@
     struct ConstructorTag {};
   public:
     SwappyGL(JNIEnv *env, jobject jactivity, ConstructorTag);
-    static void init(JNIEnv *env, jobject jactivity);
+    static bool init(JNIEnv *env, jobject jactivity);
 
     static void onChoreographer(int64_t frameTimeNanos);
 
@@ -56,6 +56,8 @@
 
     static void setAutoPipelineMode(bool enabled);
 
+    static void setMaxAutoSwapIntervalNS(std::chrono::nanoseconds maxSwapNS);
+
     static void enableStats(bool enabled);
     static void recordFrameStart(EGLDisplay display, EGLSurface surface);
     static void getStats(SwappyStats *stats);
@@ -86,10 +88,10 @@
     bool mEnableSwappy = true;
 
     static std::mutex sInstanceMutex;
-    static std::unique_ptr<SwappyGL> sInstance;
+    static std::unique_ptr<SwappyGL> sInstance GUARDED_BY(sInstanceMutex);
 
     std::mutex mEglMutex;
-    std::shared_ptr<EGL> mEgl;
+    std::unique_ptr<EGL> mEgl;
 
     std::unique_ptr<FrameStatistics> mFrameStatistics;
 
diff --git a/src/swappy/opengl/swappyGL_c.cpp b/src/swappy/opengl/swappyGL_c.cpp
index db16911..8064c95 100644
--- a/src/swappy/opengl/swappyGL_c.cpp
+++ b/src/swappy/opengl/swappyGL_c.cpp
@@ -28,8 +28,8 @@
 
 extern "C" {
 
-void SwappyGL_init_internal(JNIEnv *env, jobject jactivity) {
-    SwappyGL::init(env, jactivity);
+bool SwappyGL_init_internal(JNIEnv *env, jobject jactivity) {
+    return SwappyGL::init(env, jactivity);
 }
 
 void SwappyGL_destroy() {
@@ -44,10 +44,6 @@
     return SwappyGL::swap(display, surface);
 }
 
-void SwappyGL_setRefreshPeriod(uint64_t period_ns) {
-    Settings::getInstance()->setRefreshPeriod(std::chrono::nanoseconds(period_ns));
-}
-
 void SwappyGL_setUseAffinity(bool tf) {
     Settings::getInstance()->setUseAffinity(tf);
 }
@@ -57,7 +53,7 @@
 }
 
 uint64_t SwappyGL_getRefreshPeriodNanos() {
-    return Settings::getInstance()->getRefreshPeriod().count();
+    return Settings::getInstance()->getDisplayTimings().refreshPeriod.count();
 }
 
 bool SwappyGL_getUseAffinity() {
@@ -76,6 +72,10 @@
     SwappyGL::setAutoSwapInterval(enabled);
 }
 
+void SwappyGL_setMaxAutoSwapIntervalNS(uint64_t max_swap_ns) {
+    SwappyGL::setMaxAutoSwapIntervalNS(std::chrono::nanoseconds(max_swap_ns));
+}
+
 void SwappyGL_setAutoPipelineMode(bool enabled) {
     SwappyGL::setAutoPipelineMode(enabled);
 }
diff --git a/src/swappy/vulkan/SwappyVk.cpp b/src/swappy/vulkan/SwappyVk.cpp
index e733237..60760d8 100644
--- a/src/swappy/vulkan/SwappyVk.cpp
+++ b/src/swappy/vulkan/SwappyVk.cpp
@@ -184,6 +184,12 @@
     }
 }
 
+void SwappyVk::SetMaxAutoSwapIntervalNS(std::chrono::nanoseconds maxSwapNS) {
+    for (auto i : perSwapchainImplementation) {
+        i.second->setMaxAutoSwapIntervalNS(maxSwapNS);
+    }
+}
+
 void SwappyVk::SetFenceTimeout(std::chrono::nanoseconds t) {
     for(auto i : perDeviceImplementation) {
         i.second->setFenceTimeout(t);
diff --git a/src/swappy/vulkan/SwappyVk.h b/src/swappy/vulkan/SwappyVk.h
index 9f6ca19..facd735 100644
--- a/src/swappy/vulkan/SwappyVk.h
+++ b/src/swappy/vulkan/SwappyVk.h
@@ -77,6 +77,7 @@
 
     void SetAutoSwapInterval(bool enabled);
     void SetAutoPipelineMode(bool enabled);
+    void SetMaxAutoSwapIntervalNS(std::chrono::nanoseconds maxSwapNS);
     void SetFenceTimeout(std::chrono::nanoseconds duration);
     std::chrono::nanoseconds GetFenceTimeout() const;
 
diff --git a/src/swappy/vulkan/SwappyVkBase.cpp b/src/swappy/vulkan/SwappyVkBase.cpp
index b19bb3a..0b4025c 100644
--- a/src/swappy/vulkan/SwappyVkBase.cpp
+++ b/src/swappy/vulkan/SwappyVkBase.cpp
@@ -154,7 +154,7 @@
             return res;
         }
 
-        mFreeSync[queue].push_back(sync);
+        mFreeSyncPool[queue].push_back(sync);
     }
 
     // Create a thread that will wait for the fences
@@ -165,6 +165,7 @@
 }
 
 void SwappyVkBase::destroyVkSyncObjects() {
+    // Stop all waiters threads
     for (auto it = mThreads.begin(); it != mThreads.end(); it++) {
         {
             std::lock_guard<std::mutex> lock(it->second->lock);
@@ -174,60 +175,83 @@
         it->second->thread.join();
     }
 
-    for (auto it = mPendingSync.begin(); it != mPendingSync.end(); it++) {
-        while (mPendingSync[it->first].size() > 0) {
-            VkSync sync = mPendingSync[it->first].front();
-            mPendingSync[it->first].pop_front();
-            if (!sync.fenceSignaled) {
-                vkWaitForFences(mDevice, 1, &sync.fence, VK_TRUE,
-                                mCommonBase.getFenceTimeout().count());
-                vkResetFences(mDevice, 1, &sync.fence);
-            }
-            mFreeSync[it->first].push_back(sync);
+    // Wait for all unsignaled fences to get signlaed
+    for (auto it = mWaitingSyncs.begin(); it != mWaitingSyncs.end(); it++) {
+        auto queue = it->first;
+        auto syncList = it->second;
+        while (syncList.size() > 0) {
+            VkSync sync = syncList.front();
+            syncList.pop_front();
+            vkWaitForFences(mDevice, 1, &sync.fence, VK_TRUE,
+                            mCommonBase.getFenceTimeout().count());
+            vkResetFences(mDevice, 1, &sync.fence);
+            mSignaledSyncs[queue].push_back(sync);
         }
+    }
 
-        while (mFreeSync[it->first].size() > 0) {
-            VkSync sync = mFreeSync[it->first].front();
-            mFreeSync[it->first].pop_front();
+    // Move all signaled fences to the free pool
+    for (auto it = mSignaledSyncs.begin(); it != mSignaledSyncs.end(); it++) {
+        auto queue = it->first;
+        reclaimSignaledFences(queue);
+    }
+
+    // Free all sync objects
+    for (auto it = mFreeSyncPool.begin(); it != mFreeSyncPool.end(); it++) {
+        auto queue = it->first;
+        auto syncList = it->second;
+        while (syncList.size() > 0) {
+            VkSync sync = syncList.front();
+            syncList.pop_front();
             vkFreeCommandBuffers(mDevice, mCommandPool[it->first], 1, &sync.command);
             vkDestroyEvent(mDevice, sync.event, NULL);
             vkDestroySemaphore(mDevice, sync.semaphore, NULL);
             vkDestroyFence(mDevice, sync.fence, NULL);
         }
+    }
 
-        vkDestroyCommandPool(mDevice, mCommandPool[it->first], NULL);
+    // Free destroy the command pools
+    for (auto it = mCommandPool.begin(); it != mCommandPool.end(); it++) {
+        auto commandPool = it->second;
+        vkDestroyCommandPool(mDevice, commandPool, NULL);
+    }
+}
+
+void SwappyVkBase::reclaimSignaledFences(VkQueue queue) {
+    std::lock_guard<std::mutex> lock(mThreads[queue]->lock);
+    while (!mSignaledSyncs[queue].empty()) {
+        VkSync sync = mSignaledSyncs[queue].front();
+        mSignaledSyncs[queue].pop_front();
+        mFreeSyncPool[queue].push_back(sync);
     }
 }
 
 bool SwappyVkBase::lastFrameIsCompleted(VkQueue queue) {
+    auto pipelineMode = mCommonBase.getCurrentPipelineMode();
     std::lock_guard<std::mutex> lock(mThreads[queue]->lock);
-    if (mPendingSync[queue].size() < MAX_PENDING_FENCES) {
-        return true;
+    if (pipelineMode == SwappyCommon::PipelineMode::On) {
+        // We are in pipeline mode so we need to check the fence of frame N-1
+        return mWaitingSyncs[queue].size() < 2;
     }
 
-    VkSync& sync = mPendingSync[queue].front();
+    // We are not in pipeline mode so we need to check the fence the current frame. i.e. there
+    // are not unsignaled frames
+    return mWaitingSyncs[queue].empty();
 
-    // Waiter thread updates the pending time when the fence has signaled.
-    if (!sync.fenceSignaled) {
-        return false;
-    }
-
-    mPendingSync[queue].pop_front();
-    mFreeSync[queue].push_back(sync);
-    return true;
 }
 
 VkResult SwappyVkBase::injectFence(VkQueue                 queue,
                                    const VkPresentInfoKHR* pPresentInfo,
                                    VkSemaphore*            pSemaphore) {
+    reclaimSignaledFences(queue);
+
     // If we cross the swap interval threshold, we don't pace at all.
     // In this case we might not have a free fence, so just don't use the fence.
-    if (mFreeSync[queue].size() == 0) {
+    if (mFreeSyncPool[queue].size() == 0) {
         return VK_SUCCESS;
     }
 
-    VkSync sync = mFreeSync[queue].front();
-    mFreeSync[queue].pop_front();
+    VkSync sync = mFreeSyncPool[queue].front();
+    mFreeSyncPool[queue].pop_front();
 
     VkPipelineStageFlags pipe_stage_flags;
     VkSubmitInfo submit_info;
@@ -245,8 +269,7 @@
     *pSemaphore = sync.semaphore;
 
     std::lock_guard<std::mutex> lock(mThreads[queue]->lock);
-    sync.fenceSignaled = false;
-    mPendingSync[queue].push_back(sync);
+    mWaitingSyncs[queue].push_back(sync);
     mThreads[queue]->hasPendingWork = true;
     mThreads[queue]->condition.notify_all();
 
@@ -257,6 +280,10 @@
     mCommonBase.setAutoSwapInterval(enabled);
 }
 
+void SwappyVkBase::setMaxAutoSwapIntervalNS(std::chrono::nanoseconds swapMaxNS) {
+    mCommonBase.setMaxAutoSwapIntervalNS(swapMaxNS);
+}
+
 void SwappyVkBase::setAutoPipelineMode(bool enabled) {
     mCommonBase.setAutoPipelineMode(enabled);
 }
@@ -265,8 +292,7 @@
     ThreadContext& thread = *mThreads[queue];
 
     while (true) {
-        std::list<VkSync>::iterator pendingSyncIterator;
-        bool remainingSyncs = true;
+        bool waitingSyncsEmpty;
         {
             std::lock_guard<std::mutex> lock(thread.lock);
             // Wait for new fence object
@@ -280,48 +306,40 @@
                 break;
             }
 
-            pendingSyncIterator = mPendingSync[queue].begin();
-            while (pendingSyncIterator != mPendingSync[queue].end() &&
-                    pendingSyncIterator->fenceSignaled) {
-                ++pendingSyncIterator;
-            }
-            remainingSyncs = pendingSyncIterator != mPendingSync[queue].end();
+            waitingSyncsEmpty = mWaitingSyncs[queue].empty();
         }
 
-        while (remainingSyncs) {
-            VkSync *sync;
+        while (!waitingSyncsEmpty) {
+            VkSync sync;
             {  // Get the sync object with a lock
                 std::lock_guard<std::mutex> lock(thread.lock);
-                sync = &(*pendingSyncIterator);
+                sync = mWaitingSyncs[queue].front();
+                mWaitingSyncs[queue].pop_front();
             }
 
+            gamesdk::ScopedTrace tracer("Swappy: GPU frame time");
             const auto startTime = std::chrono::steady_clock::now();
-            VkResult result = vkWaitForFences(mDevice, 1, &sync->fence, VK_TRUE, UINT64_MAX);
+            VkResult result = vkWaitForFences(mDevice, 1, &sync.fence, VK_TRUE,
+                                              mCommonBase.getFenceTimeout().count());
             if (result) {
                 ALOGE("Failed to wait for fence %d", result);
             }
-            auto pendingTime = std::chrono::steady_clock::now() - startTime;
+            vkResetFences(mDevice, 1, &sync.fence);
+            mLastFenceTime = std::chrono::steady_clock::now() - startTime;
 
-            vkResetFences(mDevice, 1, &sync->fence);
-
-            {  // Advance the iterator
+            // Move the sync object to the signaled list
+            {
                 std::lock_guard<std::mutex> lock(thread.lock);
-                sync->pendingTime = pendingTime;
-                sync->fenceSignaled = true;
-                ++pendingSyncIterator;
-                remainingSyncs = pendingSyncIterator != mPendingSync[queue].end();
+
+                mSignaledSyncs[queue].push_back(sync);
+                waitingSyncsEmpty = mWaitingSyncs[queue].empty();
             }
         }
     }
 }
 
 std::chrono::nanoseconds SwappyVkBase::getLastFenceTime(VkQueue queue) {
-    std::lock_guard<std::mutex> lock(mThreads[queue]->lock);
-    // Last fence is either the first one pending or the last one that was free.
-    if (mPendingSync[queue].size() && mPendingSync[queue].front().pendingTime != 0ns) {
-        return mPendingSync[queue].begin()->pendingTime;
-    }
-    return mFreeSync[queue].back().pendingTime;
+    return mLastFenceTime;
 }
 
 void SwappyVkBase::setFenceTimeout(std::chrono::nanoseconds duration) {
diff --git a/src/swappy/vulkan/SwappyVkBase.h b/src/swappy/vulkan/SwappyVkBase.h
index cf1c109..cb6f43a 100644
--- a/src/swappy/vulkan/SwappyVkBase.h
+++ b/src/swappy/vulkan/SwappyVkBase.h
@@ -119,6 +119,8 @@
     void setAutoSwapInterval(bool enabled);
     void setAutoPipelineMode(bool enabled);
 
+    void setMaxAutoSwapIntervalNS(std::chrono::nanoseconds swapMaxNS);
+
     void setFenceTimeout(std::chrono::nanoseconds duration);
     std::chrono::nanoseconds getFenceTimeout() const;
 
@@ -130,8 +132,6 @@
         VkSemaphore semaphore;
         VkCommandBuffer command;
         VkEvent event;
-        bool fenceSignaled = false;
-        std::chrono::nanoseconds pendingTime = {};
     };
 
     struct ThreadContext {
@@ -159,16 +159,26 @@
     PFN_vkGetRefreshCycleDurationGOOGLE   mpfnGetRefreshCycleDurationGOOGLE   = nullptr;
     PFN_vkGetPastPresentationTimingGOOGLE mpfnGetPastPresentationTimingGOOGLE = nullptr;
 
-    std::map<VkQueue, std::list<VkSync>>              mFreeSync;
-    std::map<VkQueue, std::list<VkSync>>              mPendingSync;
+    // Holds VKSync objects ready to be used
+    std::map<VkQueue, std::list<VkSync>>              mFreeSyncPool;
+
+    // Holds VKSync objects queued and but signaled yet
+    std::map<VkQueue, std::list<VkSync>>              mWaitingSyncs;
+
+    // Holds VKSync objects that were signaled
+    std::map<VkQueue, std::list<VkSync>>              mSignaledSyncs;
+
     std::map<VkQueue, VkCommandPool>                  mCommandPool;
     std::map<VkQueue, std::unique_ptr<ThreadContext>> mThreads;
 
-    static constexpr int MAX_PENDING_FENCES = 1;
+    static constexpr int MAX_PENDING_FENCES = 2;
+
+    std::atomic<std::chrono::nanoseconds> mLastFenceTime = {};
 
     void initGoogExtension();
     VkResult initializeVkSyncObjects(VkQueue queue, uint32_t queueFamilyIndex);
     void destroyVkSyncObjects();
+    void reclaimSignaledFences(VkQueue queue);
     bool lastFrameIsCompleted(VkQueue queue);
     std::chrono::nanoseconds getLastFenceTime(VkQueue queue);
     void waitForFenceThreadMain(VkQueue queue);
diff --git a/src/swappy/vulkan/SwappyVkGoogleDisplayTiming.cpp b/src/swappy/vulkan/SwappyVkGoogleDisplayTiming.cpp
index 321e451..c907977 100644
--- a/src/swappy/vulkan/SwappyVkGoogleDisplayTiming.cpp
+++ b/src/swappy/vulkan/SwappyVkGoogleDisplayTiming.cpp
@@ -75,7 +75,6 @@
         .getPrevFrameGpuTime =
             std::bind(&SwappyVkGoogleDisplayTiming::getLastFenceTime, this, queue),
     };
-    mCommonBase.onPreSwap(handlers);
 
     VkSemaphore semaphore;
     res = injectFence(queue, pPresentInfo, &semaphore);
@@ -84,6 +83,8 @@
         return res;
     }
 
+    mCommonBase.onPreSwap(handlers);
+
     VkPresentTimeGOOGLE pPresentTimes[pPresentInfo->swapchainCount];
     VkPresentInfoKHR replacementPresentInfo;
     VkPresentTimesInfoGOOGLE presentTimesInfo;
diff --git a/src/swappy/vulkan/swappyVk_c.cpp b/src/swappy/vulkan/swappyVk_c.cpp
index b3c37fd..2aa365a 100644
--- a/src/swappy/vulkan/swappyVk_c.cpp
+++ b/src/swappy/vulkan/swappyVk_c.cpp
@@ -107,6 +107,12 @@
     swappy.SetFenceTimeout(std::chrono::nanoseconds(fence_timeout_ns));
 }
 
+void SwappyVk_setMaxAutoSwapIntervalNS(uint64_t max_swap_ns) {
+    TRACE_CALL();
+    swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
+    swappy.SetMaxAutoSwapIntervalNS(std::chrono::nanoseconds(max_swap_ns));
+}
+
 uint64_t SwappyVk_getFenceTimeoutNS() {
     TRACE_CALL();
     swappy::SwappyVk& swappy = swappy::SwappyVk::getInstance();
diff --git a/src/tuningfork/crash_handler.cpp b/src/tuningfork/crash_handler.cpp
index 1b5e48b..7d54b90 100644
--- a/src/tuningfork/crash_handler.cpp
+++ b/src/tuningfork/crash_handler.cpp
@@ -14,6 +14,16 @@
  * limitations under the License.
  */
 
+#include "crash_handler.h"
+
+#if __ANDROID_API__ < 16
+namespace tuningfork {
+    CrashHandler::CrashHandler() { }
+    CrashHandler::~CrashHandler() { }
+    void CrashHandler::Init(std::function<bool(void)> callback) { }
+}
+#else
+
 #include <vector>
 #include <pthread.h>
 #include <sys/syscall.h>
@@ -23,7 +33,6 @@
 #include <cstdlib>
 #include <algorithm>
 
-#include "crash_handler.h"
 
 #include "Log.h"
 #define LOG_TAG "TFCrashHandler"
@@ -249,3 +258,4 @@
     return true;
 }
 }  // namespace tuningfork
+#endif
diff --git a/third_party/cube/app/CMakeLists.txt b/third_party/cube/app/CMakeLists.txt
index ca2777c..467b15a 100644
--- a/third_party/cube/app/CMakeLists.txt
+++ b/third_party/cube/app/CMakeLists.txt
@@ -34,11 +34,6 @@
     )
 endforeach()
 
-# Force export ANativeActivity_onCreate(),
-# Refer to: https://github.com/android-ndk/ndk/issues/381.
-set(CMAKE_SHARED_LINKER_FLAGS
-    "${CMAKE_SHARED_LINKER_FLAGS} -u ANativeActivity_onCreate")
-
 include_directories( src/main/cpp )
 
 set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99  -Werror -DVK_USE_PLATFORM_ANDROID_KHR")
@@ -50,9 +45,8 @@
 
              cube.vert.inc
              cube.frag.inc
-             ${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c
              src/main/cpp/cube.c
-             src/main/cpp/common/android_util.cpp
+             src/main/cpp/native-lib.c
              src/main/cpp/common/vulkan_wrapper.cpp
         )
 
@@ -63,8 +57,7 @@
                            ${SHADERC_SRC}/third_party/spirv-tools/include
                            ${SHADERC_SRC}/third_party/spirv-tools/include/spirv-tools
                            src/main/cpp/include
-                           src/main/cpp/common
-                           ${ANDROID_NDK}/sources/android/native_app_glue)
+                           src/main/cpp/common)
 
 
 target_link_libraries( native-lib
diff --git a/third_party/cube/app/build.gradle b/third_party/cube/app/build.gradle
index 248ac4a..a4c1801 100644
--- a/third_party/cube/app/build.gradle
+++ b/third_party/cube/app/build.gradle
@@ -68,4 +68,5 @@
 
 dependencies {
     implementation fileTree(dir: 'libs', include: ['*.jar'])
+    implementation project(':extras')
 }
diff --git a/third_party/cube/app/src/main/AndroidManifest.xml b/third_party/cube/app/src/main/AndroidManifest.xml
index 9a311d9..ea51328 100644
--- a/third_party/cube/app/src/main/AndroidManifest.xml
+++ b/third_party/cube/app/src/main/AndroidManifest.xml
@@ -1,24 +1,22 @@
-<?xml version="1.0"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.Cube" android:versionCode="1" android:versionName="1.0">
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.samples.cube">
 
-    <!-- Allow this app to read and write files (for use by tracing libraries). -->
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
-    <uses-permission android:name="android.permission.INTERNET"/>
-
-    <!-- This .apk has no Java code itself, so set hasCode to false. -->
-    <application android:label="@string/app_name" android:hasCode="false">
-
-        <!-- Our activity is the built-in NativeActivity framework class.
-             This will take care of integrating with our NDK code. -->
-        <activity android:name="android.app.NativeActivity" android:label="@string/app_name" android:exported="true">
-            <!-- Tell NativeActivity the name of or .so -->
-            <meta-data android:name="android.app.lib_name" android:value="native-lib"/>
+    <application android:label="@string/app_name">
+        <activity
+            android:name=".CubeActivity"
+            android:launchMode="singleTop"
+            android:configChanges="orientation|screenSize"
+            android:screenOrientation="portrait">
             <intent-filter>
-                <action android:name="android.intent.action.MAIN"/>
-                <category android:name="android.intent.category.LAUNCHER"/>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.SEND" />
+                <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
     </application>
 
-</manifest>
+</manifest>
\ No newline at end of file
diff --git a/third_party/cube/app/src/main/cpp/cube.c b/third_party/cube/app/src/main/cpp/cube.c
index 7b7b250..3636fb5 100644
--- a/third_party/cube/app/src/main/cpp/cube.c
+++ b/third_party/cube/app/src/main/cpp/cube.c
@@ -255,6 +255,7 @@
     float mvp[4][4];
     float position[12 * 3][4];
     float attr[12 * 3][4];
+    uint32_t gpu_workload;
 };
 
 //--------------------------------------------------------------------------------------
@@ -496,10 +497,19 @@
     mat4x4 view_matrix;
     mat4x4 model_matrix;
 
+    uint32_t gpu_workload;
+    uint64_t cpu_workload;
+
+    float scale;
     float spin_angle;
     float spin_increment;
+    float spin_speed;
     bool pause;
 
+    bool draw_cmd_dirty;
+
+    struct vktexcube_vs_uniform uniform;
+
     VkShaderModule vert_shader_module;
     VkShaderModule frag_shader_module;
 
@@ -529,6 +539,8 @@
         JavaVM* vm;
         jobject jactivity;
     } swappy_init_data;
+
+    bool tracer_injected;
 };
 
 VKAPI_ATTR VkBool32 VKAPI_CALL debug_messenger_callback(VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity,
@@ -944,23 +956,33 @@
 }
 
 void demo_update_data_buffer(struct demo *demo) {
-    mat4x4 MVP, Model, VP;
-    int matrixSize = sizeof(MVP);
+    mat4x4 Model, VP;
     uint8_t *pData;
     VkResult U_ASSERT_ONLY err;
 
     mat4x4_mul(VP, demo->projection_matrix, demo->view_matrix);
 
-    // Rotate around the Y axis
+    // Set scale.
+    mat4x4_identity(demo->model_matrix);
+    demo->model_matrix[0][0] = demo->scale;
+    demo->model_matrix[1][1] = demo->scale;
+    demo->model_matrix[2][2] = demo->scale;
+
+    uint64_t long_time = getTimeInNanoseconds();
+    long_time = ((long_time << 16) >> 16) >> 18; // Keep only middle bits.
+    demo->spin_angle = long_time * demo->spin_speed;
+
+    // Rotate around the Y axis.
     mat4x4_dup(Model, demo->model_matrix);
-    mat4x4_rotate(demo->model_matrix, Model, 0.0f, 1.0f, 0.0f, (float)degreesToRadians(demo->spin_angle));
-    mat4x4_mul(MVP, VP, demo->model_matrix);
+    mat4x4_rotate(demo->model_matrix, Model, 0.0f, 1.0f, 0.0f, demo->spin_angle);
+    mat4x4_mul(demo->uniform.mvp, VP, demo->model_matrix);
+
+    demo->uniform.gpu_workload = demo->gpu_workload;
 
     err = vkMapMemory(demo->device, demo->swapchain_image_resources[demo->current_buffer].uniform_memory, 0, VK_WHOLE_SIZE, 0,
                       (void **)&pData);
     assert(!err);
-
-    memcpy(pData, (const void *)&MVP[0][0], matrixSize);
+    memcpy(pData, &demo->uniform, sizeof(demo->uniform));
 
     vkUnmapMemory(demo->device, demo->swapchain_image_resources[demo->current_buffer].uniform_memory);
 }
@@ -1081,6 +1103,17 @@
     }
 }
 
+static void update_draw_cmd(struct demo *demo) {
+    // Rerecord draw cmds.
+    vkDeviceWaitIdle(demo->device);
+    uint32_t current_buffer = demo->current_buffer;
+    for (uint32_t i = 0; i < demo->swapchainImageCount; i++) {
+      demo->current_buffer = i;
+      demo_draw_build_cmd(demo, demo->swapchain_image_resources[i].cmd);
+    }
+    demo->current_buffer = current_buffer;
+}
+
 static void demo_draw(struct demo *demo) {
     VkResult U_ASSERT_ONLY err;
 
@@ -1093,6 +1126,11 @@
 
     vkResetFences(demo->device, 1, &demo->fences[demo->frame_index]);
 
+    if (demo->draw_cmd_dirty) {
+        update_draw_cmd(demo);
+        demo->draw_cmd_dirty = false;
+    }
+
     do {
         // Get the index of the next available swapchain image:
         logEvent(EVENT_CALLING_ANI);
@@ -1514,14 +1552,17 @@
     // Refresh rate of this demo is locked to 30 FPS.
     SwappyVk_setSwapIntervalNS(demo->device, demo->swapchain, SWAPPY_SWAP_30FPS);
 
-    SwappyTracer tracer;
-    tracer.preWait = swappy_trace_test_preWait;
-    tracer.postWait = swappy_trace_test_postWait;
-    tracer.preSwapBuffers = swappy_trace_test_preSwapBuffers;
-    tracer.postSwapBuffers = swappy_trace_test_postSwapBuffers;
-    tracer.startFrame = swappy_trace_test_startFrame;
-    tracer.swapIntervalChanged = swappy_trace_test_swapIntervalChanged;
-    SwappyVk_injectTracer(&tracer);
+    if (!demo->tracer_injected) {
+      SwappyTracer tracer;
+      tracer.preWait = swappy_trace_test_preWait;
+      tracer.postWait = swappy_trace_test_postWait;
+      tracer.preSwapBuffers = swappy_trace_test_preSwapBuffers;
+      tracer.postSwapBuffers = swappy_trace_test_postSwapBuffers;
+      tracer.startFrame = swappy_trace_test_startFrame;
+      tracer.swapIntervalChanged = swappy_trace_test_swapIntervalChanged;
+      SwappyVk_injectTracer(&tracer);
+      demo->tracer_injected = true;
+    }
 
     if (NULL != presentModes) {
         free(presentModes);
@@ -1876,31 +1917,29 @@
     VkMemoryRequirements mem_reqs;
     VkMemoryAllocateInfo mem_alloc;
     uint8_t *pData;
-    mat4x4 MVP, VP;
+    mat4x4 VP;
     VkResult U_ASSERT_ONLY err;
     bool U_ASSERT_ONLY pass;
-    struct vktexcube_vs_uniform data;
 
     mat4x4_mul(VP, demo->projection_matrix, demo->view_matrix);
-    mat4x4_mul(MVP, VP, demo->model_matrix);
-    memcpy(data.mvp, MVP, sizeof(MVP));
-    //    dumpMatrix("MVP", MVP);
+    mat4x4_mul(demo->uniform.mvp, VP, demo->model_matrix);
+    //    dumpMatrix("MVP", demo->uniform.mvp);
 
     for (unsigned int i = 0; i < 12 * 3; i++) {
-        data.position[i][0] = g_vertex_buffer_data[i * 3];
-        data.position[i][1] = g_vertex_buffer_data[i * 3 + 1];
-        data.position[i][2] = g_vertex_buffer_data[i * 3 + 2];
-        data.position[i][3] = 1.0f;
-        data.attr[i][0] = g_uv_buffer_data[2 * i];
-        data.attr[i][1] = g_uv_buffer_data[2 * i + 1];
-        data.attr[i][2] = 0;
-        data.attr[i][3] = 0;
+        demo->uniform.position[i][0] = g_vertex_buffer_data[i * 3];
+        demo->uniform.position[i][1] = g_vertex_buffer_data[i * 3 + 1];
+        demo->uniform.position[i][2] = g_vertex_buffer_data[i * 3 + 2];
+        demo->uniform.position[i][3] = 1.0f;
+        demo->uniform.attr[i][0] = g_uv_buffer_data[2 * i];
+        demo->uniform.attr[i][1] = g_uv_buffer_data[2 * i + 1];
+        demo->uniform.attr[i][2] = 0;
+        demo->uniform.attr[i][3] = 0;
     }
 
     memset(&buf_info, 0, sizeof(buf_info));
     buf_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
     buf_info.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
-    buf_info.size = sizeof(data);
+    buf_info.size = sizeof(demo->uniform);
 
     for (unsigned int i = 0; i < demo->swapchainImageCount; i++) {
         err = vkCreateBuffer(demo->device, &buf_info, NULL, &demo->swapchain_image_resources[i].uniform_buffer);
@@ -1924,7 +1963,7 @@
         err = vkMapMemory(demo->device, demo->swapchain_image_resources[i].uniform_memory, 0, VK_WHOLE_SIZE, 0, (void **)&pData);
         assert(!err);
 
-        memcpy(pData, &data, sizeof data);
+        memcpy(pData, &demo->uniform, sizeof(demo->uniform));
 
         vkUnmapMemory(demo->device, demo->swapchain_image_resources[i].uniform_memory);
 
@@ -1941,7 +1980,7 @@
                 .binding = 0,
                 .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
                 .descriptorCount = 1,
-                .stageFlags = VK_SHADER_STAGE_VERTEX_BIT,
+                .stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
                 .pImmutableSamplers = NULL,
             },
         [1] =
@@ -2413,7 +2452,6 @@
             vkDestroyFramebuffer(demo->device, demo->swapchain_image_resources[i].framebuffer, NULL);
         }
         vkDestroyDescriptorPool(demo->device, demo->desc_pool, NULL);
-
         vkDestroyPipeline(demo->device, demo->pipeline, NULL);
         vkDestroyPipelineCache(demo->device, demo->pipelineCache, NULL);
         vkDestroyRenderPass(demo->device, demo->render_pass, NULL);
@@ -2426,8 +2464,10 @@
             vkFreeMemory(demo->device, demo->textures[i].mem, NULL);
             vkDestroySampler(demo->device, demo->textures[i].sampler, NULL);
         }
+
         SwappyVk_destroySwapchain(demo->device, demo->swapchain);
         demo->fpDestroySwapchainKHR(demo->device, demo->swapchain, NULL);
+        demo->swapchain = VK_NULL_HANDLE;
 
         vkDestroyImageView(demo->device, demo->depth.view, NULL);
         vkDestroyImage(demo->device, demo->depth.image, NULL);
@@ -2439,6 +2479,7 @@
             vkDestroyBuffer(demo->device, demo->swapchain_image_resources[i].uniform_buffer, NULL);
             vkFreeMemory(demo->device, demo->swapchain_image_resources[i].uniform_memory, NULL);
         }
+
         free(demo->swapchain_image_resources);
         free(demo->queue_props);
         vkDestroyCommandPool(demo->device, demo->cmd_pool, NULL);
@@ -2447,6 +2488,7 @@
             vkDestroyCommandPool(demo->device, demo->present_cmd_pool, NULL);
         }
     }
+
     vkDeviceWaitIdle(demo->device);
     vkDestroyDevice(demo->device, NULL);
     if (demo->validate) {
@@ -2533,10 +2575,11 @@
     demo_prepare(demo);
 }
 
+#if defined(VK_USE_PLATFORM_WIN32_KHR)
+
 // On MS-Windows, make this a global, so it's available to WndProc()
 struct demo demo;
 
-#if defined(VK_USE_PLATFORM_WIN32_KHR)
 static void demo_run(struct demo *demo) {
     if (!demo->prepared) return;
 
@@ -3821,7 +3864,6 @@
     vec3 origin = {0, 0, 0};
     vec3 up = {0.0f, 1.0f, 0.0};
 
-    memset(demo, 0, sizeof(*demo));
     demo->presentMode = VK_PRESENT_MODE_FIFO_KHR;
     demo->frameCount = INT32_MAX;
 
@@ -3895,10 +3937,15 @@
     demo->width = 500;
     demo->height = 500;
 
+    demo->scale = 1.0f;
+
     demo->spin_angle = 4.0f;
     demo->spin_increment = 0.2f;
+    demo->spin_speed = 0.0005f;
     demo->pause = false;
 
+    demo->draw_cmd_dirty = true;
+
     mat4x4_perspective(demo->projection_matrix, (float)degreesToRadians(45.0f), 1.0f, 0.1f, 100.0f);
     mat4x4_look_at(demo->view_matrix, eye, origin, up);
     mat4x4_identity(demo->model_matrix);
@@ -4000,99 +4047,60 @@
 }
 
 #elif defined(VK_USE_PLATFORM_ANDROID_KHR)
+#include "cube.h"
 #include <android/log.h>
-#include <android_native_app_glue.h>
-#include "android_util.h"
 
 static bool initialized = false;
 static bool active = false;
-struct demo demo;
+struct demo demo_;
 
-static int32_t processInput(struct android_app *app, AInputEvent *event) { return 0; }
-
-static void processCommand(struct android_app *app, int32_t cmd) {
-    switch (cmd) {
-        case APP_CMD_INIT_WINDOW: {
-            if (app->window) {
-                // We're getting a new window.  If the app is starting up, we
-                // need to initialize.  If the app has already been
-                // initialized, that means that we lost our previous window,
-                // which means that we have a lot of work to do.  At a minimum,
-                // we need to destroy the swapchain and surface associated with
-                // the old window, and create a new surface and swapchain.
-                // However, since there are a lot of other objects/state that
-                // is tied to the swapchain, it's easiest to simply cleanup and
-                // start over (i.e. use a brute-force approach of re-starting
-                // the app)
-                if (demo.prepared) {
-                    demo_cleanup(&demo);
-                }
-
-                // Parse Intents into argc, argv
-                // Use the following key to send arguments, i.e.
-                // --es args "--validate"
-                const char key[] = "args";
-                char *appTag = (char *)APP_SHORT_NAME;
-                int argc = 0;
-                char **argv = get_args(app, key, appTag, &argc);
-
-                __android_log_print(ANDROID_LOG_INFO, appTag, "argc = %i", argc);
-                for (int i = 0; i < argc; i++) __android_log_print(ANDROID_LOG_INFO, appTag, "argv[%i] = %s", i, argv[i]);
-
-                demo_init(&demo, argc, argv);
-                demo.swappy_init_data.vm        = app->activity->vm;
-                demo.swappy_init_data.jactivity = app->activity->clazz;
-
-                // Free the argv malloc'd by get_args
-                for (int i = 0; i < argc; i++) free(argv[i]);
-
-                demo.window = (void *)app->window;
-                demo_init_vk_swapchain(&demo);
-                demo_prepare(&demo);
-                initialized = true;
-            }
-            break;
-        }
-        case APP_CMD_GAINED_FOCUS: {
-            active = true;
-            break;
-        }
-        case APP_CMD_LOST_FOCUS: {
-            active = false;
-            break;
-        }
-    }
+void update_gpu_workload(int32_t new_workload) {
+    demo_.gpu_workload = new_workload;
+    demo_.draw_cmd_dirty = true;
 }
 
-void android_main(struct android_app *app) {
-#ifdef ANDROID
+void update_cpu_workload(int32_t new_workload) {
+  demo_.cpu_workload = (uint64_t)100*(uint64_t)new_workload;
+}
+
+void main_loop(struct android_app_state *app) {
     int vulkanSupport = InitVulkan();
     if (vulkanSupport == 0) return;
-#endif
 
-    demo.prepared = false;
+    demo_.prepared = false;
 
-    app->onAppCmd = processCommand;
-    app->onInputEvent = processInput;
-
-    while (1) {
-        int events;
-        struct android_poll_source *source;
-        while (ALooper_pollAll(active ? 0 : -1, NULL, &events, (void **)&source) >= 0) {
-            if (source) {
-                source->process(app, source);
-            }
-
-            if (app->destroyRequested != 0) {
-                demo_cleanup(&demo);
-                return;
-            }
+  while(true) {
+        if (!initialized) {
+            demo_.window = app->window;
+            demo_.swappy_init_data.vm        = app->vm;
+            demo_.swappy_init_data.jactivity = app->clazz;
+            demo_init(&demo_, 0, NULL);
+            demo_init_vk_swapchain(&demo_);
+            demo_prepare(&demo_);
+            initialized = true;
+            active = true;
         }
+
+        if (app->destroyRequested != 0) {
+            JavaVM* vm = app->vm;
+            (*vm)->DetachCurrentThread(vm);
+
+            demo_cleanup(&demo_);
+            initialized = false;
+            active = false;
+
+            return;
+        }
+
         if (initialized && active) {
-            demo_run(&demo);
+            demo_run(&demo_);
         }
-    }
+
+        uint64_t count = 0;
+        while(count++ < demo_.cpu_workload) {}
+  }
 }
+
 #else
 int main(int argc, char **argv) {
     struct demo demo;
diff --git a/third_party/cube/app/src/main/cpp/cube.frag b/third_party/cube/app/src/main/cpp/cube.frag
index 5bf6507..edaaecc 100644
--- a/third_party/cube/app/src/main/cpp/cube.frag
+++ b/third_party/cube/app/src/main/cpp/cube.frag
@@ -21,6 +21,13 @@
 #version 400
 #extension GL_ARB_separate_shader_objects : enable
 #extension GL_ARB_shading_language_420pack : enable
+layout(std140, binding = 0) uniform buf {
+        mat4 MVP;
+        vec4 position[12*3];
+        vec4 attr[12*3];
+        highp int gpu_workload;
+} ubuf;
+
 layout (binding = 1) uniform sampler2D tex;
 
 layout (location = 0) in vec4 texcoord;
@@ -30,9 +37,16 @@
 const vec3 lightDir= vec3(0.424, 0.566, 0.707);
 
 void main() {
+   float tint = 0;
+
+   for(int i = 0; i < ubuf.gpu_workload; ++i) {
+     tint += 0.0001;
+   }
+
    vec3 dX = dFdx(frag_pos);
    vec3 dY = dFdy(frag_pos);
    vec3 normal = normalize(cross(dX,dY));
    float light = max(0.0, dot(lightDir, normal));
    uFragColor = light * texture(tex, texcoord.xy);
+   uFragColor.x += tint;
 }
diff --git a/third_party/cube/app/src/main/cpp/cube.vert b/third_party/cube/app/src/main/cpp/cube.vert
index 6338032..1c89080 100644
--- a/third_party/cube/app/src/main/cpp/cube.vert
+++ b/third_party/cube/app/src/main/cpp/cube.vert
@@ -30,7 +30,7 @@
 layout (location = 0) out vec4 texcoord;
 layout (location = 1) out vec3 frag_pos;
 
-void main() 
+void main()
 {
    texcoord = ubuf.attr[gl_VertexIndex];
    gl_Position = ubuf.MVP * ubuf.position[gl_VertexIndex];
diff --git a/third_party/cube/app/src/main/cpp/include/cube.frag.inc b/third_party/cube/app/src/main/cpp/include/cube.frag.inc
index cbfbf5a..7c95590 100644
--- a/third_party/cube/app/src/main/cpp/include/cube.frag.inc
+++ b/third_party/cube/app/src/main/cpp/include/cube.frag.inc
@@ -1,9 +1,9 @@
-0x07230203,0x00010000,0x000d0006,0x00000030,
+0x07230203,0x00010000,0x000d0007,0x00000056,
 0x00000000,0x00020011,0x00000001,0x0006000b,
 0x00000001,0x4c534c47,0x6474732e,0x3035342e,
 0x00000000,0x0003000e,0x00000000,0x00000001,
 0x0008000f,0x00000004,0x00000004,0x6e69616d,
-0x00000000,0x0000000b,0x00000022,0x0000002a,
+0x00000000,0x0000002d,0x00000041,0x00000049,
 0x00030010,0x00000004,0x00000007,0x00030003,
 0x00000002,0x00000190,0x00090004,0x415f4c47,
 0x735f4252,0x72617065,0x5f657461,0x64616873,
@@ -15,71 +15,137 @@
 0x69746365,0x00006576,0x00080004,0x475f4c47,
 0x4c474f4f,0x6e695f45,0x64756c63,0x69645f65,
 0x74636572,0x00657669,0x00040005,0x00000004,
-0x6e69616d,0x00000000,0x00030005,0x00000009,
-0x00005864,0x00050005,0x0000000b,0x67617266,
-0x736f705f,0x00000000,0x00030005,0x0000000e,
-0x00005964,0x00040005,0x00000011,0x6d726f6e,
-0x00006c61,0x00040005,0x00000017,0x6867696c,
-0x00000074,0x00050005,0x00000022,0x61724675,
-0x6c6f4367,0x0000726f,0x00030005,0x00000027,
-0x00786574,0x00050005,0x0000002a,0x63786574,
-0x64726f6f,0x00000000,0x00040047,0x0000000b,
-0x0000001e,0x00000001,0x00040047,0x00000022,
-0x0000001e,0x00000000,0x00040047,0x00000027,
-0x00000022,0x00000000,0x00040047,0x00000027,
-0x00000021,0x00000001,0x00040047,0x0000002a,
+0x6e69616d,0x00000000,0x00040005,0x00000008,
+0x746e6974,0x00000000,0x00030005,0x0000000c,
+0x00000069,0x00030005,0x0000001a,0x00667562,
+0x00040006,0x0000001a,0x00000000,0x0050564d,
+0x00060006,0x0000001a,0x00000001,0x69736f70,
+0x6e6f6974,0x00000000,0x00050006,0x0000001a,
+0x00000002,0x72747461,0x00000000,0x00070006,
+0x0000001a,0x00000003,0x5f757067,0x6b726f77,
+0x64616f6c,0x00000000,0x00040005,0x0000001c,
+0x66756275,0x00000000,0x00030005,0x0000002b,
+0x00005864,0x00050005,0x0000002d,0x67617266,
+0x736f705f,0x00000000,0x00030005,0x00000030,
+0x00005964,0x00040005,0x00000033,0x6d726f6e,
+0x00006c61,0x00040005,0x00000038,0x6867696c,
+0x00000074,0x00050005,0x00000041,0x61724675,
+0x6c6f4367,0x0000726f,0x00030005,0x00000046,
+0x00786574,0x00050005,0x00000049,0x63786574,
+0x64726f6f,0x00000000,0x00040047,0x00000018,
+0x00000006,0x00000010,0x00040047,0x00000019,
+0x00000006,0x00000010,0x00040048,0x0000001a,
+0x00000000,0x00000005,0x00050048,0x0000001a,
+0x00000000,0x00000023,0x00000000,0x00050048,
+0x0000001a,0x00000000,0x00000007,0x00000010,
+0x00050048,0x0000001a,0x00000001,0x00000023,
+0x00000040,0x00050048,0x0000001a,0x00000002,
+0x00000023,0x00000280,0x00050048,0x0000001a,
+0x00000003,0x00000023,0x000004c0,0x00030047,
+0x0000001a,0x00000002,0x00040047,0x0000001c,
+0x00000022,0x00000000,0x00040047,0x0000001c,
+0x00000021,0x00000000,0x00040047,0x0000002d,
+0x0000001e,0x00000001,0x00040047,0x00000041,
+0x0000001e,0x00000000,0x00040047,0x00000046,
+0x00000022,0x00000000,0x00040047,0x00000046,
+0x00000021,0x00000001,0x00040047,0x00000049,
 0x0000001e,0x00000000,0x00020013,0x00000002,
 0x00030021,0x00000003,0x00000002,0x00030016,
-0x00000006,0x00000020,0x00040017,0x00000007,
-0x00000006,0x00000003,0x00040020,0x00000008,
-0x00000007,0x00000007,0x00040020,0x0000000a,
-0x00000001,0x00000007,0x0004003b,0x0000000a,
-0x0000000b,0x00000001,0x00040020,0x00000016,
+0x00000006,0x00000020,0x00040020,0x00000007,
 0x00000007,0x00000006,0x0004002b,0x00000006,
-0x00000018,0x00000000,0x0004002b,0x00000006,
-0x00000019,0x3ed91687,0x0004002b,0x00000006,
-0x0000001a,0x3f10e560,0x0004002b,0x00000006,
-0x0000001b,0x3f34fdf4,0x0006002c,0x00000007,
-0x0000001c,0x00000019,0x0000001a,0x0000001b,
-0x00040017,0x00000020,0x00000006,0x00000004,
-0x00040020,0x00000021,0x00000003,0x00000020,
-0x0004003b,0x00000021,0x00000022,0x00000003,
-0x00090019,0x00000024,0x00000006,0x00000001,
+0x00000009,0x00000000,0x00040015,0x0000000a,
+0x00000020,0x00000001,0x00040020,0x0000000b,
+0x00000007,0x0000000a,0x0004002b,0x0000000a,
+0x0000000d,0x00000000,0x00040017,0x00000014,
+0x00000006,0x00000004,0x00040018,0x00000015,
+0x00000014,0x00000004,0x00040015,0x00000016,
+0x00000020,0x00000000,0x0004002b,0x00000016,
+0x00000017,0x00000024,0x0004001c,0x00000018,
+0x00000014,0x00000017,0x0004001c,0x00000019,
+0x00000014,0x00000017,0x0006001e,0x0000001a,
+0x00000015,0x00000018,0x00000019,0x0000000a,
+0x00040020,0x0000001b,0x00000002,0x0000001a,
+0x0004003b,0x0000001b,0x0000001c,0x00000002,
+0x0004002b,0x0000000a,0x0000001d,0x00000003,
+0x00040020,0x0000001e,0x00000002,0x0000000a,
+0x00020014,0x00000021,0x0004002b,0x00000006,
+0x00000023,0x38d1b717,0x0004002b,0x0000000a,
+0x00000027,0x00000001,0x00040017,0x00000029,
+0x00000006,0x00000003,0x00040020,0x0000002a,
+0x00000007,0x00000029,0x00040020,0x0000002c,
+0x00000001,0x00000029,0x0004003b,0x0000002c,
+0x0000002d,0x00000001,0x0004002b,0x00000006,
+0x00000039,0x3ed91687,0x0004002b,0x00000006,
+0x0000003a,0x3f10e560,0x0004002b,0x00000006,
+0x0000003b,0x3f34fdf4,0x0006002c,0x00000029,
+0x0000003c,0x00000039,0x0000003a,0x0000003b,
+0x00040020,0x00000040,0x00000003,0x00000014,
+0x0004003b,0x00000040,0x00000041,0x00000003,
+0x00090019,0x00000043,0x00000006,0x00000001,
 0x00000000,0x00000000,0x00000000,0x00000001,
-0x00000000,0x0003001b,0x00000025,0x00000024,
-0x00040020,0x00000026,0x00000000,0x00000025,
-0x0004003b,0x00000026,0x00000027,0x00000000,
-0x00040020,0x00000029,0x00000001,0x00000020,
-0x0004003b,0x00000029,0x0000002a,0x00000001,
-0x00040017,0x0000002b,0x00000006,0x00000002,
+0x00000000,0x0003001b,0x00000044,0x00000043,
+0x00040020,0x00000045,0x00000000,0x00000044,
+0x0004003b,0x00000045,0x00000046,0x00000000,
+0x00040020,0x00000048,0x00000001,0x00000014,
+0x0004003b,0x00000048,0x00000049,0x00000001,
+0x00040017,0x0000004a,0x00000006,0x00000002,
+0x0004002b,0x00000016,0x00000050,0x00000000,
+0x00040020,0x00000051,0x00000003,0x00000006,
 0x00050036,0x00000002,0x00000004,0x00000000,
 0x00000003,0x000200f8,0x00000005,0x0004003b,
-0x00000008,0x00000009,0x00000007,0x0004003b,
-0x00000008,0x0000000e,0x00000007,0x0004003b,
-0x00000008,0x00000011,0x00000007,0x0004003b,
-0x00000016,0x00000017,0x00000007,0x0004003d,
-0x00000007,0x0000000c,0x0000000b,0x000400cf,
-0x00000007,0x0000000d,0x0000000c,0x0003003e,
-0x00000009,0x0000000d,0x0004003d,0x00000007,
-0x0000000f,0x0000000b,0x000400d0,0x00000007,
-0x00000010,0x0000000f,0x0003003e,0x0000000e,
-0x00000010,0x0004003d,0x00000007,0x00000012,
-0x00000009,0x0004003d,0x00000007,0x00000013,
-0x0000000e,0x0007000c,0x00000007,0x00000014,
-0x00000001,0x00000044,0x00000012,0x00000013,
-0x0006000c,0x00000007,0x00000015,0x00000001,
-0x00000045,0x00000014,0x0003003e,0x00000011,
-0x00000015,0x0004003d,0x00000007,0x0000001d,
-0x00000011,0x00050094,0x00000006,0x0000001e,
-0x0000001c,0x0000001d,0x0007000c,0x00000006,
-0x0000001f,0x00000001,0x00000028,0x00000018,
-0x0000001e,0x0003003e,0x00000017,0x0000001f,
-0x0004003d,0x00000006,0x00000023,0x00000017,
-0x0004003d,0x00000025,0x00000028,0x00000027,
-0x0004003d,0x00000020,0x0000002c,0x0000002a,
-0x0007004f,0x0000002b,0x0000002d,0x0000002c,
-0x0000002c,0x00000000,0x00000001,0x00050057,
-0x00000020,0x0000002e,0x00000028,0x0000002d,
-0x0005008e,0x00000020,0x0000002f,0x0000002e,
-0x00000023,0x0003003e,0x00000022,0x0000002f,
+0x00000007,0x00000008,0x00000007,0x0004003b,
+0x0000000b,0x0000000c,0x00000007,0x0004003b,
+0x0000002a,0x0000002b,0x00000007,0x0004003b,
+0x0000002a,0x00000030,0x00000007,0x0004003b,
+0x0000002a,0x00000033,0x00000007,0x0004003b,
+0x00000007,0x00000038,0x00000007,0x0003003e,
+0x00000008,0x00000009,0x0003003e,0x0000000c,
+0x0000000d,0x000200f9,0x0000000e,0x000200f8,
+0x0000000e,0x000400f6,0x00000010,0x00000011,
+0x00000000,0x000200f9,0x00000012,0x000200f8,
+0x00000012,0x0004003d,0x0000000a,0x00000013,
+0x0000000c,0x00050041,0x0000001e,0x0000001f,
+0x0000001c,0x0000001d,0x0004003d,0x0000000a,
+0x00000020,0x0000001f,0x000500b1,0x00000021,
+0x00000022,0x00000013,0x00000020,0x000400fa,
+0x00000022,0x0000000f,0x00000010,0x000200f8,
+0x0000000f,0x0004003d,0x00000006,0x00000024,
+0x00000008,0x00050081,0x00000006,0x00000025,
+0x00000024,0x00000023,0x0003003e,0x00000008,
+0x00000025,0x000200f9,0x00000011,0x000200f8,
+0x00000011,0x0004003d,0x0000000a,0x00000026,
+0x0000000c,0x00050080,0x0000000a,0x00000028,
+0x00000026,0x00000027,0x0003003e,0x0000000c,
+0x00000028,0x000200f9,0x0000000e,0x000200f8,
+0x00000010,0x0004003d,0x00000029,0x0000002e,
+0x0000002d,0x000400cf,0x00000029,0x0000002f,
+0x0000002e,0x0003003e,0x0000002b,0x0000002f,
+0x0004003d,0x00000029,0x00000031,0x0000002d,
+0x000400d0,0x00000029,0x00000032,0x00000031,
+0x0003003e,0x00000030,0x00000032,0x0004003d,
+0x00000029,0x00000034,0x0000002b,0x0004003d,
+0x00000029,0x00000035,0x00000030,0x0007000c,
+0x00000029,0x00000036,0x00000001,0x00000044,
+0x00000034,0x00000035,0x0006000c,0x00000029,
+0x00000037,0x00000001,0x00000045,0x00000036,
+0x0003003e,0x00000033,0x00000037,0x0004003d,
+0x00000029,0x0000003d,0x00000033,0x00050094,
+0x00000006,0x0000003e,0x0000003c,0x0000003d,
+0x0007000c,0x00000006,0x0000003f,0x00000001,
+0x00000028,0x00000009,0x0000003e,0x0003003e,
+0x00000038,0x0000003f,0x0004003d,0x00000006,
+0x00000042,0x00000038,0x0004003d,0x00000044,
+0x00000047,0x00000046,0x0004003d,0x00000014,
+0x0000004b,0x00000049,0x0007004f,0x0000004a,
+0x0000004c,0x0000004b,0x0000004b,0x00000000,
+0x00000001,0x00050057,0x00000014,0x0000004d,
+0x00000047,0x0000004c,0x0005008e,0x00000014,
+0x0000004e,0x0000004d,0x00000042,0x0003003e,
+0x00000041,0x0000004e,0x0004003d,0x00000006,
+0x0000004f,0x00000008,0x00050041,0x00000051,
+0x00000052,0x00000041,0x00000050,0x0004003d,
+0x00000006,0x00000053,0x00000052,0x00050081,
+0x00000006,0x00000054,0x00000053,0x0000004f,
+0x00050041,0x00000051,0x00000055,0x00000041,
+0x00000050,0x0003003e,0x00000055,0x00000054,
 0x000100fd,0x00010038
diff --git a/third_party/cube/app/src/main/cpp/include/cube.h b/third_party/cube/app/src/main/cpp/include/cube.h
new file mode 100644
index 0000000..58e4900
--- /dev/null
+++ b/third_party/cube/app/src/main/cpp/include/cube.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <android/log.h>
+#include <android/looper.h>
+#include <android/native_activity.h>
+#include <stdbool.h>
+
+// Struct for passing state from java app to native app.
+struct android_app_state {
+    ANativeWindow* window;
+    JavaVM* vm;
+    jobject clazz;
+    bool running;
+    bool destroyRequested;
+};
+
+// Start the cube application's render loop.
+void main_loop(struct android_app_state*);
+
+// Update the amount of GPU work done each frame.
+void update_gpu_workload(int32_t new_workload);
+
+// Update the amount of CPU work done each frame.
+void update_cpu_workload(int32_t new_workload);
diff --git a/third_party/cube/app/src/main/cpp/include/cube.vert.inc b/third_party/cube/app/src/main/cpp/include/cube.vert.inc
index 3828d39..55c5fae 100644
--- a/third_party/cube/app/src/main/cpp/include/cube.vert.inc
+++ b/third_party/cube/app/src/main/cpp/include/cube.vert.inc
@@ -1,4 +1,4 @@
-0x07230203,0x00010000,0x000d0006,0x0000002f,
+0x07230203,0x00010000,0x000d0007,0x0000002f,
 0x00000000,0x00020011,0x00000001,0x0006000b,
 0x00000001,0x4c534c47,0x6474732e,0x3035342e,
 0x00000000,0x0003000e,0x00000000,0x00000001,
diff --git a/third_party/cube/app/src/main/cpp/native-lib.c b/third_party/cube/app/src/main/cpp/native-lib.c
new file mode 100644
index 0000000..e5c2737
--- /dev/null
+++ b/third_party/cube/app/src/main/cpp/native-lib.c
@@ -0,0 +1,49 @@
+#include <jni.h>
+#include <android/native_window_jni.h>
+#include <pthread.h>
+
+#include "cube.h"
+
+static struct android_app_state state;
+static pthread_t thread;
+
+static void *startCubes(void *state_void_ptr)
+{
+    struct android_app_state* state = (struct android_app_state*)state_void_ptr;
+    state->running = true;
+    main_loop(state);
+    state->running = false;
+    state->destroyRequested = false;
+    return NULL;
+}
+
+
+JNIEXPORT void JNICALL
+Java_com_samples_cube_CubeActivity_nStartCube(JNIEnv *env, jobject clazz, jobject surface) {
+    if (!surface || state.running) {
+        return;
+    }
+    state.window = ANativeWindow_fromSurface(env, surface);
+    state.clazz = (jobject) (*env)->NewGlobalRef(env, clazz);
+    (*env)->GetJavaVM(env, &state.vm);
+
+    pthread_create(&thread, NULL, startCubes, &state);
+}
+
+JNIEXPORT void JNICALL
+Java_com_samples_cube_CubeActivity_nStopCube(JNIEnv *env, jobject clazz) {
+    if (state.running) {
+        state.destroyRequested = true;
+        pthread_join(thread, NULL);
+    }
+}
+
+JNIEXPORT void JNICALL
+Java_com_samples_cube_CubeActivity_nUpdateGpuWorkload(JNIEnv *env, jobject clazz, jint new_workload) {
+    update_gpu_workload(new_workload);
+}
+
+JNIEXPORT void JNICALL
+Java_com_samples_cube_CubeActivity_nUpdateCpuWorkload(JNIEnv *env, jobject clazz, jint new_workload) {
+    update_cpu_workload(new_workload);
+}
diff --git a/third_party/cube/app/src/main/java/com/samples/cube/CubeActivity.java b/third_party/cube/app/src/main/java/com/samples/cube/CubeActivity.java
new file mode 100644
index 0000000..ca1244d
--- /dev/null
+++ b/third_party/cube/app/src/main/java/com/samples/cube/CubeActivity.java
@@ -0,0 +1,208 @@
+package com.samples.cube;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.Window;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+public class CubeActivity extends Activity implements SurfaceHolder.Callback  {
+
+    private static final String GPU_WORKLOAD = "com.samples.GPU_WORKLOAD";
+    private static final String CPU_WORKLOAD = "com.samples.CPU_WORKLOAD";
+    private static final String APP_NAME = "CubeActivity";
+
+    private LinearLayout settingsLayout;
+
+    private SeekBar gpuWorkSeekBar;
+    private TextView gpuWorkText;
+    private SeekBar cpuWorkSeekBar;
+    private TextView cpuWorkText;
+
+    // Used to load the 'native-lib' library on application startup.
+    static {
+        try {
+            System.loadLibrary("native-lib");
+        } catch (Exception e) {
+            Log.e(APP_NAME, "Native code library failed to load.\n" + e);
+        }
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        setContentView(R.layout.activity_cube);
+        settingsLayout = findViewById(R.id.settingsLayout);
+
+        setupGpuWorkSeekBar();
+        setupCpuWorkSeekBar();
+
+        SurfaceView surfaceView = findViewById(R.id.surface_view);
+        surfaceView.getHolder().addCallback(this);
+        surfaceView.setOnTouchListener(new OnTouchListener() {
+            @Override
+            public boolean onTouch(View view, MotionEvent event) {
+                if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+                    toggleOptionsPanel();
+                }
+                return true;
+            }
+        });
+
+        Intent intent = getIntent();
+        String action = intent.getAction();
+        String type = intent.getType();
+        switch (action) {
+          case Intent.ACTION_MAIN:
+            updateGpuWork(0);
+            updateCpuWork(0);
+            break;
+          case Intent.ACTION_SEND:
+            handleSendIntent(intent);
+            break;
+          default:
+            Log.e(APP_NAME, "Unknown intent received: " + action);
+            break;
+        }
+    }
+
+    private void toggleOptionsPanel() {
+        if (settingsLayout.getVisibility() == View.GONE) {
+            settingsLayout.setVisibility(View.VISIBLE);
+        } else {
+            settingsLayout.setVisibility(View.GONE);
+        }
+    }
+
+    @Override
+    protected void onNewIntent(Intent intent) {
+        String action = intent.getAction();
+        if (Intent.ACTION_SEND.equals(action)) {
+            handleSendIntent(intent);
+        }
+    }
+
+    private void handleSendIntent(Intent intent) {
+        String gpuWorkStr = intent.getStringExtra(GPU_WORKLOAD);
+        if (gpuWorkStr != null) {
+            updateGpuWork(Integer.parseInt(gpuWorkStr));
+            Log.d(APP_NAME, "GPU work changed by intent. gpu_workload: " + gpuWorkStr);
+        }
+        String cpuWorkStr = intent.getStringExtra(CPU_WORKLOAD);
+        if (cpuWorkStr != null) {
+            updateCpuWork(Integer.parseInt(cpuWorkStr));
+            Log.d(APP_NAME, "CPU work changed by intent. cpu_workload: " + cpuWorkStr);
+        }
+    }
+
+    private void setupGpuWorkSeekBar() {
+        gpuWorkSeekBar = findViewById(R.id.seekBarGpuWork);
+        gpuWorkText = findViewById(R.id.textViewGpuWork);
+
+        gpuWorkSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+            @Override
+            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                if (!fromUser) {
+                    return;
+                }
+
+                int newGpuWork = linearToExponential(progress);
+                updateGpuWork(newGpuWork, true);
+            }
+
+            @Override
+            public void onStartTrackingTouch(SeekBar seekBar) {}
+            @Override
+            public void onStopTrackingTouch(SeekBar seekBar) {}
+        });
+    }
+
+    private void updateGpuWork(int newGpuWork) {
+        updateGpuWork(newGpuWork, false);
+    }
+
+    private void updateGpuWork(int newGpuWork, boolean fromSeekBar) {
+        gpuWorkText.setText(String.format("GPU Work: %,d", newGpuWork));
+        if (!fromSeekBar) {
+            gpuWorkSeekBar.setProgress(exponentialToLinear(newGpuWork));
+        }
+        nUpdateGpuWorkload(newGpuWork);
+    }
+
+    private void setupCpuWorkSeekBar() {
+      cpuWorkSeekBar = findViewById(R.id.seekBarCpuWork);
+      cpuWorkText = findViewById(R.id.textViewCpuWork);
+
+      cpuWorkSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+          @Override
+          public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+              if (!fromUser) {
+                  return;
+              }
+
+              int newCpuWork = linearToExponential(progress);
+              updateCpuWork(newCpuWork, true);
+          }
+
+          @Override
+          public void onStartTrackingTouch(SeekBar seekBar) {}
+          @Override
+          public void onStopTrackingTouch(SeekBar seekBar) {}
+      });
+    }
+
+    private void updateCpuWork(int newCpuWork) {
+        updateCpuWork(newCpuWork, false);
+    }
+
+    private void updateCpuWork(int newCpuWork, boolean fromSeekBar) {
+        cpuWorkText.setText(String.format("CPU Work: %,d", newCpuWork));
+        if (!fromSeekBar) {
+            cpuWorkSeekBar.setProgress(exponentialToLinear(newCpuWork));
+        }
+        nUpdateCpuWorkload(newCpuWork);
+    }
+
+    static private int linearToExponential(int linearValue) {
+        return linearValue == 0 ? 0 : (int)Math.pow(10, (double)linearValue / 1000.0);
+    }
+
+    static private int exponentialToLinear(int exponentialValue) {
+        return exponentialValue == 0 ? 0 : (int)Math.log10(exponentialValue) * 1000;
+    }
+
+    @Override
+    public void surfaceCreated(SurfaceHolder holder) {
+        Log.d(APP_NAME, "Surface created.");
+        Surface surface = holder.getSurface();
+        nStartCube(surface);
+    }
+
+    @Override
+    public void surfaceDestroyed(SurfaceHolder holder) {
+        Log.d(APP_NAME, "Surface destroyed.");
+        nStopCube();
+    }
+
+    @Override
+    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
+
+    /**
+     * Native methods that are implemented by the 'native-lib' native library,
+     * which is packaged with this application.
+     */
+    public native void nStartCube(Surface holder);
+    public native void nStopCube();
+    public native void nUpdateGpuWorkload(int newWorkload);
+    public native void nUpdateCpuWorkload(int newWorkload);
+}
diff --git a/third_party/cube/app/src/main/res/layout/activity_cube.xml b/third_party/cube/app/src/main/res/layout/activity_cube.xml
new file mode 100644
index 0000000..e44f3ab
--- /dev/null
+++ b/third_party/cube/app/src/main/res/layout/activity_cube.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    tools:context="com.samples.cube.CubeActivity">
+
+    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:app="http://schemas.android.com/apk/res-auto"
+        xmlns:tools="http://schemas.android.com/tools"
+        android:id="@+id/settingsLayout"
+        android:visibility="gone"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+        <TextView
+            android:id="@+id/textViewGpuWork"
+            android:layout_margin="5dp"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
+        <SeekBar
+            android:id="@+id/seekBarGpuWork"
+            android:max="8000"
+            android:progress="0"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
+        <TextView
+            android:id="@+id/textViewCpuWork"
+            android:layout_margin="5dp"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
+        <SeekBar
+            android:id="@+id/seekBarCpuWork"
+            android:max="8000"
+            android:progress="0"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
+    </LinearLayout>
+
+    <SurfaceView
+        android:id="@+id/surface_view"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1" />
+</LinearLayout>