Merge 24d37d9baf695d2b9106f806665819e2b14ebfb2 on remote branch

Change-Id: I90f37a30634dde907794edd70dab2e6aa7ad2b87
diff --git a/.gitignore b/.gitignore
index d5f3eb0..e847034 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,7 @@
 third_party/vulkan-headers
 third_party/vulkan-loader
 third_party/vulkan-validationlayers/
+third_party/robin-hood-hashing
 .vs
 
 *.pyc
@@ -25,5 +26,5 @@
 [._]*.s[a-w][a-z]
 
 # C-Lion
-.idea
-cmake-build-debug
+.idea/
+cmake-build-*/
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9f65d5f..c064649 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -202,7 +202,6 @@
 function(amber_default_compile_options TARGET)
   if (${COMPILER_IS_LIKE_GNU})
     target_compile_options(${TARGET} PRIVATE
-      -std=c++11
       -fno-exceptions
       -fvisibility=hidden
       -Wall
@@ -230,7 +229,6 @@
   endif()
 
   if (MSVC)
-    # Specify /EHs for exception handling.
     target_compile_options(${TARGET} PRIVATE
       /bigobj
       /EHsc
@@ -266,6 +264,6 @@
 add_subdirectory(third_party)
 add_subdirectory(src)
 
-if (${AMBER_ENABLE_SAMPLES} AND NOT ANDROID)
+if (${AMBER_ENABLE_SAMPLES})
   add_subdirectory(samples)
 endif()
diff --git a/DEPS b/DEPS
index ff7507f..1286b6d 100644
--- a/DEPS
+++ b/DEPS
@@ -8,23 +8,25 @@
   'microsoft_git': 'https://github.com/Microsoft',
   'nlohmann_git': 'https://github.com/nlohmann',
   'swiftshader_git': 'https://swiftshader.googlesource.com',
+  'martinus_git': 'https://github.com/martinus',
 
-  'clspv_llvm_revision': '7e30989dabce9ddbca0cbad7a8f25fb4e756d334',
-  'clspv_revision': 'e0406e7053d1bb46b4bbeb57f0f2bbfca32f5612',
-  'cppdap_revision': '1fd23dda91e01550be1a421de307e6fedb2035a9',
+  'clspv_llvm_revision': 'b70366c9c430e1eadd59d5a1dfbb9c4d84f83de5',
+  'clspv_revision': 'f99809bdab1710846633b4ec24f5448263e75da7',
+  'cppdap_revision': '88e89520148b2f95e17ca9348587a28215ffc921',
   'cpplint_revision': '26470f9ccb354ff2f6d098f831271a1833701b28',
-  'dxc_revision': '489c2e4d32417cd6693db5673ab071d82e1f5974',
-  'glslang_revision': '7f6559d2802d0653541060f0909e33d137b9c8ba',
-  'googletest_revision': '0555b0eacbc56df1fd762c6aa87bb84be9e4ce7e',
-  'json_revision': '350ff4f7ced7c4117eae2fb93df02823c8021fcb',
-  'lodepng_revision': '7fdcc96a5e5864eee72911c3ca79b1d9f0d12292',
-  'shaderc_revision': '88f9156d7f6a2a30baed1ace196faa3bc5eccc05',
-  'spirv_headers_revision': '5ab5c96198f30804a6a29961b8905f292a8ae600',
-  'spirv_tools_revision': '1f2fcddd3963b9c29bf360daf7656c5977c2aadd',
-  'swiftshader_revision': '04515da400d5fbc22d852af1369c4d46bd54991e',
-  'vulkan_headers_revision': '11c6670b4a4f766ed4f1e777d1b3c3dc082dfa5f',
-  'vulkan_loader_revision': 'be6ccb9ecaf77dfef59246a1e8502e99c8e1a511',
-  'vulkan_validationlayers_revision': '0cb8cc8cfcb2b86a767c9516ac2d62edb4e38ebe',
+  'dxc_revision': 'c45db48d565a9edc14b025e43b90e62264d06eea',
+  'glslang_revision': '81cc10a498b25a90147cccd6e8939493c1e9e20e',
+  'googletest_revision': '16f637fbf4ffc3f7a01fa4eceb7906634565242f',
+  'json_revision': '4f8fba14066156b73f1189a2b8bd568bde5284c5',
+  'lodepng_revision': '5601b8272a6850b7c5d693dd0c0e16da50be8d8d',
+  'shaderc_revision': 'e72186b66bb90ed06aaf15cbdc9a053581a0616b',
+  'spirv_headers_revision': 'b42ba6d92faf6b4938e6f22ddd186dbdacc98d78',
+  'spirv_tools_revision': 'a73e724359a274d7cf4f4248eba5be1e7764fbfd',
+  'swiftshader_revision': 'bca23447ad4667a7b79973569ab5d8d905d211ac',
+  'vulkan_headers_revision': '1dace16d8044758d32736eb59802d171970e9448',
+  'vulkan_loader_revision': '8aad559a09388ceb5b968af64a2b965d3886e5a0',
+  'vulkan_validationlayers_revision': 'a6c1ddca49331d8addde052554487180ee8aec13',
+  'robin_hood_hashing_revision': '24b3f50f9532153edc23b29ae277dcccfd75a462',
 }
 
 deps = {
@@ -75,4 +77,7 @@
 
   'third_party/vulkan-loader': Var('khronos_git') + '/Vulkan-Loader.git@' +
       Var('vulkan_loader_revision'),
+
+  'third_party/robin-hood-hashing': Var('martinus_git') + '/robin-hood-hashing.git@' +
+      Var('robin_hood_hashing_revision'),
 }
diff --git a/android_gradle/app/build.gradle b/android_gradle/app/build.gradle
index 40c4c54..b756f6b 100644
--- a/android_gradle/app/build.gradle
+++ b/android_gradle/app/build.gradle
@@ -1,34 +1,49 @@
-apply plugin: 'com.android.application'
+plugins {
+    id 'com.android.application'
+}
 
 android {
-    compileSdkVersion 29
-    buildToolsVersion "29.0.2"
+    compileSdk 30
+    buildToolsVersion "30.0.2"
+    ndkVersion "21.4.7075529"
 
     defaultConfig {
         applicationId "com.google.amber"
         minSdkVersion 24
-        targetSdkVersion 29
+        targetSdkVersion 30
         versionCode 1
         versionName "1.0"
 
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+        externalNativeBuild {
+            cmake {
+                arguments "-DAMBER_USE_LOCAL_VULKAN=1"
+                targets "amber_ndk"
+            }
+        }
+    }
+
+    externalNativeBuild {
+        cmake {
+            path "../../CMakeLists.txt"
+        }
     }
 
     sourceSets {
         androidTest.manifest.srcFile "src/androidTest/AndroidManifest.xml"
     }
 
-    dependencies {
-        androidTestImplementation 'androidx.test:runner:1.1.0'
-        androidTestImplementation 'androidx.test:rules:1.1.0'
-    }
-
     buildTypes {
         release {
             minifyEnabled false
             proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
         }
     }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
 
     sourceSets {
         main {
@@ -41,9 +56,10 @@
 dependencies {
     implementation fileTree(dir: 'libs', include: ['*.jar'])
 
-    implementation 'androidx.appcompat:appcompat:1.1.0'
-    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
-    testImplementation 'junit:junit:4.12'
-    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
-    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+    implementation 'androidx.appcompat:appcompat:1.3.1'
+    implementation 'com.google.android.material:material:1.4.0'
+    implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
+    testImplementation 'junit:junit:4.13.2'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
 }
diff --git a/android_gradle/app/src/androidTest/java/com/google/amber/AmberLauncher.java b/android_gradle/app/src/androidTest/java/com/google/amber/AmberLauncher.java
index 4864843..ad92154 100644
--- a/android_gradle/app/src/androidTest/java/com/google/amber/AmberLauncher.java
+++ b/android_gradle/app/src/androidTest/java/com/google/amber/AmberLauncher.java
@@ -54,7 +54,7 @@
     String stdout_file = args.getString("stdout", new File(outputDir, "amber_stdout.txt").toString());
     String stderr_file = args.getString("stderr", new File(outputDir, "amber_stderr.txt").toString());
 
-    int res = Amber.androidMain(args_list.toArray(new String[0]), stdout_file, stderr_file);
+    int res = Amber.androidHelper(args_list.toArray(new String[0]), stdout_file, stderr_file);
 
     // If the process crashes during the above call or we call System.exit below, the output
     // from `adb shell am instrument ...` will be:
diff --git a/android_gradle/app/src/main/AndroidManifest.xml b/android_gradle/app/src/main/AndroidManifest.xml
index 0ab9856..c74c390 100644
--- a/android_gradle/app/src/main/AndroidManifest.xml
+++ b/android_gradle/app/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
 
     <application
         android:allowBackup="true"
+        android:fullBackupOnly="true"
         android:label="Amber"
         android:supportsRtl="true">
         <meta-data
diff --git a/android_gradle/app/src/main/java/com/google/amber/Amber.java b/android_gradle/app/src/main/java/com/google/amber/Amber.java
index 8319ccf..008c528 100644
--- a/android_gradle/app/src/main/java/com/google/amber/Amber.java
+++ b/android_gradle/app/src/main/java/com/google/amber/Amber.java
@@ -19,5 +19,5 @@
     System.loadLibrary("amber_ndk");
   }
 
-  public static native int androidMain(String[] args, String stdout_file, String stderr_file);
+  public static native int androidHelper(String[] args, String stdout_file, String stderr_file);
 }
diff --git a/android_gradle/build.gradle b/android_gradle/build.gradle
index 82c93bf..d340082 100644
--- a/android_gradle/build.gradle
+++ b/android_gradle/build.gradle
@@ -4,25 +4,16 @@
 
     repositories {
         google()
-        jcenter()
-        maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }
+        mavenCentral()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:4.0.0-alpha04'
+        classpath 'com.android.tools.build:gradle:7.0.2'
 
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
     }
 }
 
-allprojects {
-    repositories {
-        google()
-        jcenter()
-        maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' }
-    }
-}
-
 task clean(type: Delete) {
     delete rootProject.buildDir
 }
diff --git a/android_gradle/gradle.properties b/android_gradle/gradle.properties
index 199d16e..4941ecd 100644
--- a/android_gradle/gradle.properties
+++ b/android_gradle/gradle.properties
@@ -6,7 +6,7 @@
 # http://www.gradle.org/docs/current/userguide/build_environment.html
 # Specifies the JVM arguments used for the daemon process.
 # The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs=-Xmx1536m
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
 # When configured, Gradle will run in incubating parallel mode.
 # This option should only be used with decoupled projects. More details, visit
 # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
diff --git a/android_gradle/gradle/wrapper/gradle-wrapper.jar b/android_gradle/gradle/wrapper/gradle-wrapper.jar
index f6b961f..e708b1c 100644
--- a/android_gradle/gradle/wrapper/gradle-wrapper.jar
+++ b/android_gradle/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/android_gradle/gradle/wrapper/gradle-wrapper.properties b/android_gradle/gradle/wrapper/gradle-wrapper.properties
index 421d5b6..e81b594 100644
--- a/android_gradle/gradle/wrapper/gradle-wrapper.properties
+++ b/android_gradle/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Mon Dec 09 14:37:37 GMT 2019
+#Sat Sep 25 16:43:12 BST 2021
 distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
 distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.0-rc-1-all.zip
+zipStoreBase=GRADLE_USER_HOME
diff --git a/android_gradle/settings.gradle b/android_gradle/settings.gradle
index 9a979bb..2c4357f 100644
--- a/android_gradle/settings.gradle
+++ b/android_gradle/settings.gradle
@@ -1,2 +1,9 @@
+dependencyResolutionManagement {
+    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
 rootProject.name='Amber'
 include ':app'
diff --git a/docs/amber_script.md b/docs/amber_script.md
index 295cdc7..7c78d82 100644
--- a/docs/amber_script.md
+++ b/docs/amber_script.md
@@ -48,6 +48,20 @@
  * `Storage16BitFeatures.storageInputOutput16`
  * `SubgroupSizeControl.subgroupSizeControl`
  * `SubgroupSizeControl.computeFullSubgroups`
+ * `SubgroupSupportedOperations.basic`
+ * `SubgroupSupportedOperations.vote`
+ * `SubgroupSupportedOperations.arithmetic`
+ * `SubgroupSupportedOperations.ballot`
+ * `SubgroupSupportedOperations.shuffle`
+ * `SubgroupSupportedOperations.shuffleRelative`
+ * `SubgroupSupportedOperations.clustered`
+ * `SubgroupSupportedOperations.quad`
+ * `SubgroupSupportedStages.vertex`
+ * `SubgroupSupportedStages.tessellationControl`
+ * `SubgroupSupportedStages.tessellationEvaluation`
+ * `SubgroupSupportedStages.geometry`
+ * `SubgroupSupportedStages.fragment`
+ * `SubgroupSupportedStages.compute`
 
 
 Extensions can be enabled with the `DEVICE_EXTENSION` and `INSTANCE_EXTENSION`
@@ -427,6 +441,11 @@
   POLYGON_MODE {mode}
 ```
 
+```groovy
+  # Set the number of patch control points used by tessellation. The default value is 3.
+  PATCH_CONTROL_POINTS {control_points}
+```
+
 #### Compare operations
  * `never`
  * `less`
@@ -478,6 +497,96 @@
   END
 ```
 
+#### Blend factors
+* `zero`
+* `one`
+* `src_color`
+* `one_minus_src_color`
+* `dst_color`
+* `one_minus_dst_color`
+* `src_alpha`
+* `one_minus_src_alpha`
+* `dst_alpha`
+* `one_minus_dst_alpha`
+* `constant_color`
+* `one_minus_constant_color`
+* `constant_alpha`
+* `one_minus_constant_alpha`
+* `src_alpha_saturate`
+* `src1_color`
+* `one_minus_src1_color`
+* `src1_alpha`
+* `one_minus_src1_alpha`
+
+#### Blend operations
+* `add`
+* `substract`
+* `reverse_substract`
+* `min`
+* `max`
+
+The following operations also require VK_EXT_blend_operation_advanced
+when using a Vulkan backend.
+* `zero`
+* `src`
+* `dst`
+* `src_over`
+* `dst_over`
+* `src_in`
+* `dst_in`
+* `src_out`
+* `dst_out`
+* `src_atop`
+* `dst_atop`
+* `xor`
+* `multiply`
+* `screen`
+* `overlay`
+* `darken`
+* `lighten`
+* `color_dodge`
+* `color_burn`
+* `hard_light`
+* `soft_light`
+* `difference`
+* `exclusion`
+* `invert`
+* `invert_rgb`
+* `linear_dodge`
+* `linear_burn`
+* `vivid_light`
+* `linear_light`
+* `pin_light`
+* `hard_mix`
+* `hsl_hue`
+* `hsl_saturation`
+* `hsl_color`
+* `hsl_luminosity`
+* `plus`
+* `plus_clamped`
+* `plus_clamped_alpha`
+* `plus_darker`
+* `minus`
+* `minus_clamped`
+* `contrast`
+* `invert_org`
+* `red`
+* `green`
+* `blue`
+
+```groovy
+  # Enable alpha blending and set blend factors and operations. Available
+  # blend factors and operations are listed above.
+  BLEND
+    SRC_COLOR_FACTOR {src_color_factor}
+    DST_COLOR_FACTOR {dst_color_factor}
+    COLOR_OP {color_op}
+    SRC_ALPHA_FACTOR {src_alpha_factor}
+    DST_ALPHA_FACTOR {dst_alpha_factor}
+    ALPHA_OP {alpha_op}
+  END
+```
+
 ```groovy
   # Set the size of the render buffers. |width| and |height| are integers and
   # default to 250x250.
@@ -536,6 +645,11 @@
   # pipelines.
   BIND BUFFER {buffer_name} AS depth_stencil
 
+  # Attach |buffer_name| as a multisample resolve target. The order of resolve
+  # target images match with the order of color attachments that have more than
+  # one sample.
+  BIND BUFFER {buffer_name} AS resolve
+
   # Attach |buffer_name| as the push_constant buffer. There can be only one
   # push constant buffer attached to a pipeline.
   BIND BUFFER {buffer_name} AS push_constant
diff --git a/docs/debugger.md b/docs/debugger.md
index 6c3208b..dc887d0 100644
--- a/docs/debugger.md
+++ b/docs/debugger.md
@@ -3,9 +3,7 @@
 This document describes Amber's shader debugger testing framework, which allows developers to write tests for Vulkan drivers that expose shader debugging functionality via the [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/).
 
 ---
-**Work In Progress**
-
-Note that shader debugging is very much work-in-progress:
+**Caveats**
 
 * Vulkan shader debugging currently does not have a formalized specification. A shader debugger implementation is being developed in [SwiftShader](https://swiftshader.googlesource.com/SwiftShader/), which one day may become a reference implementation for a formal specifiction.
 * Currently SwiftShader is the only Vulkan driver to implement a [DAP based shader debugger](https://swiftshader.googlesource.com/SwiftShader/+/refs/heads/master/docs/VulkanShaderDebugging.md). This implementation is also work-in-progress, and may significantly change.
@@ -189,4 +187,4 @@
 * If the `amber::Command` holds a `amber::debug::Script`, then this script is executed on the `amber::Engine::Debugger` using the `amber::debug::Script::Run(amber::debug::Events *)` method, before the Vulkan command is executed.
 * The command is then executed with `amber::Executor::ExecuteCommand()`
 * Once the command has completed, all debugger threads are synchronized and debugger test results are collected with a call to `amber::Engine::Debugger::Flush()`.
-* This process is repeated for all commands in the script.
\ No newline at end of file
+* This process is repeated for all commands in the script.
diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt
index 3fa1882..1e91c4d 100644
--- a/samples/CMakeLists.txt
+++ b/samples/CMakeLists.txt
@@ -74,3 +74,11 @@
 target_link_libraries(image_diff libamber "lodepng")
 amber_default_compile_options(image_diff)
 set_target_properties(image_diff PROPERTIES OUTPUT_NAME "image_diff")
+
+if (ANDROID)
+  add_library(amber_ndk SHARED android_helper.cc ${AMBER_SOURCES})
+  target_include_directories(amber_ndk PRIVATE "${CMAKE_BINARY_DIR}")
+  target_link_libraries(amber_ndk libamber ${AMBER_EXTRA_LIBS})
+  amber_default_compile_options(amber_ndk)
+  target_compile_definitions(amber_ndk PRIVATE AMBER_ANDROID_MAIN=1)
+endif()
diff --git a/samples/amber.cc b/samples/amber.cc
index d037ba4..5973dba 100644
--- a/samples/amber.cc
+++ b/samples/amber.cc
@@ -447,7 +447,15 @@
 
 }  // namespace
 
+#ifdef AMBER_ANDROID_MAIN
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wmissing-prototypes"
+#pragma ide diagnostic ignored "OCUnusedGlobalDeclarationInspection"
+int android_main(int argc, const char** argv) {
+#pragma clang diagnostic pop
+#else
 int main(int argc, const char** argv) {
+#endif
   std::vector<std::string> args(argv, argv + argc);
   Options options;
   SampleDelegate delegate;
diff --git a/samples/android_main.cc b/samples/android_helper.cc
similarity index 79%
rename from samples/android_main.cc
rename to samples/android_helper.cc
index b24a9bb..7da69ad 100644
--- a/samples/android_main.cc
+++ b/samples/android_helper.cc
@@ -18,16 +18,20 @@
 #include <string>
 #include <vector>
 
-extern int main(int argc, const char** argv);
+extern int android_main(int argc, const char** argv);
 
-extern "C" JNIEXPORT JNICALL int Java_com_google_amber_Amber_androidMain(
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wmissing-prototypes"
+#pragma ide diagnostic ignored "OCUnusedGlobalDeclarationInspection"
+
+extern "C" JNIEXPORT JNICALL int Java_com_google_amber_Amber_androidHelper(
     JNIEnv* env,
     jobject,
     jobjectArray args,
     jstring stdoutFile,
     jstring stderrFile) {
-  const char* stdout_file_cstr = env->GetStringUTFChars(stdoutFile, NULL);
-  const char* stderr_file_cstr = env->GetStringUTFChars(stderrFile, NULL);
+  const char* stdout_file_cstr = env->GetStringUTFChars(stdoutFile, nullptr);
+  const char* stderr_file_cstr = env->GetStringUTFChars(stderrFile, nullptr);
 
   // Redirect std output to a file
   freopen(stdout_file_cstr, "w", stdout);
@@ -43,7 +47,7 @@
 
   for (jsize i = 0; i < arg_count; i++) {
     jstring js = static_cast<jstring>(env->GetObjectArrayElement(args, i));
-    const char* arg_cstr = env->GetStringUTFChars(js, NULL);
+    const char* arg_cstr = env->GetStringUTFChars(js, nullptr);
     argv_string.push_back(arg_cstr);
     env->ReleaseStringUTFChars(js, arg_cstr);
   }
@@ -53,5 +57,7 @@
   for (const std::string& arg : argv_string)
     argv.push_back(arg.c_str());
 
-  return main(argv.size(), argv.data());
+  return android_main(static_cast<int>(argv.size()), argv.data());
 }
+
+#pragma clang diagnostic pop
diff --git a/samples/config_helper_vulkan.cc b/samples/config_helper_vulkan.cc
index 68114ab..6a82a4a 100644
--- a/samples/config_helper_vulkan.cc
+++ b/samples/config_helper_vulkan.cc
@@ -758,25 +758,60 @@
     const VkPhysicalDevice physical_device,
     const std::vector<std::string>& required_features,
     const std::vector<std::string>& required_extensions) {
+  available_device_extensions_ = GetAvailableDeviceExtensions(physical_device);
+  if (!AreAllExtensionsSupported(available_device_extensions_,
+                                 required_extensions)) {
+    return amber::Result("Device does not support all required extensions");
+  }
+  for (const auto& ext : available_device_extensions_) {
+    if (ext == "VK_KHR_shader_float16_int8")
+      supports_shader_float16_int8_ = true;
+    else if (ext == "VK_KHR_8bit_storage")
+      supports_shader_8bit_storage_ = true;
+    else if (ext == "VK_KHR_16bit_storage")
+      supports_shader_16bit_storage_ = true;
+    else if (ext == "VK_EXT_subgroup_size_control")
+      supports_subgroup_size_control_ = true;
+  }
+
   VkPhysicalDeviceFeatures required_vulkan_features =
       VkPhysicalDeviceFeatures();
 
   if (supports_get_physical_device_properties2_) {
-    VkPhysicalDeviceSubgroupSizeControlFeaturesEXT size_control =
-        VkPhysicalDeviceSubgroupSizeControlFeaturesEXT();
-    size_control.sType =
-        VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_FEATURES_EXT;
-    size_control.pNext = nullptr;
+    VkPhysicalDeviceSubgroupSizeControlFeaturesEXT
+        subgroup_size_control_features = {};
+    VkPhysicalDeviceVariablePointerFeaturesKHR variable_pointers_features = {};
+    VkPhysicalDeviceFloat16Int8FeaturesKHR float16_int8_features = {};
+    VkPhysicalDevice8BitStorageFeaturesKHR storage_8bit_features = {};
+    VkPhysicalDevice16BitStorageFeaturesKHR storage_16bit_features = {};
 
-    VkPhysicalDeviceVariablePointerFeaturesKHR var_ptrs =
-        VkPhysicalDeviceVariablePointerFeaturesKHR();
-    var_ptrs.sType =
+    subgroup_size_control_features.sType =
+        VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_FEATURES_EXT;
+    subgroup_size_control_features.pNext = nullptr;
+
+    // Add subgroup size control struct into the chain only if
+    // VK_EXT_subgroup_size_control is supported.
+    variable_pointers_features.sType =
         VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VARIABLE_POINTER_FEATURES_KHR;
-    var_ptrs.pNext = &size_control;
+    variable_pointers_features.pNext = supports_subgroup_size_control_
+                                           ? &subgroup_size_control_features
+                                           : nullptr;
+
+    float16_int8_features.sType =
+        VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FLOAT16_INT8_FEATURES_KHR;
+    float16_int8_features.pNext = &variable_pointers_features;
+
+    storage_8bit_features.sType =
+        VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_8BIT_STORAGE_FEATURES_KHR;
+    storage_8bit_features.pNext = &float16_int8_features;
+
+    storage_16bit_features.sType =
+        VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_16BIT_STORAGE_FEATURES_KHR;
+    storage_16bit_features.pNext = &storage_8bit_features;
 
     VkPhysicalDeviceFeatures2KHR features2 = VkPhysicalDeviceFeatures2KHR();
     features2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2_KHR;
-    features2.pNext = &var_ptrs;
+    features2.pNext = &storage_16bit_features;
 
     auto vkGetPhysicalDeviceFeatures2KHR =
         reinterpret_cast<PFN_vkGetPhysicalDeviceFeatures2KHR>(
@@ -794,13 +829,34 @@
       }
 
       if ((feature == kVariablePointers &&
-           var_ptrs.variablePointers == VK_FALSE) ||
+           variable_pointers_features.variablePointers == VK_FALSE) ||
           (feature == kVariablePointersStorageBuffer &&
-           var_ptrs.variablePointersStorageBuffer == VK_FALSE) ||
+           variable_pointers_features.variablePointersStorageBuffer ==
+               VK_FALSE) ||
           (feature == kSubgroupSizeControl &&
-           size_control.subgroupSizeControl == VK_FALSE) ||
+           subgroup_size_control_features.subgroupSizeControl == VK_FALSE) ||
           (feature == kComputeFullSubgroups &&
-           size_control.computeFullSubgroups == VK_FALSE)) {
+           subgroup_size_control_features.computeFullSubgroups == VK_FALSE) ||
+          (feature == kFloat16Int8_Float16 &&
+           float16_int8_features.shaderFloat16 == VK_FALSE) ||
+          (feature == kFloat16Int8_Int8 &&
+           float16_int8_features.shaderInt8 == VK_FALSE) ||
+          (feature == k8BitStorage_Storage &&
+           storage_8bit_features.storageBuffer8BitAccess == VK_FALSE) ||
+          (feature == k8BitStorage_UniformAndStorage &&
+           storage_8bit_features.uniformAndStorageBuffer8BitAccess ==
+               VK_FALSE) ||
+          (feature == k8BitStorage_PushConstant &&
+           storage_8bit_features.storagePushConstant8 == VK_FALSE) ||
+          (feature == k16BitStorage_Storage &&
+           storage_16bit_features.storageBuffer16BitAccess == VK_FALSE) ||
+          (feature == k16BitStorage_InputOutput &&
+           storage_16bit_features.storageInputOutput16 == VK_FALSE) ||
+          (feature == k16BitStorage_PushConstant &&
+           storage_16bit_features.storagePushConstant16 == VK_FALSE) ||
+          (feature == k16BitStorage_UniformAndStorage &&
+           storage_16bit_features.uniformAndStorageBuffer16BitAccess ==
+               VK_FALSE)) {
         return amber::Result("Device does not support all required features");
       }
     }
@@ -823,22 +879,6 @@
     return amber::Result("Device does not support all required features");
   }
 
-  available_device_extensions_ = GetAvailableDeviceExtensions(physical_device);
-  if (!AreAllExtensionsSupported(available_device_extensions_,
-                                 required_extensions)) {
-    return amber::Result("Device does not support all required extensions");
-  }
-  for (const auto& ext : available_device_extensions_) {
-    if (ext == "VK_KHR_shader_float16_int8")
-      supports_shader_float16_int8_ = true;
-    else if (ext == "VK_KHR_8bit_storage")
-      supports_shader_8bit_storage_ = true;
-    else if (ext == "VK_KHR_16bit_storage")
-      supports_shader_16bit_storage_ = true;
-    else if (ext == "VK_EXT_subgroup_size_control")
-      supports_subgroup_size_control_ = true;
-  }
-
   vulkan_queue_family_index_ = ChooseQueueFamilyIndex(physical_device);
   if (vulkan_queue_family_index_ == std::numeric_limits<uint32_t>::max()) {
     return amber::Result("Device does not support required queue flags");
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 901681f..e51d1b0 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -124,6 +124,10 @@
 if (${Vulkan_FOUND})
   target_link_libraries(libamber libamberenginevulkan)
   target_include_directories(libamber PRIVATE "${VulkanHeaders_INCLUDE_DIR}")
+
+  if (${VULKAN_CTS_HEADER} AND DEFINED AMBER_CTS_INL_DIR)
+    target_include_directories(libamber PRIVATE "${AMBER_CTS_INL_DIR}")
+  endif()
 endif()
 if (${Dawn_FOUND})
   target_link_libraries(libamber libamberenginedawn)
@@ -156,6 +160,7 @@
     amberscript/parser_shader_opt_test.cc
     amberscript/parser_shader_test.cc
     amberscript/parser_stencil_test.cc
+    amberscript/parser_blend_test.cc
     amberscript/parser_struct_test.cc
     amberscript/parser_subgroup_size_control_test.cc
     amberscript/parser_test.cc
diff --git a/src/amberscript/parser.cc b/src/amberscript/parser.cc
index 809baa1..d3e0eb6 100644
--- a/src/amberscript/parser.cc
+++ b/src/amberscript/parser.cc
@@ -622,6 +622,10 @@
       r = ParsePipelineStencil(pipeline.get());
     } else if (tok == "SUBGROUP") {
       r = ParsePipelineSubgroup(pipeline.get());
+    } else if (tok == "PATCH_CONTROL_POINTS") {
+      r = ParsePipelinePatchControlPoints(pipeline.get());
+    } else if (tok == "BLEND") {
+      r = ParsePipelineBlend(pipeline.get());
     } else {
       r = Result("unknown token in pipeline block: " + tok);
     }
@@ -931,6 +935,20 @@
   return ValidateEndOfStatement("SUBGROUP command");
 }
 
+Result Parser::ParsePipelinePatchControlPoints(Pipeline* pipeline) {
+  auto token = tokenizer_->NextToken();
+  if (token->IsEOL() || token->IsEOS())
+    return Result(
+        "missing number of control points in PATCH_CONTROL_POINTS command");
+
+  if (!token->IsInteger())
+    return Result("expecting integer for the number of control points");
+
+  pipeline->GetPipelineData()->SetPatchControlPoints(token->AsUint32());
+
+  return ValidateEndOfStatement("PATCH_CONTROL_POINTS command");
+}
+
 Result Parser::ParsePipelineFramebufferSize(Pipeline* pipeline) {
   auto token = tokenizer_->NextToken();
   if (token->IsEOL() || token->IsEOS())
@@ -1046,6 +1064,8 @@
     *type = BufferType::kUniformTexelBuffer;
   else if (name == "storage_texel_buffer")
     *type = BufferType::kStorageTexelBuffer;
+  else if (name == "resolve")
+    *type = BufferType::kResolve;
   else
     return Result("unknown buffer_type: " + name);
 
@@ -1160,6 +1180,8 @@
 
         for (auto& buf : buffers)
           buf->SetSampler(sampler);
+      } else if (buffer_type == BufferType::kResolve) {
+        r = pipeline->AddResolveTarget(buffer);
       }
     }
 
@@ -1843,6 +1865,98 @@
   return ValidateEndOfStatement("STENCIL command");
 }
 
+Result Parser::ParsePipelineBlend(Pipeline* pipeline) {
+  pipeline->GetPipelineData()->SetEnableBlend(true);
+
+  while (true) {
+    auto token = tokenizer_->NextToken();
+    if (token->IsEOL())
+      continue;
+    if (token->IsEOS())
+      return Result("BLEND missing END command");
+    if (!token->IsIdentifier())
+      return Result("BLEND options must be identifiers");
+    if (token->AsString() == "END")
+      break;
+
+    if (token->AsString() == "SRC_COLOR_FACTOR") {
+      token = tokenizer_->NextToken();
+
+      if (!token->IsIdentifier())
+        return Result("BLEND invalid value for SRC_COLOR_FACTOR");
+
+      const auto factor = NameToBlendFactor(token->AsString());
+      if (factor == BlendFactor::kUnknown)
+        return Result("BLEND invalid value for SRC_COLOR_FACTOR: " +
+                      token->AsString());
+      pipeline->GetPipelineData()->SetSrcColorBlendFactor(
+          NameToBlendFactor(token->AsString()));
+    } else if (token->AsString() == "DST_COLOR_FACTOR") {
+      token = tokenizer_->NextToken();
+
+      if (!token->IsIdentifier())
+        return Result("BLEND invalid value for DST_COLOR_FACTOR");
+
+      const auto factor = NameToBlendFactor(token->AsString());
+      if (factor == BlendFactor::kUnknown)
+        return Result("BLEND invalid value for DST_COLOR_FACTOR: " +
+                      token->AsString());
+      pipeline->GetPipelineData()->SetDstColorBlendFactor(
+          NameToBlendFactor(token->AsString()));
+    } else if (token->AsString() == "SRC_ALPHA_FACTOR") {
+      token = tokenizer_->NextToken();
+
+      if (!token->IsIdentifier())
+        return Result("BLEND invalid value for SRC_ALPHA_FACTOR");
+
+      const auto factor = NameToBlendFactor(token->AsString());
+      if (factor == BlendFactor::kUnknown)
+        return Result("BLEND invalid value for SRC_ALPHA_FACTOR: " +
+                      token->AsString());
+      pipeline->GetPipelineData()->SetSrcAlphaBlendFactor(
+          NameToBlendFactor(token->AsString()));
+    } else if (token->AsString() == "DST_ALPHA_FACTOR") {
+      token = tokenizer_->NextToken();
+
+      if (!token->IsIdentifier())
+        return Result("BLEND invalid value for DST_ALPHA_FACTOR");
+
+      const auto factor = NameToBlendFactor(token->AsString());
+      if (factor == BlendFactor::kUnknown)
+        return Result("BLEND invalid value for DST_ALPHA_FACTOR: " +
+                      token->AsString());
+      pipeline->GetPipelineData()->SetDstAlphaBlendFactor(
+          NameToBlendFactor(token->AsString()));
+    } else if (token->AsString() == "COLOR_OP") {
+      token = tokenizer_->NextToken();
+
+      if (!token->IsIdentifier())
+        return Result("BLEND invalid value for COLOR_OP");
+
+      const auto op = NameToBlendOp(token->AsString());
+      if (op == BlendOp::kUnknown)
+        return Result("BLEND invalid value for COLOR_OP: " + token->AsString());
+      pipeline->GetPipelineData()->SetColorBlendOp(
+          NameToBlendOp(token->AsString()));
+    } else if (token->AsString() == "ALPHA_OP") {
+      token = tokenizer_->NextToken();
+
+      if (!token->IsIdentifier())
+        return Result("BLEND invalid value for ALPHA_OP");
+
+      const auto op = NameToBlendOp(token->AsString());
+      if (op == BlendOp::kUnknown)
+        return Result("BLEND invalid value for ALPHA_OP: " + token->AsString());
+      pipeline->GetPipelineData()->SetAlphaBlendOp(
+          NameToBlendOp(token->AsString()));
+    } else {
+      return Result("BLEND invalid value for BLEND: " + token->AsString());
+    }
+  }
+
+  return ValidateEndOfStatement("BLEND command");
+}
+
 Result Parser::ParseStruct() {
   auto token = tokenizer_->NextToken();
   if (!token->IsIdentifier())
diff --git a/src/amberscript/parser.h b/src/amberscript/parser.h
index 6fb25ce..25d3493 100644
--- a/src/amberscript/parser.h
+++ b/src/amberscript/parser.h
@@ -64,6 +64,7 @@
   Result ParsePipelineShaderOptimizations(Pipeline*);
   Result ParsePipelineShaderCompileOptions(Pipeline*);
   Result ParsePipelineSubgroup(Pipeline* pipeline);
+  Result ParsePipelinePatchControlPoints(Pipeline* pipeline);
   Result ParsePipelineFramebufferSize(Pipeline*);
   Result ParsePipelineViewport(Pipeline*);
   Result ParsePipelineBind(Pipeline*);
@@ -73,6 +74,7 @@
   Result ParsePipelinePolygonMode(Pipeline*);
   Result ParsePipelineDepth(Pipeline* pipeline);
   Result ParsePipelineStencil(Pipeline* pipeline);
+  Result ParsePipelineBlend(Pipeline* pipeline);
   Result ParseRun();
   Result ParseDebug();
   Result ParseDebugThread(debug::Events*, Pipeline* pipeline);
diff --git a/src/amberscript/parser_bind_test.cc b/src/amberscript/parser_bind_test.cc
index a39b69e..8aac5e5 100644
--- a/src/amberscript/parser_bind_test.cc
+++ b/src/amberscript/parser_bind_test.cc
@@ -3508,5 +3508,132 @@
             r.Error());
 }
 
+TEST_F(AmberScriptParserTest, BindResolveTarget) {
+  std::string in = R"(
+SHADER vertex my_shader PASSTHROUGH
+SHADER fragment my_fragment GLSL
+# GLSL Shader
+END
+IMAGE my_fb_ms DIM_2D WIDTH 64 HEIGHT 64 FORMAT R32G32B32A32_SFLOAT SAMPLES 4
+IMAGE my_fb DIM_2D WIDTH 64 HEIGHT 64 FORMAT R32G32B32A32_SFLOAT
+
+PIPELINE graphics my_pipeline
+  ATTACH my_shader
+  ATTACH my_fragment
+
+  FRAMEBUFFER_SIZE 64 64
+  BIND BUFFER my_fb_ms AS color LOCATION 0
+  BIND BUFFER my_fb AS resolve
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto script = parser.GetScript();
+  const auto& pipelines = script->GetPipelines();
+  ASSERT_EQ(1U, pipelines.size());
+
+  const auto* pipeline = pipelines[0].get();
+  const auto& resolve_targets = pipeline->GetResolveTargets();
+  ASSERT_EQ(1U, resolve_targets.size());
+
+  const auto& buf_info = resolve_targets[0];
+  ASSERT_TRUE(buf_info.buffer != nullptr);
+  EXPECT_EQ(64u * 64u, buf_info.buffer->ElementCount());
+  EXPECT_EQ(64u * 64u * 4u, buf_info.buffer->ValueCount());
+  EXPECT_EQ(64u * 64u * 4u * sizeof(float), buf_info.buffer->GetSizeInBytes());
+}
+
+TEST_F(AmberScriptParserTest, BindResolveTargetMissingBuffer) {
+  std::string in = R"(
+SHADER vertex my_shader PASSTHROUGH
+SHADER fragment my_fragment GLSL
+# GLSL Shader
+END
+BUFFER my_fb FORMAT R32G32B32A32_SFLOAT
+
+PIPELINE graphics my_pipeline
+  ATTACH my_shader
+  ATTACH my_fragment
+
+  BIND BUFFER AS resolve
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("12: unknown buffer: AS", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, BindResolveTargetNonDeclaredBuffer) {
+  std::string in = R"(
+SHADER vertex my_shader PASSTHROUGH
+SHADER fragment my_fragment GLSL
+# GLSL Shader
+END
+IMAGE my_fb_ms DIM_2D WIDTH 64 HEIGHT 64 FORMAT R32G32B32A32_SFLOAT SAMPLES 4
+
+PIPELINE graphics my_pipeline
+ATTACH my_shader
+ATTACH my_fragment
+
+FRAMEBUFFER_SIZE 64 64
+BIND BUFFER my_fb_ms AS color LOCATION 0
+BIND BUFFER my_fb AS resolve
+END)";
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("14: unknown buffer: my_fb", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, BindMultipleResolveTargets) {
+  std::string in = R"(
+SHADER vertex my_shader PASSTHROUGH
+SHADER fragment my_fragment GLSL
+# GLSL Shader
+END
+IMAGE my_fb_ms0 DIM_2D WIDTH 64 HEIGHT 64 FORMAT R32G32B32A32_SFLOAT SAMPLES 4
+IMAGE my_fb_ms1 DIM_2D WIDTH 64 HEIGHT 64 FORMAT R32G32B32A32_SFLOAT SAMPLES 4
+IMAGE my_fb_ms2 DIM_2D WIDTH 64 HEIGHT 64 FORMAT R32G32B32A32_SFLOAT SAMPLES 4
+IMAGE my_fb0 DIM_2D WIDTH 64 HEIGHT 64 FORMAT R32G32B32A32_SFLOAT
+IMAGE my_fb1 DIM_2D WIDTH 64 HEIGHT 64 FORMAT R32G32B32A32_SFLOAT
+IMAGE my_fb2 DIM_2D WIDTH 64 HEIGHT 64 FORMAT R32G32B32A32_SFLOAT
+
+PIPELINE graphics my_pipeline
+  ATTACH my_shader
+  ATTACH my_fragment
+
+  FRAMEBUFFER_SIZE 64 64
+  BIND BUFFER my_fb_ms0 AS color LOCATION 0
+  BIND BUFFER my_fb_ms1 AS color LOCATION 1
+  BIND BUFFER my_fb_ms2 AS color LOCATION 2
+  BIND BUFFER my_fb0 AS resolve
+  BIND BUFFER my_fb1 AS resolve
+  BIND BUFFER my_fb2 AS resolve
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto script = parser.GetScript();
+  const auto& pipelines = script->GetPipelines();
+  ASSERT_EQ(1U, pipelines.size());
+
+  const auto* pipeline = pipelines[0].get();
+  const auto& resolve_targets = pipeline->GetResolveTargets();
+  ASSERT_EQ(3U, resolve_targets.size());
+
+  for (const auto& buf_info : resolve_targets) {
+    ASSERT_TRUE(buf_info.buffer != nullptr);
+    EXPECT_EQ(64u * 64u, buf_info.buffer->ElementCount());
+    EXPECT_EQ(64u * 64u * 4u, buf_info.buffer->ValueCount());
+    EXPECT_EQ(64u * 64u * 4u * sizeof(float),
+              buf_info.buffer->GetSizeInBytes());
+  }
+}
+
 }  // namespace amberscript
 }  // namespace amber
diff --git a/src/amberscript/parser_blend_test.cc b/src/amberscript/parser_blend_test.cc
new file mode 100644
index 0000000..013ccbd
--- /dev/null
+++ b/src/amberscript/parser_blend_test.cc
@@ -0,0 +1,140 @@
+// Copyright 2021 The Amber Authors.
+//
+// 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 parseried.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "gtest/gtest.h"
+#include "src/amberscript/parser.h"
+
+namespace amber {
+namespace amberscript {
+
+using AmberScriptParserTest = testing::Test;
+
+TEST_F(AmberScriptParserTest, BlendAllValues) {
+  std::string in = R"(
+SHADER vertex my_shader PASSTHROUGH
+SHADER fragment my_fragment GLSL
+# GLSL Shader
+END
+BUFFER my_fb FORMAT R32G32B32A32_SFLOAT
+
+PIPELINE graphics my_pipeline
+  ATTACH my_shader
+  ATTACH my_fragment
+  BIND BUFFER my_fb AS color LOCATION 0
+
+  BLEND
+    SRC_COLOR_FACTOR src_alpha
+    DST_COLOR_FACTOR one_minus_src_alpha
+    COLOR_OP add
+    SRC_ALPHA_FACTOR one
+    DST_ALPHA_FACTOR zero
+    ALPHA_OP max
+  END
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto script = parser.GetScript();
+  const auto& pipelines = script->GetPipelines();
+  ASSERT_EQ(1U, pipelines.size());
+
+  auto* pipeline = pipelines[0].get();
+
+  ASSERT_TRUE(pipeline->GetPipelineData()->GetEnableBlend());
+  ASSERT_EQ(BlendFactor::kSrcAlpha,
+            pipeline->GetPipelineData()->GetSrcColorBlendFactor());
+  ASSERT_EQ(BlendFactor::kOneMinusSrcAlpha,
+            pipeline->GetPipelineData()->GetDstColorBlendFactor());
+  ASSERT_EQ(BlendOp::kAdd,
+            pipeline->GetPipelineData()->GetColorBlendOp());
+
+  ASSERT_EQ(BlendFactor::kOne,
+            pipeline->GetPipelineData()->GetSrcAlphaBlendFactor());
+  ASSERT_EQ(BlendFactor::kZero,
+            pipeline->GetPipelineData()->GetDstAlphaBlendFactor());
+  ASSERT_EQ(BlendOp::kMax,
+            pipeline->GetPipelineData()->GetAlphaBlendOp());
+}
+
+TEST_F(AmberScriptParserTest, BlendDefaultValues) {
+  std::string in = R"(
+SHADER vertex my_shader PASSTHROUGH
+SHADER fragment my_fragment GLSL
+# GLSL Shader
+END
+BUFFER my_fb FORMAT R32G32B32A32_SFLOAT
+
+PIPELINE graphics my_pipeline
+  ATTACH my_shader
+  ATTACH my_fragment
+  BIND BUFFER my_fb AS color LOCATION 0
+
+  BLEND
+  END
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto script = parser.GetScript();
+  const auto& pipelines = script->GetPipelines();
+  ASSERT_EQ(1U, pipelines.size());
+
+  auto* pipeline = pipelines[0].get();
+
+  ASSERT_TRUE(pipeline->GetPipelineData()->GetEnableBlend());
+  ASSERT_EQ(BlendFactor::kOne,
+            pipeline->GetPipelineData()->GetSrcColorBlendFactor());
+  ASSERT_EQ(BlendFactor::kZero,
+            pipeline->GetPipelineData()->GetDstColorBlendFactor());
+  ASSERT_EQ(BlendOp::kAdd,
+            pipeline->GetPipelineData()->GetColorBlendOp());
+
+  ASSERT_EQ(BlendFactor::kOne,
+            pipeline->GetPipelineData()->GetSrcAlphaBlendFactor());
+  ASSERT_EQ(BlendFactor::kZero,
+            pipeline->GetPipelineData()->GetDstAlphaBlendFactor());
+  ASSERT_EQ(BlendOp::kAdd,
+            pipeline->GetPipelineData()->GetAlphaBlendOp());
+}
+
+TEST_F(AmberScriptParserTest, BlendInvalidColorFactor) {
+  std::string in = R"(
+SHADER vertex my_shader PASSTHROUGH
+SHADER fragment my_fragment GLSL
+# GLSL Shader
+END
+BUFFER my_fb FORMAT R32G32B32A32_SFLOAT
+
+PIPELINE graphics my_pipeline
+  ATTACH my_shader
+  ATTACH my_fragment
+  BIND BUFFER my_fb AS color LOCATION 0
+
+  BLEND
+    SRC_COLOR_FACTOR foo
+  END
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ("14: BLEND invalid value for SRC_COLOR_FACTOR: foo", r.Error());
+}
+
+}  // namespace amberscript
+}  // namespace amber
diff --git a/src/amberscript/parser_pipeline_test.cc b/src/amberscript/parser_pipeline_test.cc
index 2b56e2e..4707dd9 100644
--- a/src/amberscript/parser_pipeline_test.cc
+++ b/src/amberscript/parser_pipeline_test.cc
@@ -61,6 +61,8 @@
   EXPECT_EQ(kShaderTypeFragment, shaders[1].GetShader()->GetType());
   EXPECT_EQ(static_cast<uint32_t>(0),
             shaders[1].GetShaderOptimizations().size());
+
+  EXPECT_EQ(pipelines[0]->GetPipelineData()->GetPatchControlPoints(), 3u);
 }
 
 TEST_F(AmberScriptParserTest, PipelineMissingEnd) {
@@ -541,5 +543,85 @@
   EXPECT_EQ(4u, s2[0].GetSpecialization().at(3));
 }
 
+TEST_F(AmberScriptParserTest, PipelinePatchControlPoints) {
+  std::string in = R"(
+DEVICE_FEATURE tessellationShader
+
+SHADER vertex my_shader PASSTHROUGH
+SHADER fragment my_fragment GLSL
+# GLSL Shader
+END
+
+SHADER tessellation_control my_tesc GLSL
+# GLSL Shader
+END
+
+SHADER tessellation_evaluation my_tese GLSL
+# GLSL Shader
+END
+
+PIPELINE graphics my_pipeline
+  ATTACH my_shader
+  ATTACH my_tesc
+  ATTACH my_tese
+  ATTACH my_fragment
+
+  PATCH_CONTROL_POINTS 4
+END
+)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto script = parser.GetScript();
+  const auto& pipelines = script->GetPipelines();
+  ASSERT_EQ(1U, pipelines.size());
+
+  EXPECT_EQ(pipelines[0]->GetPipelineData()->GetPatchControlPoints(), 4u);
+}
+
+TEST_F(AmberScriptParserTest, PipelineDerivePatchControlPoints) {
+  std::string in = R"(
+DEVICE_FEATURE tessellationShader
+
+SHADER vertex my_shader PASSTHROUGH
+SHADER fragment my_fragment GLSL
+# GLSL Shader
+END
+
+SHADER tessellation_control my_tesc GLSL
+# GLSL Shader
+END
+
+SHADER tessellation_evaluation my_tese GLSL
+# GLSL Shader
+END
+
+PIPELINE graphics my_pipeline
+  ATTACH my_shader
+  ATTACH my_tesc
+  ATTACH my_tese
+  ATTACH my_fragment
+
+  PATCH_CONTROL_POINTS 4
+END
+
+DERIVE_PIPELINE child_pipeline FROM my_pipeline
+END
+)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto script = parser.GetScript();
+  const auto& pipelines = script->GetPipelines();
+  ASSERT_EQ(2U, pipelines.size());
+
+  EXPECT_EQ(pipelines[0]->GetPipelineData()->GetPatchControlPoints(), 4u);
+  EXPECT_EQ(pipelines[1]->GetPipelineData()->GetPatchControlPoints(), 4u);
+}
+
 }  // namespace amberscript
 }  // namespace amber
diff --git a/src/buffer.h b/src/buffer.h
index 18f7fed..90f3b01 100644
--- a/src/buffer.h
+++ b/src/buffer.h
@@ -62,7 +62,9 @@
   /// A uniform texel buffer.
   kUniformTexelBuffer,
   /// A storage texel buffer.
-  kStorageTexelBuffer
+  kStorageTexelBuffer,
+  /// A resolve target.
+  kResolve
 };
 
 enum class InputRate : int8_t {
diff --git a/src/command_data.cc b/src/command_data.cc
index 80083e1..c0b7612 100644
--- a/src/command_data.cc
+++ b/src/command_data.cc
@@ -54,4 +54,154 @@
   return Topology::kUnknown;
 }
 
+BlendFactor NameToBlendFactor(const std::string& name) {
+  if (name == "zero")
+    return BlendFactor::kZero;
+  else if (name == "one")
+    return BlendFactor::kOne;
+  else if (name == "src_color")
+    return BlendFactor::kSrcColor;
+  else if (name == "one_minus_src_color")
+    return BlendFactor::kOneMinusSrcColor;
+  else if (name == "dst_color")
+    return BlendFactor::kDstColor;
+  else if (name == "one_minus_dst_color")
+    return BlendFactor::kOneMinusDstColor;
+  else if (name == "src_alpha")
+    return BlendFactor::kSrcAlpha;
+  else if (name == "one_minus_src_alpha")
+    return BlendFactor::kOneMinusSrcAlpha;
+  else if (name == "dst_alpha")
+    return BlendFactor::kDstAlpha;
+  else if (name == "one_minus_dst_alpha")
+    return BlendFactor::kOneMinusDstAlpha;
+  else if (name == "constant_color")
+    return BlendFactor::kConstantColor;
+  else if (name == "one_minus_constant_color")
+    return BlendFactor::kOneMinusConstantColor;
+  else if (name == "costant_alpha")
+    return BlendFactor::kConstantAlpha;
+  else if (name == "one_minus_constant_alpha")
+    return BlendFactor::kOneMinusConstantAlpha;
+  else if (name == "src_alpha_saturate")
+    return BlendFactor::kSrcAlphaSaturate;
+  else if (name == "src1_color")
+    return BlendFactor::kSrc1Color;
+  else if (name == "one_minus_src1_color")
+    return BlendFactor::kOneMinusSrc1Color;
+  else if (name == "src1_alpha")
+    return BlendFactor::kSrc1Alpha;
+  else if (name == "one_minus_src1_alpha")
+    return BlendFactor::kOneMinusSrc1Alpha;
+  else
+    return BlendFactor::kUnknown;
+}
+
+BlendOp NameToBlendOp(const std::string& name) {
+  if (name == "add")
+    return BlendOp::kAdd;
+  else if (name == "substract")
+    return BlendOp::kSubtract;
+  else if (name == "reverse_substract")
+    return BlendOp::kReverseSubtract;
+  else if (name == "min")
+    return BlendOp::kMin;
+  else if (name == "max")
+    return BlendOp::kMax;
+  else if (name == "zero")
+    return BlendOp::kZero;
+  else if (name == "src")
+    return BlendOp::kSrc;
+  else if (name == "dst")
+    return BlendOp::kDst;
+  else if (name == "src_over")
+    return BlendOp::kSrcOver;
+  else if (name == "dst_over")
+    return BlendOp::kDstOver;
+  else if (name == "src_in")
+    return BlendOp::kSrcIn;
+  else if (name == "dst_in")
+    return BlendOp::kDstIn;
+  else if (name == "src_out")
+    return BlendOp::kSrcOut;
+  else if (name == "dst_out")
+    return BlendOp::kDstOut;
+  else if (name == "src_atop")
+    return BlendOp::kSrcAtop;
+  else if (name == "dst_atop")
+    return BlendOp::kDstAtop;
+  else if (name == "xor")
+    return BlendOp::kXor;
+  else if (name == "multiply")
+    return BlendOp::kMultiply;
+  else if (name == "screen")
+    return BlendOp::kScreen;
+  else if (name == "overlay")
+    return BlendOp::kOverlay;
+  else if (name == "darken")
+    return BlendOp::kDarken;
+  else if (name == "lighten")
+    return BlendOp::kLighten;
+  else if (name == "color_dodge")
+    return BlendOp::kColorDodge;
+  else if (name == "color_burn")
+    return BlendOp::kColorBurn;
+  else if (name == "hard_light")
+    return BlendOp::kHardLight;
+  else if (name == "soft_light")
+    return BlendOp::kSoftLight;
+  else if (name == "difference")
+    return BlendOp::kDifference;
+  else if (name == "exclusion")
+    return BlendOp::kExclusion;
+  else if (name == "invert")
+    return BlendOp::kInvert;
+  else if (name == "invert_rgb")
+    return BlendOp::kInvertRGB;
+  else if (name == "linear_dodge")
+    return BlendOp::kLinearDodge;
+  else if (name == "linear_burn")
+    return BlendOp::kLinearBurn;
+  else if (name == "vivid_light")
+    return BlendOp::kVividLight;
+  else if (name == "linear_light")
+    return BlendOp::kLinearLight;
+  else if (name == "pin_light")
+    return BlendOp::kPinLight;
+  else if (name == "hard_mix")
+    return BlendOp::kHardMix;
+  else if (name == "hsl_hue")
+    return BlendOp::kHslHue;
+  else if (name == "hsl_saturation")
+    return BlendOp::kHslSaturation;
+  else if (name == "hsl_color")
+    return BlendOp::kHslColor;
+  else if (name == "hsl_luminosity")
+    return BlendOp::kHslLuminosity;
+  else if (name == "plus")
+    return BlendOp::kPlus;
+  else if (name == "plus_clamped")
+    return BlendOp::kPlusClamped;
+  else if (name == "plus_clamped_alpha")
+    return BlendOp::kPlusClampedAlpha;
+  else if (name == "plus_darker")
+    return BlendOp::kPlusDarker;
+  else if (name == "minus")
+    return BlendOp::kMinus;
+  else if (name == "minus_clamped")
+    return BlendOp::kMinusClamped;
+  else if (name == "contrast")
+    return BlendOp::kContrast;
+  else if (name == "invert_ovg")
+    return BlendOp::kInvertOvg;
+  else if (name == "red")
+    return BlendOp::kRed;
+  else if (name == "green")
+    return BlendOp::kGreen;
+  else if (name == "blue")
+    return BlendOp::kBlue;
+  else
+    return BlendOp::kUnknown;
+}
+
 }  // namespace amber
diff --git a/src/command_data.h b/src/command_data.h
index 98ec405..f7e82a2 100644
--- a/src/command_data.h
+++ b/src/command_data.h
@@ -104,7 +104,8 @@
 };
 
 enum class BlendOp : uint8_t {
-  kAdd = 0,
+  kUnknown = 0,
+  kAdd,
   kSubtract,
   kReverseSubtract,
   kMin,
@@ -158,7 +159,8 @@
 };
 
 enum class BlendFactor : uint8_t {
-  kZero = 0,
+  kUnknown = 0,
+  kZero,
   kOne,
   kSrcColor,
   kOneMinusSrcColor,
@@ -180,6 +182,8 @@
 };
 
 Topology NameToTopology(const std::string& name);
+BlendFactor NameToBlendFactor(const std::string& name);
+BlendOp NameToBlendOp(const std::string& name);
 
 }  // namespace amber
 
diff --git a/src/pipeline.cc b/src/pipeline.cc
index 9b73041..ae3bf0a 100644
--- a/src/pipeline.cc
+++ b/src/pipeline.cc
@@ -389,6 +389,18 @@
   return {};
 }
 
+Result Pipeline::AddResolveTarget(Buffer* buf) {
+  resolve_targets_.push_back(BufferInfo{buf});
+
+  auto& info = resolve_targets_.back();
+  info.type = BufferType::kResolve;
+  buf->SetWidth(fb_width_);
+  buf->SetHeight(fb_height_);
+  buf->SetElementCount(fb_width_ * fb_height_);
+
+  return {};
+}
+
 Result Pipeline::GetLocationForColorAttachment(Buffer* buf,
                                                uint32_t* loc) const {
   for (const auto& info : color_attachments_) {
diff --git a/src/pipeline.h b/src/pipeline.h
index 7a80ad4..9010c59 100644
--- a/src/pipeline.h
+++ b/src/pipeline.h
@@ -303,6 +303,14 @@
   /// something goes wrong.
   Result GetLocationForColorAttachment(Buffer* buf, uint32_t* loc) const;
 
+  /// Returns a list of all resolve targets in this pipeline.
+  const std::vector<BufferInfo>& GetResolveTargets() const {
+    return resolve_targets_;
+  }
+
+  /// Adds |buf| as a multisample resolve target in the pipeline.
+  Result AddResolveTarget(Buffer* buf);
+
   /// Sets |buf| as the depth/stencil buffer for this pipeline.
   Result SetDepthStencilBuffer(Buffer* buf);
   /// Returns information on the depth/stencil buffer bound to the pipeline. If
@@ -436,6 +444,7 @@
   std::string name_;
   std::vector<ShaderInfo> shaders_;
   std::vector<BufferInfo> color_attachments_;
+  std::vector<BufferInfo> resolve_targets_;
   std::vector<BufferInfo> vertex_buffers_;
   std::vector<BufferInfo> buffers_;
   std::vector<std::unique_ptr<type::Type>> types_;
diff --git a/src/pipeline_data.h b/src/pipeline_data.h
index 9a3b4d1..c763f37 100644
--- a/src/pipeline_data.h
+++ b/src/pipeline_data.h
@@ -178,6 +178,11 @@
   bool HasViewportData() const { return has_viewport_data; }
   const Viewport& GetViewport() const { return vp; }
 
+  void SetPatchControlPoints(uint32_t control_points) {
+    patch_control_points_ = control_points;
+  }
+  uint32_t GetPatchControlPoints() const { return patch_control_points_; }
+
  private:
   StencilOp front_fail_op_ = StencilOp::kKeep;
   StencilOp front_pass_op_ = StencilOp::kKeep;
@@ -233,6 +238,8 @@
 
   bool has_viewport_data = false;
   Viewport vp;
+
+  uint32_t patch_control_points_ = 3u;
 };
 
 }  // namespace amber
diff --git a/src/script.cc b/src/script.cc
index 7d7483f..8da8f11 100644
--- a/src/script.cc
+++ b/src/script.cc
@@ -114,7 +114,21 @@
          name == "Storage16BitFeatures.storagePushConstant16" ||
          name == "Storage16BitFeatures.storageInputOutput16" ||
          name == "SubgroupSizeControl.subgroupSizeControl" ||
-         name == "SubgroupSizeControl.computeFullSubgroups";
+         name == "SubgroupSizeControl.computeFullSubgroups" ||
+         name == "SubgroupSupportedOperations.basic" ||
+         name == "SubgroupSupportedOperations.vote" ||
+         name == "SubgroupSupportedOperations.arithmetic" ||
+         name == "SubgroupSupportedOperations.ballot" ||
+         name == "SubgroupSupportedOperations.shuffle" ||
+         name == "SubgroupSupportedOperations.shuffleRelative" ||
+         name == "SubgroupSupportedOperations.clustered" ||
+         name == "SubgroupSupportedOperations.quad" ||
+         name == "SubgroupSupportedStages.vertex" ||
+         name == "SubgroupSupportedStages.tessellationControl" ||
+         name == "SubgroupSupportedStages.tessellationEvaluation" ||
+         name == "SubgroupSupportedStages.geometry" ||
+         name == "SubgroupSupportedStages.fragment" ||
+         name == "SubgroupSupportedStages.compute";
 }
 
 type::Type* Script::ParseType(const std::string& str) {
diff --git a/src/vulkan/CMakeLists.txt b/src/vulkan/CMakeLists.txt
index b633c71..37bdd1f 100644
--- a/src/vulkan/CMakeLists.txt
+++ b/src/vulkan/CMakeLists.txt
@@ -43,6 +43,13 @@
 # Add the Vulkan include directory to the list of include paths.
 target_include_directories(libamberenginevulkan PRIVATE "${VulkanHeaders_INCLUDE_DIR}")
 
+# When building with dEQP Vulkan CTS the inl files needs to be included and a dependency 
+# must be added to the target `deqp-vk-inl` that generates the inl files.
+if (${VULKAN_CTS_HEADER} AND DEFINED AMBER_CTS_INL_DIR)
+    target_include_directories(libamberenginevulkan PRIVATE "${AMBER_CTS_INL_DIR}")
+    add_dependencies(libamberenginevulkan deqp-vk-inl)
+endif()
+
 set_target_properties(libamberenginevulkan PROPERTIES
     OUTPUT_NAME "amberenginevulkan"
 )
@@ -52,7 +59,7 @@
 
 if(${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang")
   # vulkan/vulkan.h defines VK_NULL_HANDLE as 0u and that also serves as a null pointer.
-  # Disable Clang's warning that will alwaays fire on that.  This is required to build
+  # Disable Clang's warning that will always fire on that. This is required to build
   # with XCode 10.
   target_compile_options(libamberenginevulkan PRIVATE -Wno-zero-as-null-pointer-constant)
 endif()
diff --git a/src/vulkan/buffer_backed_descriptor.cc b/src/vulkan/buffer_backed_descriptor.cc
index d1b1aed..a2e874d 100644
--- a/src/vulkan/buffer_backed_descriptor.cc
+++ b/src/vulkan/buffer_backed_descriptor.cc
@@ -26,79 +26,65 @@
                                                DescriptorType type,
                                                Device* device,
                                                uint32_t desc_set,
-                                               uint32_t binding)
-    : Descriptor(type, device, desc_set, binding) {
+                                               uint32_t binding,
+                                               Pipeline* pipeline)
+    : Descriptor(type, device, desc_set, binding), pipeline_(pipeline) {
   AddAmberBuffer(buffer);
 }
 
 BufferBackedDescriptor::~BufferBackedDescriptor() = default;
 
-Result BufferBackedDescriptor::RecordCopyDataToResourceIfNeeded(
-    CommandBuffer* command) {
-  for (const auto& resource : GetResources()) {
-    if (!resource.first->ValuePtr()->empty()) {
-      resource.second->UpdateMemoryWithRawData(*resource.first->ValuePtr());
-      // If the resource is read-only, keep the buffer data; Amber won't copy
-      // read-only resources back into the host buffers, so it makes sense to
-      // leave the buffer intact.
-      if (!IsReadOnly())
-        resource.first->ValuePtr()->clear();
-    }
+Result BufferBackedDescriptor::RecordCopyBufferDataToTransferResourceIfNeeded(
+    CommandBuffer* command_buffer,
+    Buffer* buffer,
+    Resource* transfer_resource) {
+  transfer_resource->UpdateMemoryWithRawData(*buffer->ValuePtr());
+  // If the resource is read-only, keep the buffer data; Amber won't copy
+  // read-only resources back into the host buffers, so it makes sense to
+  // leave the buffer intact.
+  if (!transfer_resource->IsReadOnly())
+    buffer->ValuePtr()->clear();
 
-    resource.second->CopyToDevice(command);
-  }
+  transfer_resource->CopyToDevice(command_buffer);
   return {};
 }
 
-Result BufferBackedDescriptor::RecordCopyDataToHost(CommandBuffer* command) {
-  if (!IsReadOnly()) {
-    if (GetResources().empty()) {
-      return Result(
-          "Vulkan: BufferBackedDescriptor::RecordCopyDataToHost() no transfer "
-          "resources");
-    }
-
-    for (const auto& r : GetResources())
-      r.second->CopyToHost(command);
+Result BufferBackedDescriptor::RecordCopyTransferResourceToHost(
+    CommandBuffer* command_buffer,
+    Resource* transfer_resource) {
+  if (!transfer_resource->IsReadOnly()) {
+    transfer_resource->CopyToHost(command_buffer);
   }
 
   return {};
 }
 
-Result BufferBackedDescriptor::MoveResourceToBufferOutput() {
-  // No need to copy results of read only resources.
-  if (IsReadOnly())
+Result BufferBackedDescriptor::MoveTransferResourceToBufferOutput(
+    Resource* transfer_resource,
+    Buffer* buffer) {
+  // No need to move read only resources to an output buffer.
+  if (transfer_resource->IsReadOnly()) {
     return {};
+  }
 
-  auto resources = GetResources();
-
-  if (resources.empty()) {
+  void* resource_memory_ptr = transfer_resource->HostAccessibleMemoryPtr();
+  if (!resource_memory_ptr) {
     return Result(
-        "Vulkan: BufferBackedDescriptor::MoveResourceToBufferOutput() no "
-        "transfer resource");
+        "Vulkan: BufferBackedDescriptor::MoveTransferResourceToBufferOutput() "
+        "no host accessible memory pointer");
   }
 
-  for (const auto& resource : resources) {
-    void* resource_memory_ptr = resource.second->HostAccessibleMemoryPtr();
-    auto* buffer = resource.first;
-    if (!resource_memory_ptr) {
-      return Result(
-          "Vulkan: BufferBackedDescriptor::MoveResourceToBufferOutput() "
-          "no host accessible memory pointer");
-    }
-
-    if (!buffer->ValuePtr()->empty()) {
-      return Result(
-          "Vulkan: BufferBackedDescriptor::MoveResourceToBufferOutput() "
-          "output buffer is not empty");
-    }
-
-    auto size_in_bytes = resource.second->GetSizeInBytes();
-    buffer->SetElementCount(size_in_bytes / buffer->GetFormat()->SizeInBytes());
-    buffer->ValuePtr()->resize(size_in_bytes);
-    std::memcpy(buffer->ValuePtr()->data(), resource_memory_ptr, size_in_bytes);
+  if (!buffer->ValuePtr()->empty()) {
+    return Result(
+        "Vulkan: BufferBackedDescriptor::MoveTransferResourceToBufferOutput() "
+        "output buffer is not empty");
   }
 
+  auto size_in_bytes = transfer_resource->GetSizeInBytes();
+  buffer->SetElementCount(size_in_bytes / buffer->GetFormat()->SizeInBytes());
+  buffer->ValuePtr()->resize(size_in_bytes);
+  std::memcpy(buffer->ValuePtr()->data(), resource_memory_ptr, size_in_bytes);
+
   return {};
 }
 
diff --git a/src/vulkan/buffer_backed_descriptor.h b/src/vulkan/buffer_backed_descriptor.h
index 8f9df41..8a2d0a5 100644
--- a/src/vulkan/buffer_backed_descriptor.h
+++ b/src/vulkan/buffer_backed_descriptor.h
@@ -25,6 +25,7 @@
 #include "src/buffer.h"
 #include "src/engine.h"
 #include "src/vulkan/descriptor.h"
+#include "src/vulkan/pipeline.h"
 #include "src/vulkan/resource.h"
 
 namespace amber {
@@ -36,13 +37,19 @@
                          DescriptorType type,
                          Device* device,
                          uint32_t desc_set,
-                         uint32_t binding);
+                         uint32_t binding,
+                         Pipeline* pipeline);
   ~BufferBackedDescriptor() override;
 
   Result CreateResourceIfNeeded() override { return {}; }
-  Result RecordCopyDataToResourceIfNeeded(CommandBuffer* command) override;
-  Result RecordCopyDataToHost(CommandBuffer* command) override;
-  Result MoveResourceToBufferOutput() override;
+  static Result RecordCopyBufferDataToTransferResourceIfNeeded(
+      CommandBuffer* command_buffer,
+      Buffer* buffer,
+      Resource* transfer_resource);
+  static Result RecordCopyTransferResourceToHost(CommandBuffer* command_buffer,
+                                                 Resource* transfer_resource);
+  static Result MoveTransferResourceToBufferOutput(Resource* transfer_resource,
+                                                   Buffer* buffer);
   uint32_t GetDescriptorCount() override {
     return static_cast<uint32_t>(amber_buffers_.size());
   }
@@ -52,10 +59,8 @@
   bool IsReadOnly() const;
 
  protected:
-  /// Returns a list of unique transfer buffer resources. Note that this list
-  /// may contain less items than the |amber_buffers| vector contains if two or
-  /// more amber buffers use same Vulkan buffer.
-  virtual std::vector<std::pair<Buffer*, Resource*>> GetResources() = 0;
+  // Pipeline where this descriptor is attached to.
+  Pipeline* pipeline_;
 
  private:
   std::vector<Buffer*> amber_buffers_;
diff --git a/src/vulkan/buffer_descriptor.cc b/src/vulkan/buffer_descriptor.cc
index aa8b2dd..d60ce08 100644
--- a/src/vulkan/buffer_descriptor.cc
+++ b/src/vulkan/buffer_descriptor.cc
@@ -29,67 +29,65 @@
                                    DescriptorType type,
                                    Device* device,
                                    uint32_t desc_set,
-                                   uint32_t binding)
-    : BufferBackedDescriptor(buffer, type, device, desc_set, binding) {}
+                                   uint32_t binding,
+                                   Pipeline* pipeline)
+    : BufferBackedDescriptor(buffer,
+                             type,
+                             device,
+                             desc_set,
+                             binding,
+                             pipeline) {}
 
 BufferDescriptor::~BufferDescriptor() = default;
 
 Result BufferDescriptor::CreateResourceIfNeeded() {
-  if (!transfer_buffers_.empty()) {
-    return Result(
-        "Vulkan: BufferDescriptor::CreateResourceIfNeeded() must be called "
-        "only when |transfer_buffers| is empty");
+  auto& transfer_resources = pipeline_->GetDescriptorTransferResources();
+
+  VkBufferUsageFlags flags =
+      VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
+  if (IsUniformBuffer() || IsUniformBufferDynamic()) {
+    flags |= VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
+  } else if (IsStorageBuffer() || IsStorageBufferDynamic()) {
+    flags |= VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
+  } else if (IsUniformTexelBuffer()) {
+    flags |= VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT;
+  } else if (IsStorageTexelBuffer()) {
+    flags |= VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT;
+  } else {
+    return Result("Unexpected buffer type when deciding usage flags");
   }
 
+  for (const auto& amber_buffer : GetAmberBuffers()) {
+    // Create (but don't initialize) the transfer buffer if not already created.
+    if (transfer_resources.count(amber_buffer) == 0) {
+      auto size_in_bytes =
+          static_cast<uint32_t>(amber_buffer->ValuePtr()->size());
+      auto transfer_buffer = MakeUnique<TransferBuffer>(
+          device_, size_in_bytes, amber_buffer->GetFormat());
+      transfer_buffer->SetReadOnly(IsReadOnly());
+      transfer_resources[amber_buffer] = std::move(transfer_buffer);
+    } else {
+      // Unset transfer buffer's read only property if needed.
+      if (!IsReadOnly()) {
+        transfer_resources[amber_buffer]->SetReadOnly(false);
+      }
+    }
+
+    // Update the buffer create flags.
+    Result r =
+        transfer_resources[amber_buffer]->AsTransferBuffer()->AddUsageFlags(
+            flags);
+    if (!r.IsSuccess())
+      return r;
+  }
+  is_descriptor_set_update_needed_ = true;
+
   descriptor_offsets_.reserve(GetAmberBuffers().size());
   descriptor_ranges_.reserve(GetAmberBuffers().size());
 
-  for (const auto& amber_buffer : GetAmberBuffers()) {
-    if (amber_buffer->ValuePtr()->empty())
-      continue;
-
-    // Check if the transfer buffer is already created.
-    if (transfer_buffers_.count(amber_buffer) > 0)
-      continue;
-
-    auto size_in_bytes =
-        static_cast<uint32_t>(amber_buffer->ValuePtr()->size());
-
-    auto transfer_buffer = MakeUnique<TransferBuffer>(
-        device_, size_in_bytes, amber_buffer->GetFormat());
-
-    VkBufferUsageFlags flags =
-        VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
-    if (IsUniformBuffer() || IsUniformBufferDynamic()) {
-      flags |= VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
-    } else if (IsStorageBuffer() || IsStorageBufferDynamic()) {
-      flags |= VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
-    } else if (IsUniformTexelBuffer()) {
-      flags |= VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT;
-    } else if (IsStorageTexelBuffer()) {
-      flags |= VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT;
-    } else {
-      return Result("Unexpected buffer type when deciding usage flags");
-    }
-
-    Result r = transfer_buffer->Initialize(flags);
-    if (!r.IsSuccess())
-      return r;
-    transfer_buffers_[amber_buffer] = std::move(transfer_buffer);
-  }
-
-  is_descriptor_set_update_needed_ = true;
   return {};
 }
 
-Result BufferDescriptor::MoveResourceToBufferOutput() {
-  Result r = BufferBackedDescriptor::MoveResourceToBufferOutput();
-
-  transfer_buffers_.clear();
-
-  return r;
-}
-
 void BufferDescriptor::UpdateDescriptorSetIfNeeded(
     VkDescriptorSet descriptor_set) {
   if (!is_descriptor_set_update_needed_)
@@ -100,7 +98,9 @@
 
   // Create VkDescriptorBufferInfo for every descriptor buffer.
   for (uint32_t i = 0; i < GetAmberBuffers().size(); i++) {
-    const auto& buffer = transfer_buffers_[GetAmberBuffers()[i]];
+    const auto& buffer =
+        pipeline_->GetDescriptorTransferResources()[GetAmberBuffers()[i]]
+            ->AsTransferBuffer();
     assert(buffer->GetVkBuffer() && "Unexpected descriptor type");
     // Add buffer infos for uniform and storage buffers.
     if (IsUniformBuffer() || IsUniformBufferDynamic() || IsStorageBuffer() ||
@@ -146,23 +146,5 @@
   is_descriptor_set_update_needed_ = false;
 }
 
-std::vector<std::pair<Buffer*, Resource*>> BufferDescriptor::GetResources() {
-  std::vector<std::pair<Buffer*, Resource*>> ret;
-  // Add unique amber buffers and related transfer buffers to the vector.
-  for (const auto& amber_buffer : GetAmberBuffers()) {
-    // Skip duplicate values.
-    const auto& image =
-        std::find_if(ret.begin(), ret.end(),
-                     [&](const std::pair<Buffer*, Resource*>& buffer) {
-                       return buffer.first == amber_buffer;
-                     });
-    if (image != ret.end())
-      continue;
-
-    ret.emplace_back(amber_buffer, transfer_buffers_[amber_buffer].get());
-  }
-  return ret;
-}
-
 }  // namespace vulkan
 }  // namespace amber
diff --git a/src/vulkan/buffer_descriptor.h b/src/vulkan/buffer_descriptor.h
index 2c3f390..439afa5 100644
--- a/src/vulkan/buffer_descriptor.h
+++ b/src/vulkan/buffer_descriptor.h
@@ -26,6 +26,7 @@
 #include "src/buffer.h"
 #include "src/engine.h"
 #include "src/vulkan/buffer_backed_descriptor.h"
+#include "src/vulkan/pipeline.h"
 #include "src/vulkan/transfer_buffer.h"
 
 namespace amber {
@@ -42,12 +43,12 @@
                    DescriptorType type,
                    Device* device,
                    uint32_t desc_set,
-                   uint32_t binding);
+                   uint32_t binding,
+                   vulkan::Pipeline* pipeline);
   ~BufferDescriptor() override;
 
   void UpdateDescriptorSetIfNeeded(VkDescriptorSet descriptor_set) override;
   Result CreateResourceIfNeeded() override;
-  Result MoveResourceToBufferOutput() override;
   std::vector<uint32_t> GetDynamicOffsets() override {
     return dynamic_offsets_;
   }
@@ -67,12 +68,7 @@
 
   BufferDescriptor* AsBufferDescriptor() override { return this; }
 
- protected:
-  std::vector<std::pair<Buffer*, Resource*>> GetResources() override;
-
  private:
-  std::unordered_map<Buffer*, std::unique_ptr<TransferBuffer>>
-      transfer_buffers_;
   std::vector<uint32_t> dynamic_offsets_;
   std::vector<VkDeviceSize> descriptor_offsets_;
   std::vector<VkDeviceSize> descriptor_ranges_;
diff --git a/src/vulkan/command_buffer.cc b/src/vulkan/command_buffer.cc
index 2843667..765ef94 100644
--- a/src/vulkan/command_buffer.cc
+++ b/src/vulkan/command_buffer.cc
@@ -108,7 +108,6 @@
 
 void CommandBuffer::Reset() {
   if (guarded_) {
-    device_->GetPtrs()->vkEndCommandBuffer(command_);
     device_->GetPtrs()->vkResetCommandBuffer(command_, 0);
     guarded_ = false;
   }
diff --git a/src/vulkan/descriptor.h b/src/vulkan/descriptor.h
index 03434a3..88f6813 100644
--- a/src/vulkan/descriptor.h
+++ b/src/vulkan/descriptor.h
@@ -31,6 +31,7 @@
 class CommandBuffer;
 class Device;
 class BufferDescriptor;
+class ImageDescriptor;
 class BufferBackedDescriptor;
 class SamplerDescriptor;
 
@@ -57,16 +58,12 @@
 
   virtual void UpdateDescriptorSetIfNeeded(VkDescriptorSet descriptor_set) = 0;
   virtual Result CreateResourceIfNeeded() = 0;
-  virtual Result RecordCopyDataToResourceIfNeeded(CommandBuffer*) { return {}; }
-  virtual Result RecordCopyDataToHost(CommandBuffer*) { return {}; }
-  virtual Result MoveResourceToBufferOutput() { return {}; }
-  virtual Result SetSizeInElements(uint32_t) { return {}; }
-  virtual Result AddToBuffer(const std::vector<Value>&, uint32_t) { return {}; }
   virtual uint32_t GetDescriptorCount() { return 1; }
   virtual std::vector<uint32_t> GetDynamicOffsets() { return {}; }
   virtual std::vector<VkDeviceSize> GetDescriptorOffsets() { return {}; }
   virtual std::vector<VkDeviceSize> GetDescriptorRanges() { return {}; }
   virtual BufferDescriptor* AsBufferDescriptor() { return nullptr; }
+  virtual ImageDescriptor* AsImageDescriptor() { return nullptr; }
   virtual BufferBackedDescriptor* AsBufferBackedDescriptor() { return nullptr; }
   virtual SamplerDescriptor* AsSamplerDescriptor() { return nullptr; }
   uint32_t GetDescriptorSet() const { return descriptor_set_; }
diff --git a/src/vulkan/device.cc b/src/vulkan/device.cc
index 22cd77a..d951f49 100644
--- a/src/vulkan/device.cc
+++ b/src/vulkan/device.cc
@@ -53,6 +53,36 @@
 const char kSubgroupSizeControl[] = "SubgroupSizeControl.subgroupSizeControl";
 const char kComputeFullSubgroups[] = "SubgroupSizeControl.computeFullSubgroups";
 
+const char kSubgroupSupportedOperations[] = "SubgroupSupportedOperations";
+const char kSubgroupSupportedOperationsBasic[] =
+    "SubgroupSupportedOperations.basic";
+const char kSubgroupSupportedOperationsVote[] =
+    "SubgroupSupportedOperations.vote";
+const char kSubgroupSupportedOperationsArithmetic[] =
+    "SubgroupSupportedOperations.arithmetic";
+const char kSubgroupSupportedOperationsBallot[] =
+    "SubgroupSupportedOperations.ballot";
+const char kSubgroupSupportedOperationsShuffle[] =
+    "SubgroupSupportedOperations.shuffle";
+const char kSubgroupSupportedOperationsShuffleRelative[] =
+    "SubgroupSupportedOperations.shuffleRelative";
+const char kSubgroupSupportedOperationsClustered[] =
+    "SubgroupSupportedOperations.clustered";
+const char kSubgroupSupportedOperationsQuad[] =
+    "SubgroupSupportedOperations.quad";
+const char kSubgroupSupportedStages[] = "SubgroupSupportedStages";
+const char kSubgroupSupportedStagesVertex[] = "SubgroupSupportedStages.vertex";
+const char kSubgroupSupportedStagesTessellationControl[] =
+    "SubgroupSupportedStages.tessellationControl";
+const char kSubgroupSupportedStagesTessellationEvaluation[] =
+    "SubgroupSupportedStages.tessellationEvaluation";
+const char kSubgroupSupportedStagesGeometry[] =
+    "SubgroupSupportedStages.geometry";
+const char kSubgroupSupportedStagesFragment[] =
+    "SubgroupSupportedStages.fragment";
+const char kSubgroupSupportedStagesCompute[] =
+    "SubgroupSupportedStages.compute";
+
 struct BaseOutStructure {
   VkStructureType sType;
   void* pNext;
@@ -437,6 +467,7 @@
   VkPhysicalDevice16BitStorageFeaturesKHR* storage16_ptrs = nullptr;
   VkPhysicalDeviceVulkan11Features* vulkan11_ptrs = nullptr;
   VkPhysicalDeviceVulkan12Features* vulkan12_ptrs = nullptr;
+  VkPhysicalDeviceVulkan13Features* vulkan13_ptrs = nullptr;
   VkPhysicalDeviceSubgroupSizeControlFeaturesEXT*
       subgroup_size_control_features = nullptr;
   void* ptr = available_features2.pNext;
@@ -469,6 +500,9 @@
       case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES:
         vulkan12_ptrs = static_cast<VkPhysicalDeviceVulkan12Features*>(ptr);
         break;
+      case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES:
+          vulkan13_ptrs = static_cast<VkPhysicalDeviceVulkan13Features*>(ptr);
+          break;
       default:
         break;
     }
@@ -513,7 +547,7 @@
           "Shader 8-bit storage requested but feature not returned");
     }
     if ((feature == kSubgroupSizeControl || feature == kComputeFullSubgroups) &&
-        subgroup_size_control_features == nullptr) {
+        subgroup_size_control_features == nullptr && vulkan13_ptrs == nullptr) {
       return amber::Result("Missing subgroup size control features");
     }
 
@@ -622,13 +656,25 @@
       }
     }
 
-    if (feature == kSubgroupSizeControl &&
-        subgroup_size_control_features->subgroupSizeControl != VK_TRUE) {
-      return amber::Result("Missing subgroup size control feature");
-    }
-    if (feature == kComputeFullSubgroups &&
-        subgroup_size_control_features->computeFullSubgroups != VK_TRUE) {
-      return amber::Result("Missing compute full subgroups feature");
+    // If Vulkan 1.3 structure exists the features are set there.
+    if (vulkan13_ptrs) {
+        if (feature == kSubgroupSizeControl &&
+            vulkan13_ptrs->subgroupSizeControl != VK_TRUE) {
+          return amber::Result("Missing subgroup size control feature");
+        }
+        if (feature == kComputeFullSubgroups &&
+            vulkan13_ptrs->computeFullSubgroups != VK_TRUE) {
+          return amber::Result("Missing compute full subgroups feature");
+        }
+    } else {
+      if (feature == kSubgroupSizeControl &&
+          subgroup_size_control_features->subgroupSizeControl != VK_TRUE) {
+        return amber::Result("Missing subgroup size control feature");
+      }
+      if (feature == kComputeFullSubgroups &&
+          subgroup_size_control_features->computeFullSubgroups != VK_TRUE) {
+        return amber::Result("Missing compute full subgroups feature");
+      }
     }
   }
 
@@ -647,19 +693,153 @@
       std::find(required_features.begin(), required_features.end(),
                 kSubgroupSizeControl) != required_features.end();
 
-  if (needs_subgroup_size_control) {
+  bool needs_subgroup_supported_operations = false;
+  bool needs_subgroup_supported_stages = false;
+
+  // Search for subgroup supported operations requirements.
+  for (const auto& feature : required_features)
+    if (feature.find(kSubgroupSupportedOperations) != std::string::npos)
+      needs_subgroup_supported_operations = true;
+
+  // Search for subgroup supported stages requirements.
+  for (const auto& feature : required_features)
+    if (feature.find(kSubgroupSupportedStages) != std::string::npos)
+      needs_subgroup_supported_stages = true;
+
+  const bool needs_subgroup_properties =
+      needs_subgroup_supported_operations || needs_subgroup_supported_stages;
+
+  if (needs_subgroup_size_control || needs_subgroup_properties) {
+    // Always chain all physical device properties structs in case at least one
+    // of them is needed.
     VkPhysicalDeviceProperties2 properties2 = {};
+    VkPhysicalDeviceSubgroupProperties subgroup_properties = {};
+    VkPhysicalDeviceVulkan11Properties vulkan11_properties = {};
+    VkSubgroupFeatureFlags subgroup_supported_operations;
+    VkShaderStageFlags subgroup_supported_stages;
     properties2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
     properties2.pNext = &subgroup_size_control_properties_;
     subgroup_size_control_properties_.sType =
         VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_PROPERTIES_EXT;
+    if (SupportsApiVersion(1, 2, 0)) {
+      subgroup_size_control_properties_.pNext = &vulkan11_properties;
+      vulkan11_properties.sType =
+          VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_1_PROPERTIES;
+    } else {
+      subgroup_size_control_properties_.pNext = &subgroup_properties;
+      subgroup_properties.sType =
+          VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES;
+    }
 
-    if (!SupportsApiVersion(1, 1, 0)) {
+    if (needs_subgroup_size_control && !SupportsApiVersion(1, 1, 0)) {
       return Result(
           "Vulkan: Device::Initialize subgroup size control feature also "
           "requires an API version of 1.1 or higher");
     }
+    if (needs_subgroup_properties && !SupportsApiVersion(1, 1, 0)) {
+      return Result(
+          "Vulkan: Device::Initialize subgroup properties also "
+          "requires an API version of 1.1 or higher");
+    }
     ptrs_.vkGetPhysicalDeviceProperties2(physical_device_, &properties2);
+
+    if (needs_subgroup_supported_operations) {
+      // Read supported subgroup operations from the correct struct depending on
+      // the device API
+      if (SupportsApiVersion(1, 2, 0)) {
+        subgroup_supported_operations =
+            vulkan11_properties.subgroupSupportedOperations;
+      } else {
+        subgroup_supported_operations = subgroup_properties.supportedOperations;
+      }
+
+      for (const auto& feature : required_features) {
+        if (feature == kSubgroupSupportedOperationsBasic &&
+            !(subgroup_supported_operations & VK_SUBGROUP_FEATURE_BASIC_BIT)) {
+          return amber::Result("Missing subgroup operation basic feature");
+        }
+        if (feature == kSubgroupSupportedOperationsVote &&
+            !(subgroup_supported_operations & VK_SUBGROUP_FEATURE_VOTE_BIT)) {
+          return amber::Result("Missing subgroup operation vote feature");
+        }
+        if (feature == kSubgroupSupportedOperationsArithmetic &&
+            !(subgroup_supported_operations &
+              VK_SUBGROUP_FEATURE_ARITHMETIC_BIT)) {
+          return amber::Result("Missing subgroup operation arithmetic feature");
+        }
+        if (feature == kSubgroupSupportedOperationsBallot &&
+            !(subgroup_supported_operations & VK_SUBGROUP_FEATURE_BALLOT_BIT)) {
+          return amber::Result("Missing subgroup operation ballot feature");
+        }
+        if (feature == kSubgroupSupportedOperationsShuffle &&
+            !(subgroup_supported_operations &
+              VK_SUBGROUP_FEATURE_SHUFFLE_BIT)) {
+          return amber::Result("Missing subgroup operation shuffle feature");
+        }
+        if (feature == kSubgroupSupportedOperationsShuffleRelative &&
+            !(subgroup_supported_operations &
+              VK_SUBGROUP_FEATURE_SHUFFLE_RELATIVE_BIT)) {
+          return amber::Result(
+              "Missing subgroup operation shuffle relative feature");
+        }
+        if (feature == kSubgroupSupportedOperationsClustered &&
+            !(subgroup_supported_operations &
+              VK_SUBGROUP_FEATURE_CLUSTERED_BIT)) {
+          return amber::Result("Missing subgroup operation clustered feature");
+        }
+        if (feature == kSubgroupSupportedOperationsQuad &&
+            !(subgroup_supported_operations & VK_SUBGROUP_FEATURE_QUAD_BIT)) {
+          return amber::Result("Missing subgroup operation quad feature");
+        }
+      }
+    }
+
+    if (needs_subgroup_supported_stages) {
+      // Read supported subgroup stages from the correct struct depending on the
+      // device API
+      if (SupportsApiVersion(1, 2, 0)) {
+        subgroup_supported_stages = vulkan11_properties.subgroupSupportedStages;
+      } else {
+        subgroup_supported_stages = subgroup_properties.supportedStages;
+      }
+
+      for (const auto& feature : required_features) {
+        if (feature == kSubgroupSupportedStagesVertex &&
+            !(subgroup_supported_stages & VK_SHADER_STAGE_VERTEX_BIT)) {
+          return amber::Result(
+              "Subgroup operations not supported for vertex shader stage");
+        }
+        if (feature == kSubgroupSupportedStagesTessellationControl &&
+            !(subgroup_supported_stages &
+              VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT)) {
+          return amber::Result(
+              "Subgroup operations not supported for tessellation control "
+              "shader stage");
+        }
+        if (feature == kSubgroupSupportedStagesTessellationEvaluation &&
+            !(subgroup_supported_stages &
+              VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT)) {
+          return amber::Result(
+              "Subgroup operations not supported for tessellation evaluation "
+              "shader stage");
+        }
+        if (feature == kSubgroupSupportedStagesGeometry &&
+            !(subgroup_supported_stages & VK_SHADER_STAGE_GEOMETRY_BIT)) {
+          return amber::Result(
+              "Subgroup operations not supported for geometry shader stage");
+        }
+        if (feature == kSubgroupSupportedStagesFragment &&
+            !(subgroup_supported_stages & VK_SHADER_STAGE_FRAGMENT_BIT)) {
+          return amber::Result(
+              "Subgroup operations not supported for fragment shader stage");
+        }
+        if (feature == kSubgroupSupportedStagesCompute &&
+            !(subgroup_supported_stages & VK_SHADER_STAGE_COMPUTE_BIT)) {
+          return amber::Result(
+              "Subgroup operations not supported for compute shader stage");
+        }
+      }
+    }
   }
 
   return {};
@@ -676,6 +856,7 @@
   bool is_buffer_type_image = false;
   switch (type) {
     case BufferType::kColor:
+    case BufferType::kResolve:
     case BufferType::kStorageImage:
       flag = VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT;
       is_buffer_type_image = true;
diff --git a/src/vulkan/engine_vulkan.cc b/src/vulkan/engine_vulkan.cc
index 6bed716..1ed60f4 100644
--- a/src/vulkan/engine_vulkan.cc
+++ b/src/vulkan/engine_vulkan.cc
@@ -177,8 +177,11 @@
   } else {
     vk_pipeline = MakeUnique<GraphicsPipeline>(
         device_.get(), pipeline->GetColorAttachments(),
-        pipeline->GetDepthStencilBuffer(), engine_data.fence_timeout_ms,
-        stage_create_info);
+        pipeline->GetDepthStencilBuffer(), pipeline->GetResolveTargets(),
+        engine_data.fence_timeout_ms, stage_create_info);
+
+    vk_pipeline->AsGraphics()->SetPatchControlPoints(
+        pipeline->GetPipelineData()->GetPatchControlPoints());
 
     r = vk_pipeline->AsGraphics()->Initialize(pipeline->GetFramebufferWidth(),
                                               pipeline->GetFramebufferHeight(),
diff --git a/src/vulkan/frame_buffer.cc b/src/vulkan/frame_buffer.cc
index 47eb82d..b6ad13a 100644
--- a/src/vulkan/frame_buffer.cc
+++ b/src/vulkan/frame_buffer.cc
@@ -30,10 +30,12 @@
     Device* device,
     const std::vector<const amber::Pipeline::BufferInfo*>& color_attachments,
     amber::Pipeline::BufferInfo depth_stencil_attachment,
+    const std::vector<const amber::Pipeline::BufferInfo*>& resolve_targets,
     uint32_t width,
     uint32_t height)
     : device_(device),
       color_attachments_(color_attachments),
+      resolve_targets_(resolve_targets),
       depth_stencil_attachment_(depth_stencil_attachment),
       width_(width),
       height_(height) {}
@@ -62,15 +64,16 @@
 
     attachments.resize(color_attachments_.size());
     for (auto* info : color_attachments_) {
+      const VkImageUsageFlags usage_flags = VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
+                                            VK_IMAGE_USAGE_TRANSFER_DST_BIT |
+                                            VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
       color_images_.push_back(MakeUnique<TransferImage>(
           device_, *info->buffer->GetFormat(), VK_IMAGE_ASPECT_COLOR_BIT,
-          VK_IMAGE_TYPE_2D, width_ << info->base_mip_level,
+          VK_IMAGE_TYPE_2D, usage_flags, width_ << info->base_mip_level,
           height_ << info->base_mip_level, depth_, info->buffer->GetMipLevels(),
-          info->base_mip_level, 1u, 1u));
+          info->base_mip_level, 1u, info->buffer->GetSamples()));
 
-      Result r = color_images_.back()->Initialize(
-          VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
-          VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT);
+      Result r = color_images_.back()->Initialize();
       if (!r.IsSuccess())
         return r;
 
@@ -87,19 +90,37 @@
       aspect |= VK_IMAGE_ASPECT_STENCIL_BIT;
     assert(aspect != 0);
 
+    const VkImageUsageFlags usage_flags =
+        VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
+        VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
+
     depth_stencil_image_ = MakeUnique<TransferImage>(
         device_, *depth_stencil_attachment_.buffer->GetFormat(), aspect,
-        VK_IMAGE_TYPE_2D, width_, height_, depth_, 1u, 0u, 1u, 1u);
+        VK_IMAGE_TYPE_2D, usage_flags, width_, height_, depth_, 1u, 0u, 1u, 1u);
 
-    Result r = depth_stencil_image_->Initialize(
-        VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
-        VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT);
+    Result r = depth_stencil_image_->Initialize();
     if (!r.IsSuccess())
       return r;
 
     attachments.push_back(depth_stencil_image_->GetVkImageView());
   }
 
+  for (auto* info : resolve_targets_) {
+    const VkImageUsageFlags usage_flags = VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
+                                          VK_IMAGE_USAGE_TRANSFER_DST_BIT |
+                                          VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
+    resolve_images_.push_back(MakeUnique<TransferImage>(
+        device_, *info->buffer->GetFormat(), VK_IMAGE_ASPECT_COLOR_BIT,
+        VK_IMAGE_TYPE_2D, usage_flags, width_, height_, depth_, 1u, 0u, 1u,
+        1u));
+
+    Result r = resolve_images_.back()->Initialize();
+    if (!r.IsSuccess())
+      return r;
+
+    attachments.push_back(resolve_images_.back()->GetVkImageView());
+  }
+
   VkFramebufferCreateInfo frame_buffer_info = VkFramebufferCreateInfo();
   frame_buffer_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
   frame_buffer_info.renderPass = render_pass;
@@ -126,6 +147,9 @@
   for (auto& img : color_images_)
     img->ImageBarrier(command, color_layout, color_stage);
 
+  for (auto& img : resolve_images_)
+    img->ImageBarrier(command, color_layout, color_stage);
+
   if (depth_stencil_image_)
     depth_stencil_image_->ImageBarrier(command, depth_layout, depth_stage);
 }
@@ -162,6 +186,9 @@
   for (auto& img : color_images_)
     img->CopyToHost(command);
 
+  for (auto& img : resolve_images_)
+    img->CopyToHost(command);
+
   if (depth_stencil_image_)
     depth_stencil_image_->CopyToHost(command);
 }
@@ -176,6 +203,15 @@
                 info->buffer->GetSizeInBytes());
   }
 
+  for (size_t i = 0; i < resolve_images_.size(); ++i) {
+    auto& img = resolve_images_[i];
+    auto* info = resolve_targets_[i];
+    auto* values = info->buffer->ValuePtr();
+    values->resize(info->buffer->GetSizeInBytes());
+    std::memcpy(values->data(), img->HostAccessibleMemoryPtr(),
+                info->buffer->GetSizeInBytes());
+  }
+
   if (depth_stencil_image_) {
     auto* values = depth_stencil_attachment_.buffer->ValuePtr();
     values->resize(depth_stencil_attachment_.buffer->GetSizeInBytes());
@@ -205,6 +241,18 @@
                 info->buffer->GetSizeInBytes());
   }
 
+  for (size_t i = 0; i < resolve_images_.size(); ++i) {
+    auto& img = resolve_images_[i];
+    auto* info = resolve_targets_[i];
+    auto* values = info->buffer->ValuePtr();
+    // Nothing to do if our local buffer is empty
+    if (values->empty())
+      continue;
+
+    std::memcpy(img->HostAccessibleMemoryPtr(), values->data(),
+                info->buffer->GetSizeInBytes());
+  }
+
   if (depth_stencil_image_) {
     auto* values = depth_stencil_attachment_.buffer->ValuePtr();
     // Nothing to do if our local buffer is empty
diff --git a/src/vulkan/frame_buffer.h b/src/vulkan/frame_buffer.h
index 064b6d3..5774289 100644
--- a/src/vulkan/frame_buffer.h
+++ b/src/vulkan/frame_buffer.h
@@ -34,6 +34,7 @@
       Device* device,
       const std::vector<const amber::Pipeline::BufferInfo*>& color_attachments,
       amber::Pipeline::BufferInfo depth_stencil_attachment,
+      const std::vector<const amber::Pipeline::BufferInfo*>& resolve_targets,
       uint32_t width,
       uint32_t height);
   ~FrameBuffer();
@@ -70,9 +71,11 @@
 
   Device* device_ = nullptr;
   std::vector<const amber::Pipeline::BufferInfo*> color_attachments_;
+  std::vector<const amber::Pipeline::BufferInfo*> resolve_targets_;
   amber::Pipeline::BufferInfo depth_stencil_attachment_;
   VkFramebuffer frame_ = VK_NULL_HANDLE;
   std::vector<std::unique_ptr<TransferImage>> color_images_;
+  std::vector<std::unique_ptr<TransferImage>> resolve_images_;
   std::unique_ptr<TransferImage> depth_stencil_image_;
   uint32_t width_ = 0;
   uint32_t height_ = 0;
diff --git a/src/vulkan/graphics_pipeline.cc b/src/vulkan/graphics_pipeline.cc
index 5536440..138f32c 100644
--- a/src/vulkan/graphics_pipeline.cc
+++ b/src/vulkan/graphics_pipeline.cc
@@ -234,6 +234,8 @@
       return VK_BLEND_FACTOR_SRC1_ALPHA;
     case BlendFactor::kOneMinusSrc1Alpha:
       return VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA;
+    case BlendFactor::kUnknown:
+      break;
   }
   assert(false && "Vulkan::Unknown BlendFactor");
   return VK_BLEND_FACTOR_ZERO;
@@ -343,6 +345,8 @@
       return VK_BLEND_OP_GREEN_EXT;
     case BlendOp::kBlue:
       return VK_BLEND_OP_BLUE_EXT;
+    case BlendOp::kUnknown:
+      break;
   }
   assert(false && "Vulkan::Unknown BlendOp");
   return VK_BLEND_OP_ADD;
@@ -386,6 +390,7 @@
     Device* device,
     const std::vector<amber::Pipeline::BufferInfo>& color_buffers,
     amber::Pipeline::BufferInfo depth_stencil_buffer,
+    const std::vector<amber::Pipeline::BufferInfo>& resolve_targets,
     uint32_t fence_timeout_ms,
     const std::vector<VkPipelineShaderStageCreateInfo>& shader_stage_info)
     : Pipeline(PipelineType::kGraphics,
@@ -395,6 +400,8 @@
       depth_stencil_buffer_(depth_stencil_buffer) {
   for (const auto& info : color_buffers)
     color_buffers_.push_back(&info);
+  for (const auto& info : resolve_targets)
+    resolve_targets_.push_back(&info);
 }
 
 GraphicsPipeline::~GraphicsPipeline() {
@@ -412,6 +419,7 @@
 
   std::vector<VkAttachmentReference> color_refer;
   VkAttachmentReference depth_refer = VkAttachmentReference();
+  std::vector<VkAttachmentReference> resolve_refer;
 
   for (const auto* info : color_buffers_) {
     attachment_desc.push_back(kDefaultAttachmentDesc);
@@ -421,6 +429,8 @@
         VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
     attachment_desc.back().finalLayout =
         VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+    attachment_desc.back().samples =
+        static_cast<VkSampleCountFlagBits>(info->buffer->GetSamples());
 
     VkAttachmentReference ref = VkAttachmentReference();
     ref.attachment = static_cast<uint32_t>(attachment_desc.size() - 1);
@@ -446,6 +456,23 @@
     subpass_desc.pDepthStencilAttachment = &depth_refer;
   }
 
+  for (const auto* info : resolve_targets_) {
+    attachment_desc.push_back(kDefaultAttachmentDesc);
+    attachment_desc.back().format =
+        device_->GetVkFormat(*info->buffer->GetFormat());
+    attachment_desc.back().initialLayout =
+        VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+    attachment_desc.back().finalLayout =
+        VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+
+    VkAttachmentReference ref = VkAttachmentReference();
+    ref.attachment = static_cast<uint32_t>(attachment_desc.size() - 1);
+    ref.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+    resolve_refer.push_back(ref);
+  }
+
+  subpass_desc.pResolveAttachments = resolve_refer.data();
+
   VkRenderPassCreateInfo render_pass_info = VkRenderPassCreateInfo();
   render_pass_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
   render_pass_info.attachmentCount =
@@ -621,6 +648,16 @@
       VK_FALSE,                       /* alphaToOneEnable */
   };
 
+  // Search for multisampled color buffers and adjust the rasterization samples
+  // to match.
+  for (const auto& cb : color_buffers_) {
+    uint32_t samples = cb->buffer->GetSamples();
+    assert(static_cast<VkSampleCountFlagBits>(samples) >=
+           multisampleInfo.rasterizationSamples);
+    multisampleInfo.rasterizationSamples =
+        static_cast<VkSampleCountFlagBits>(samples);
+  }
+
   VkGraphicsPipelineCreateInfo pipeline_info = VkGraphicsPipelineCreateInfo();
   pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
   pipeline_info.stageCount = static_cast<uint32_t>(shader_stage_info.size());
@@ -704,8 +741,9 @@
   if (!r.IsSuccess())
     return r;
 
-  frame_ = MakeUnique<FrameBuffer>(device_, color_buffers_,
-                                   depth_stencil_buffer_, width, height);
+  frame_ =
+      MakeUnique<FrameBuffer>(device_, color_buffers_, depth_stencil_buffer_,
+                              resolve_targets_, width, height);
   r = frame_->Initialize(render_pass_);
   if (!r.IsSuccess())
     return r;
diff --git a/src/vulkan/graphics_pipeline.h b/src/vulkan/graphics_pipeline.h
index 7076231..cd55aad 100644
--- a/src/vulkan/graphics_pipeline.h
+++ b/src/vulkan/graphics_pipeline.h
@@ -43,6 +43,7 @@
       Device* device,
       const std::vector<amber::Pipeline::BufferInfo>& color_buffers,
       amber::Pipeline::BufferInfo depth_stencil_buffer,
+      const std::vector<amber::Pipeline::BufferInfo>& resolve_targets,
       uint32_t fence_timeout_ms,
       const std::vector<VkPipelineShaderStageCreateInfo>&);
   ~GraphicsPipeline() override;
@@ -86,8 +87,9 @@
   VkRenderPass render_pass_ = VK_NULL_HANDLE;
   std::unique_ptr<FrameBuffer> frame_;
 
-  // color buffers are owned by the amber::Pipeline.
+  // color buffers and resolve targets are owned by the amber::Pipeline.
   std::vector<const amber::Pipeline::BufferInfo*> color_buffers_;
+  std::vector<const amber::Pipeline::BufferInfo*> resolve_targets_;
   amber::Pipeline::BufferInfo depth_stencil_buffer_;
   std::unique_ptr<IndexBuffer> index_buffer_;
 
diff --git a/src/vulkan/image_descriptor.cc b/src/vulkan/image_descriptor.cc
index 94e67d0..1d20244 100644
--- a/src/vulkan/image_descriptor.cc
+++ b/src/vulkan/image_descriptor.cc
@@ -29,51 +29,25 @@
                                  Device* device,
                                  uint32_t base_mip_level,
                                  uint32_t desc_set,
-                                 uint32_t binding)
-    : BufferBackedDescriptor(buffer, type, device, desc_set, binding),
+                                 uint32_t binding,
+                                 Pipeline* pipeline)
+    : BufferBackedDescriptor(buffer, type, device, desc_set, binding, pipeline),
       base_mip_level_(base_mip_level),
       vulkan_sampler_(device) {}
 
 ImageDescriptor::~ImageDescriptor() = default;
 
-Result ImageDescriptor::RecordCopyDataToResourceIfNeeded(
-    CommandBuffer* command) {
-  const auto transfer_images = GetResources();
-  for (const auto& image : transfer_images) {
-    // Static cast is safe, because the type is known to be TransferImage*.
-    static_cast<TransferImage*>(image.second)
-        ->ImageBarrier(command, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
-                       VK_PIPELINE_STAGE_TRANSFER_BIT);
-  }
-
-  Result r = BufferBackedDescriptor::RecordCopyDataToResourceIfNeeded(command);
-  if (!r.IsSuccess())
-    return r;
-
-  // Just do this as early as possible.
-  for (const auto& image : transfer_images) {
-    static_cast<TransferImage*>(image.second)
-        ->ImageBarrier(command, VK_IMAGE_LAYOUT_GENERAL,
-                       VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT);
-  }
-
-  return {};
-}
-
 Result ImageDescriptor::CreateResourceIfNeeded() {
-  if (!transfer_images_.empty()) {
-    return Result(
-        "Vulkan: ImageDescriptor::CreateResourceIfNeeded() must be called "
-        "only when |transfer_images| is empty");
-  }
+  auto& transfer_resources = pipeline_->GetDescriptorTransferResources();
 
   for (const auto& amber_buffer : GetAmberBuffers()) {
     if (amber_buffer->ValuePtr()->empty())
       continue;
 
     // Check if the transfer image is already created.
-    if (transfer_images_.count(amber_buffer) > 0)
+    if (transfer_resources.count(amber_buffer) > 0) {
       continue;
+    }
 
     // Default to 2D image.
     VkImageType image_type = VK_IMAGE_TYPE_2D;
@@ -103,14 +77,8 @@
       aspect = VK_IMAGE_ASPECT_COLOR_BIT;
     }
 
-    auto transfer_image = MakeUnique<TransferImage>(
-        device_, *fmt, aspect, image_type, amber_buffer->GetWidth(),
-        amber_buffer->GetHeight(), amber_buffer->GetDepth(),
-        amber_buffer->GetMipLevels(), base_mip_level_, VK_REMAINING_MIP_LEVELS,
-        amber_buffer->GetSamples());
     VkImageUsageFlags usage =
         VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
-
     if (type_ == DescriptorType::kStorageImage) {
       usage |= VK_IMAGE_USAGE_STORAGE_BIT;
     } else {
@@ -119,11 +87,14 @@
       usage |= VK_IMAGE_USAGE_SAMPLED_BIT;
     }
 
-    Result r = transfer_image->Initialize(usage);
-    if (!r.IsSuccess())
-      return r;
+    auto transfer_image = MakeUnique<TransferImage>(
+        device_, *fmt, aspect, image_type, usage, amber_buffer->GetWidth(),
+        amber_buffer->GetHeight(), amber_buffer->GetDepth(),
+        amber_buffer->GetMipLevels(), base_mip_level_, VK_REMAINING_MIP_LEVELS,
+        amber_buffer->GetSamples());
 
-    transfer_images_[amber_buffer] = std::move(transfer_image);
+    // Store the transfer image to the pipeline's map of transfer images.
+    transfer_resources[amber_buffer] = std::move(transfer_image);
   }
 
   if (amber_sampler_) {
@@ -136,28 +107,6 @@
   return {};
 }
 
-Result ImageDescriptor::RecordCopyDataToHost(CommandBuffer* command) {
-  if (!IsReadOnly()) {
-    for (auto& image : GetResources()) {
-      static_cast<TransferImage*>(image.second)
-          ->ImageBarrier(command, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
-                         VK_PIPELINE_STAGE_TRANSFER_BIT);
-    }
-
-    BufferBackedDescriptor::RecordCopyDataToHost(command);
-  }
-
-  return {};
-}
-
-Result ImageDescriptor::MoveResourceToBufferOutput() {
-  Result r = BufferBackedDescriptor::MoveResourceToBufferOutput();
-
-  transfer_images_.clear();
-
-  return r;
-}
-
 void ImageDescriptor::UpdateDescriptorSetIfNeeded(
     VkDescriptorSet descriptor_set) {
   if (!is_descriptor_set_update_needed_)
@@ -170,7 +119,9 @@
 
   // Create VkDescriptorImageInfo for every descriptor image.
   for (const auto& amber_buffer : GetAmberBuffers()) {
-    const auto& image = transfer_images_[amber_buffer];
+    const auto& image =
+        pipeline_->GetDescriptorTransferResources()[amber_buffer]
+            ->AsTransferImage();
     VkDescriptorImageInfo image_info = {vulkan_sampler_.GetVkSampler(),
                                         image->GetVkImageView(), layout};
     image_infos.push_back(image_info);
@@ -191,23 +142,5 @@
   is_descriptor_set_update_needed_ = false;
 }
 
-std::vector<std::pair<Buffer*, Resource*>> ImageDescriptor::GetResources() {
-  std::vector<std::pair<Buffer*, Resource*>> ret;
-  // Add unique amber buffers and related transfer images to the vector.
-  for (const auto& amber_buffer : GetAmberBuffers()) {
-    // Skip duplicate values.
-    const auto& image =
-        std::find_if(ret.begin(), ret.end(),
-                     [&](const std::pair<Buffer*, Resource*>& buffer) {
-                       return buffer.first == amber_buffer;
-                     });
-    if (image != ret.end())
-      continue;
-
-    ret.emplace_back(amber_buffer, transfer_images_[amber_buffer].get());
-  }
-  return ret;
-}
-
 }  // namespace vulkan
 }  // namespace amber
diff --git a/src/vulkan/image_descriptor.h b/src/vulkan/image_descriptor.h
index 908ed17..3046436 100644
--- a/src/vulkan/image_descriptor.h
+++ b/src/vulkan/image_descriptor.h
@@ -21,6 +21,7 @@
 #include <vector>
 
 #include "src/vulkan/buffer_backed_descriptor.h"
+#include "src/vulkan/pipeline.h"
 #include "src/vulkan/sampler.h"
 #include "src/vulkan/transfer_image.h"
 
@@ -34,22 +35,18 @@
                   Device* device,
                   uint32_t base_mip_level,
                   uint32_t desc_set,
-                  uint32_t binding);
+                  uint32_t binding,
+                  Pipeline* pipeline);
   ~ImageDescriptor() override;
 
   void UpdateDescriptorSetIfNeeded(VkDescriptorSet descriptor_set) override;
-  Result RecordCopyDataToResourceIfNeeded(CommandBuffer* command) override;
   Result CreateResourceIfNeeded() override;
-  Result RecordCopyDataToHost(CommandBuffer* command) override;
-  Result MoveResourceToBufferOutput() override;
   void SetAmberSampler(amber::Sampler* sampler) { amber_sampler_ = sampler; }
 
- protected:
-  std::vector<std::pair<Buffer*, Resource*>> GetResources() override;
+  ImageDescriptor* AsImageDescriptor() override { return this; }
 
  private:
   uint32_t base_mip_level_ = 0;
-  std::unordered_map<Buffer*, std::unique_ptr<TransferImage>> transfer_images_;
   amber::Sampler* amber_sampler_ = nullptr;
   amber::vulkan::Sampler vulkan_sampler_;
 };
diff --git a/src/vulkan/index_buffer.cc b/src/vulkan/index_buffer.cc
index 73a9083..b651427 100644
--- a/src/vulkan/index_buffer.cc
+++ b/src/vulkan/index_buffer.cc
@@ -38,8 +38,12 @@
 
   transfer_buffer_ =
       MakeUnique<TransferBuffer>(device_, buffer->GetSizeInBytes(), nullptr);
-  Result r = transfer_buffer_->Initialize(VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
-                                          VK_BUFFER_USAGE_TRANSFER_DST_BIT);
+  Result r = transfer_buffer_->AddUsageFlags(VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
+                                             VK_BUFFER_USAGE_TRANSFER_DST_BIT);
+  if (!r.IsSuccess())
+    return r;
+
+  r = transfer_buffer_->Initialize();
   if (!r.IsSuccess())
     return r;
 
diff --git a/src/vulkan/pipeline.cc b/src/vulkan/pipeline.cc
index 259f375..df301e4 100644
--- a/src/vulkan/pipeline.cc
+++ b/src/vulkan/pipeline.cc
@@ -273,6 +273,18 @@
   return {};
 }
 
+Result Pipeline::AddDescriptorBuffer(Buffer* amber_buffer) {
+  // Don't add the buffer if it's already added.
+  const auto& buffer = std::find_if(
+      descriptor_buffers_.begin(), descriptor_buffers_.end(),
+      [&](const Buffer* buf) { return buf == amber_buffer; });
+  if (buffer != descriptor_buffers_.end()) {
+    return {};
+  }
+  descriptor_buffers_.push_back(amber_buffer);
+  return {};
+}
+
 Result Pipeline::AddBufferDescriptor(const BufferCommand* cmd) {
   if (cmd == nullptr)
     return Result("Pipeline::AddBufferDescriptor BufferCommand is nullptr");
@@ -319,7 +331,7 @@
     if (is_image) {
       auto image_desc = MakeUnique<ImageDescriptor>(
           cmd->GetBuffer(), desc_type, device_, cmd->GetBaseMipLevel(),
-          cmd->GetDescriptorSet(), cmd->GetBinding());
+          cmd->GetDescriptorSet(), cmd->GetBinding(), this);
       if (cmd->IsCombinedImageSampler())
         image_desc->SetAmberSampler(cmd->GetSampler());
 
@@ -327,9 +339,10 @@
     } else {
       auto buffer_desc = MakeUnique<BufferDescriptor>(
           cmd->GetBuffer(), desc_type, device_, cmd->GetDescriptorSet(),
-          cmd->GetBinding());
+          cmd->GetBinding(), this);
       descriptors.push_back(std::move(buffer_desc));
     }
+    AddDescriptorBuffer(cmd->GetBuffer());
     desc = descriptors.back().get();
   } else {
     if (desc->GetDescriptorType() != desc_type) {
@@ -338,6 +351,7 @@
           "descriptor types");
     }
     desc->AsBufferBackedDescriptor()->AddAmberBuffer(cmd->GetBuffer());
+    AddDescriptorBuffer(cmd->GetBuffer());
   }
 
   if (cmd->IsUniformDynamic() || cmd->IsSSBODynamic())
@@ -409,6 +423,16 @@
       }
     }
 
+    // Initialize transfer buffers / images.
+    for (auto buffer : descriptor_buffers_) {
+      if (descriptor_transfer_resources_.count(buffer) == 0) {
+        return Result(
+            "Vulkan: Pipeline::SendDescriptorDataToDeviceIfNeeded() "
+            "descriptor's transfer resource is not found");
+      }
+      descriptor_transfer_resources_[buffer]->Initialize();
+    }
+
     // Note that if a buffer for a descriptor is host accessible and
     // does not need to record a command to copy data to device, it
     // directly writes data to the buffer. The direct write must be
@@ -424,11 +448,27 @@
   if (!guard.IsRecording())
     return guard.GetResult();
 
-  for (auto& info : descriptor_set_info_) {
-    for (auto& desc : info.descriptors) {
-      Result r = desc->RecordCopyDataToResourceIfNeeded(command_.get());
-      if (!r.IsSuccess())
-        return r;
+  // Copy descriptor data to transfer resources.
+  for (auto& buffer : descriptor_buffers_) {
+    if (auto transfer_buffer =
+            descriptor_transfer_resources_[buffer]->AsTransferBuffer()) {
+      BufferBackedDescriptor::RecordCopyBufferDataToTransferResourceIfNeeded(
+          GetCommandBuffer(), buffer, transfer_buffer);
+    } else if (auto transfer_image =
+                   descriptor_transfer_resources_[buffer]->AsTransferImage()) {
+      transfer_image->ImageBarrier(GetCommandBuffer(),
+                                   VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+                                   VK_PIPELINE_STAGE_TRANSFER_BIT);
+
+      BufferBackedDescriptor::RecordCopyBufferDataToTransferResourceIfNeeded(
+          GetCommandBuffer(), buffer, transfer_image);
+
+      transfer_image->ImageBarrier(GetCommandBuffer(), VK_IMAGE_LAYOUT_GENERAL,
+                                   VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT);
+    } else {
+      return Result(
+          "Vulkan: Pipeline::SendDescriptorDataToDeviceIfNeeded() "
+          "this should be unreachable");
     }
   }
   return guard.Submit(GetFenceTimeout());
@@ -472,14 +512,38 @@
 }
 
 Result Pipeline::ReadbackDescriptorsToHostDataQueue() {
+  // Record required commands to copy the data to a host visible buffer.
   {
     CommandBufferGuard guard(GetCommandBuffer());
     if (!guard.IsRecording())
       return guard.GetResult();
 
-    for (auto& desc_set : descriptor_set_info_) {
-      for (auto& desc : desc_set.descriptors)
-        desc->RecordCopyDataToHost(command_.get());
+    for (auto& buffer : descriptor_buffers_) {
+      if (descriptor_transfer_resources_.count(buffer) == 0) {
+        return Result(
+            "Vulkan: Pipeline::ReadbackDescriptorsToHostDataQueue() "
+            "descriptor's transfer resource is not found");
+      }
+      if (auto transfer_buffer =
+              descriptor_transfer_resources_[buffer]->AsTransferBuffer()) {
+        Result r = BufferBackedDescriptor::RecordCopyTransferResourceToHost(
+            GetCommandBuffer(), transfer_buffer);
+        if (!r.IsSuccess())
+          return r;
+      } else if (auto transfer_image = descriptor_transfer_resources_[buffer]
+                                           ->AsTransferImage()) {
+        transfer_image->ImageBarrier(GetCommandBuffer(),
+                                     VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+                                     VK_PIPELINE_STAGE_TRANSFER_BIT);
+        Result r = BufferBackedDescriptor::RecordCopyTransferResourceToHost(
+            GetCommandBuffer(), transfer_image);
+        if (!r.IsSuccess())
+          return r;
+      } else {
+        return Result(
+            "Vulkan: Pipeline::ReadbackDescriptorsToHostDataQueue() "
+            "this should be unreachable");
+      }
     }
 
     Result r = guard.Submit(GetFenceTimeout());
@@ -487,14 +551,15 @@
       return r;
   }
 
-  for (auto& desc_set : descriptor_set_info_) {
-    for (auto& desc : desc_set.descriptors) {
-      Result r = desc->MoveResourceToBufferOutput();
-      if (!r.IsSuccess())
-        return r;
-    }
+  // Move data from transfer buffers to output buffers.
+  for (auto& buffer : descriptor_buffers_) {
+    auto& transfer_resource = descriptor_transfer_resources_[buffer];
+    Result r = BufferBackedDescriptor::MoveTransferResourceToBufferOutput(
+        transfer_resource.get(), buffer);
+    if (!r.IsSuccess())
+      return r;
   }
-
+  descriptor_transfer_resources_.clear();
   return {};
 }
 
diff --git a/src/vulkan/pipeline.h b/src/vulkan/pipeline.h
index 98b0c2a..1f85cb6 100644
--- a/src/vulkan/pipeline.h
+++ b/src/vulkan/pipeline.h
@@ -27,6 +27,7 @@
 #include "src/vulkan/buffer_backed_descriptor.h"
 #include "src/vulkan/command_buffer.h"
 #include "src/vulkan/push_constant.h"
+#include "src/vulkan/resource.h"
 
 namespace amber {
 
@@ -59,6 +60,11 @@
   /// buffer data object and put it into buffer data queue in host.
   Result ReadbackDescriptorsToHostDataQueue();
 
+  std::unordered_map<Buffer*, std::unique_ptr<Resource>>&
+  GetDescriptorTransferResources() {
+    return descriptor_transfer_resources_;
+  }
+
   void SetEntryPointName(VkShaderStageFlagBits stage,
                          const std::string& entry) {
     entry_points_[stage] = entry;
@@ -115,10 +121,17 @@
   Result CreateDescriptorSetLayouts();
   Result CreateDescriptorPools();
   Result CreateDescriptorSets();
+  /// Adds a buffer used by a descriptor. The added buffers are be stored in
+  /// |descriptor_buffers_| vector in the order they are added.
+  Result AddDescriptorBuffer(Buffer* amber_buffer);
 
   PipelineType pipeline_type_;
   std::vector<DescriptorSetInfo> descriptor_set_info_;
   std::vector<VkPipelineShaderStageCreateInfo> shader_stage_info_;
+  std::unordered_map<Buffer*, std::unique_ptr<Resource>>
+      descriptor_transfer_resources_;
+  /// Buffers used by descriptors (buffer descriptors and image descriptors).
+  std::vector<Buffer*> descriptor_buffers_;
 
   uint32_t fence_timeout_ms_ = 1000;
   bool descriptor_related_objects_already_created_ = false;
diff --git a/src/vulkan/resource.h b/src/vulkan/resource.h
index 1ed447a..d3cc0de 100644
--- a/src/vulkan/resource.h
+++ b/src/vulkan/resource.h
@@ -27,9 +27,10 @@
 
 class CommandBuffer;
 class Device;
+class TransferBuffer;
+class TransferImage;
 
-// Class for Vulkan resources. Its children are Vulkan Buffer, Vulkan Image,
-// and a class for push constant.
+// Class for Vulkan resources. Its children are Vulkan Buffer and Vulkan Image.
 class Resource {
  public:
   virtual ~Resource();
@@ -46,6 +47,12 @@
   uint32_t GetSizeInBytes() const { return size_in_bytes_; }
   void UpdateMemoryWithRawData(const std::vector<uint8_t>& raw_data);
 
+  bool IsReadOnly() const { return is_read_only_; }
+  void SetReadOnly(bool read_only) { is_read_only_ = read_only; }
+  virtual Result Initialize() = 0;
+  virtual TransferBuffer* AsTransferBuffer() { return nullptr; }
+  virtual TransferImage* AsTransferImage() { return nullptr; }
+
  protected:
   Resource(Device* device, uint32_t size);
   Result CreateVkBuffer(VkBuffer* buffer, VkBufferUsageFlags usage);
@@ -82,6 +89,7 @@
  private:
   uint32_t size_in_bytes_ = 0;
   void* memory_ptr_ = nullptr;
+  bool is_read_only_ = false;
 };
 
 }  // namespace vulkan
diff --git a/src/vulkan/transfer_buffer.cc b/src/vulkan/transfer_buffer.cc
index a13b22e..512fb7b 100644
--- a/src/vulkan/transfer_buffer.cc
+++ b/src/vulkan/transfer_buffer.cc
@@ -44,8 +44,14 @@
   }
 }
 
-Result TransferBuffer::Initialize(const VkBufferUsageFlags usage) {
-  Result r = CreateVkBuffer(&buffer_, usage);
+Result TransferBuffer::Initialize() {
+  if (buffer_) {
+    return Result(
+        "Vulkan: TransferBuffer::Initialize() transfer buffer already "
+        "initialized.");
+  }
+
+  Result r = CreateVkBuffer(&buffer_, usage_flags_);
   if (!r.IsSuccess())
     return r;
 
@@ -58,8 +64,8 @@
     return r;
 
   // Create buffer view
-  if (usage & (VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT |
-               VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT)) {
+  if (usage_flags_ & (VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT |
+                      VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT)) {
     VkBufferViewCreateInfo buffer_view_info = VkBufferViewCreateInfo();
     buffer_view_info.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO;
     buffer_view_info.buffer = buffer_;
diff --git a/src/vulkan/transfer_buffer.h b/src/vulkan/transfer_buffer.h
index 2c0a51d..7d96bec 100644
--- a/src/vulkan/transfer_buffer.h
+++ b/src/vulkan/transfer_buffer.h
@@ -34,7 +34,17 @@
   TransferBuffer(Device* device, uint32_t size_in_bytes, Format* format);
   ~TransferBuffer() override;
 
-  Result Initialize(const VkBufferUsageFlags usage);
+  TransferBuffer* AsTransferBuffer() override { return this; }
+  Result AddUsageFlags(VkBufferUsageFlags flags) {
+    if (buffer_ != VK_NULL_HANDLE) {
+      return Result(
+          "Vulkan: TransferBuffer::AddUsageFlags Usage flags can't be changed "
+          "after initializing the buffer.");
+    }
+    usage_flags_ |= flags;
+    return {};
+  }
+  Result Initialize() override;
   const VkBufferView* GetVkBufferView() const { return &view_; }
 
   VkBuffer GetVkBuffer() const { return buffer_; }
@@ -47,6 +57,7 @@
   void CopyToHost(CommandBuffer* command_buffer) override;
 
  private:
+  VkBufferUsageFlags usage_flags_ = 0;
   VkBuffer buffer_ = VK_NULL_HANDLE;
   VkDeviceMemory memory_ = VK_NULL_HANDLE;
   VkBufferView view_ = VK_NULL_HANDLE;
diff --git a/src/vulkan/transfer_image.cc b/src/vulkan/transfer_image.cc
index 545549d..adfae62 100644
--- a/src/vulkan/transfer_image.cc
+++ b/src/vulkan/transfer_image.cc
@@ -70,6 +70,7 @@
                              const Format& format,
                              VkImageAspectFlags aspect,
                              VkImageType image_type,
+                             VkImageUsageFlags image_usage_flags,
                              uint32_t x,
                              uint32_t y,
                              uint32_t z,
@@ -96,6 +97,7 @@
   image_info_.extent = {x, y, z};
   image_info_.mipLevels = mip_levels;
   image_info_.samples = GetVkSampleCount(samples);
+  image_info_.usage = image_usage_flags;
 }
 
 TransferImage::~TransferImage() {
@@ -122,12 +124,10 @@
   }
 }
 
-Result TransferImage::Initialize(VkImageUsageFlags usage) {
+Result TransferImage::Initialize() {
   if (image_ != VK_NULL_HANDLE)
     return Result("Vulkan::TransferImage was already initialized");
 
-  image_info_.usage = usage;
-
   if (device_->GetPtrs()->vkCreateImage(device_->GetVkDevice(), &image_info_,
                                         nullptr, &image_) != VK_SUCCESS) {
     return Result("Vulkan::Calling vkCreateImage Fail");
@@ -141,7 +141,7 @@
     return r;
 
   if (aspect_ & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT) &&
-      !(usage & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT)) {
+      !(image_info_.usage & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT)) {
     // Combined depth/stencil image used as a descriptor. Only one aspect can be
     // used for the image view.
     r = CreateVkImageView(VK_IMAGE_ASPECT_DEPTH_BIT);
diff --git a/src/vulkan/transfer_image.h b/src/vulkan/transfer_image.h
index 741ee3c..3af71d2 100644
--- a/src/vulkan/transfer_image.h
+++ b/src/vulkan/transfer_image.h
@@ -33,6 +33,7 @@
                 const Format& format,
                 VkImageAspectFlags aspect,
                 VkImageType image_type,
+                VkImageUsageFlags image_usage_flags,
                 uint32_t x,
                 uint32_t y,
                 uint32_t z,
@@ -42,7 +43,8 @@
                 uint32_t samples);
   ~TransferImage() override;
 
-  Result Initialize(VkImageUsageFlags usage);
+  TransferImage* AsTransferImage() override { return this; }
+  Result Initialize() override;
   VkImageView GetVkImageView() const { return view_; }
 
   void ImageBarrier(CommandBuffer* command_buffer,
diff --git a/src/vulkan/vertex_buffer.cc b/src/vulkan/vertex_buffer.cc
index 7503681..557b0a3 100644
--- a/src/vulkan/vertex_buffer.cc
+++ b/src/vulkan/vertex_buffer.cc
@@ -85,8 +85,11 @@
     uint32_t bytes = buf->GetSizeInBytes();
     transfer_buffers_.push_back(
         MakeUnique<TransferBuffer>(device_, bytes, nullptr));
-    Result r = transfer_buffers_.back()->Initialize(
+    Result r = transfer_buffers_.back()->AddUsageFlags(
         VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
+    if (!r.IsSuccess())
+      return r;
+    r = transfer_buffers_.back()->Initialize();
 
     std::memcpy(transfer_buffers_.back()->HostAccessibleMemoryPtr(),
                 buf->GetValues<void>(), bytes);
diff --git a/tests/cases/compute_one_buffer_in_multiple_bindings.amber b/tests/cases/compute_one_buffer_in_multiple_bindings.amber
new file mode 100644
index 0000000..d16bda5
--- /dev/null
+++ b/tests/cases/compute_one_buffer_in_multiple_bindings.amber
@@ -0,0 +1,94 @@
+#!amber
+# Copyright 2021 The Amber Authors.
+#
+# 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
+#
+#     https://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.
+
+SHADER compute compute_shader GLSL
+#version 430
+
+layout(set = 0, binding = 0) uniform uniform_0 {
+  vec4 uniform_data_0;
+};
+layout(set = 0, binding = 1) buffer block_0 {
+  vec4 storage_data_0;
+};
+
+layout(set = 1, binding = 0) uniform uniform_1 {
+  vec4 uniform_data_1;
+};
+layout(set = 1, binding = 1) buffer block_1 {
+  vec4 storage_data_1;
+};
+
+uniform layout(set = 2, binding = 0, rgba32f) imageBuffer texelBuffer;
+uniform layout(set = 2, binding = 1) samplerBuffer texelsIn;
+
+void main() {
+  // Verify that the uniform and storage buffers have the same contents and multiply by 2 if true)
+  if (uniform_data_0 == storage_data_0) {
+    storage_data_0 = storage_data_0 * 2;
+  }
+  if (uniform_data_1 == storage_data_1) {
+      storage_data_1 = storage_data_1 * 2;
+  }
+
+  // Load values from index 1 using storage texel buffer.
+  vec4 texel = imageLoad(texelBuffer, 1);
+
+  // Load values from index 2 using uniform texel buffer.
+  vec4 color = texelFetch(texelsIn, 2);
+
+  // Store values to index 3.
+  imageStore(texelBuffer, 3, texel + color);
+}
+END
+
+# The Vulkan spec lists the maximum value of minStorageBufferOffsetAlignment
+# (i.e. the maximum possible alignment requirement) as 256 bytes.
+# Meaningful data is placed at this offset.
+BUFFER buf0 DATA_TYPE R32G32B32A32_SFLOAT DATA
+1.0 2.0 3.0 4.0
+0.1 0.2 0.3 0.4
+0.5 0.6 0.7 0.8
+0.0 0.0 0.0 0.0
+0.0 0.0 0.0 0.0
+0.0 0.0 0.0 0.0
+0.0 0.0 0.0 0.0
+0.0 0.0 0.0 0.0
+0.0 0.0 0.0 0.0
+0.0 0.0 0.0 0.0
+0.0 0.0 0.0 0.0
+0.0 0.0 0.0 0.0
+0.0 0.0 0.0 0.0
+0.0 0.0 0.0 0.0
+0.0 0.0 0.0 0.0
+0.0 0.0 0.0 0.0
+5.0 6.0 7.0 8.0
+END
+
+PIPELINE compute pipeline
+  ATTACH compute_shader
+
+  BIND BUFFER buf0 AS uniform DESCRIPTOR_SET 0 BINDING 0
+  BIND BUFFER buf0 AS storage DESCRIPTOR_SET 0 BINDING 1
+  BIND BUFFER buf0 AS uniform DESCRIPTOR_SET 1 BINDING 0 DESCRIPTOR_OFFSET 256
+  BIND BUFFER buf0 AS storage DESCRIPTOR_SET 1 BINDING 1 DESCRIPTOR_OFFSET 256
+  BIND BUFFER buf0 AS storage_texel_buffer DESCRIPTOR_SET 2 BINDING 0
+  BIND BUFFER buf0 AS uniform_texel_buffer DESCRIPTOR_SET 2 BINDING 1
+END
+
+RUN pipeline 1 1 1
+
+EXPECT buf0 IDX 0   EQ 2.0  4.0  6.0  8.0  # Values written to the first storage buffer binding
+EXPECT buf0 IDX 48  EQ 0.6  0.8  1.0  1.2  # Values written to the storage texel buffer binding
+EXPECT buf0 IDX 256 EQ 10.0 12.0 14.0 16.0 # Values written to the second storage buffer binding
diff --git a/tests/cases/debugger_hlsl_basic_compute.amber b/tests/cases/debugger_hlsl_basic_compute.amber
index 84db5df..f1572b3 100644
--- a/tests/cases/debugger_hlsl_basic_compute.amber
+++ b/tests/cases/debugger_hlsl_basic_compute.amber
@@ -18,7 +18,7 @@
 VIRTUAL_FILE "compute.hlsl"
 
 [[vk::binding(0)]]
-StructuredBuffer<int> data;
+RWStructuredBuffer<int> data;
 
 [numthreads(1,1,1)]
 void main() {
diff --git a/tests/cases/debugger_hlsl_basic_vertex.amber b/tests/cases/debugger_hlsl_basic_vertex.amber
index 86d0381..79ea512 100644
--- a/tests/cases/debugger_hlsl_basic_vertex.amber
+++ b/tests/cases/debugger_hlsl_basic_vertex.amber
@@ -106,6 +106,12 @@
 
 DEBUG pipeline DRAW_ARRAY AS TRIANGLE_LIST START_IDX 0 COUNT 6
     THREAD VERTEX_INDEX 0
+        EXPECT LOCATION "vs.hlsl" 6 "VS_OUTPUT main(float4 pos : POSITION,"
+        STEP_IN
+        EXPECT LOCATION "vs.hlsl" 7 "               float4 color : COLOR) {"
+        STEP_IN
+        EXPECT LOCATION "vs.hlsl" 8 "  VS_OUTPUT vout;"
+        STEP_IN
         EXPECT LOCATION "vs.hlsl" 9 "  vout.pos = pos;"
         EXPECT LOCAL "pos.x" EQ -1.000000
         EXPECT LOCAL "pos.y" EQ -1.000000
diff --git a/tests/cases/debugger_hlsl_function_call.amber b/tests/cases/debugger_hlsl_function_call.amber
index 9504b03..3542b15 100644
--- a/tests/cases/debugger_hlsl_function_call.amber
+++ b/tests/cases/debugger_hlsl_function_call.amber
@@ -18,7 +18,7 @@
 VIRTUAL_FILE "compute.hlsl"
 
 [[vk::binding(0)]]
-StructuredBuffer<int> data;
+RWStructuredBuffer<int> data;
 
 int C(int x)
 {
diff --git a/tests/cases/debugger_hlsl_shadowed_vars.amber b/tests/cases/debugger_hlsl_shadowed_vars.amber
index bf8a790..d1f75a5 100644
--- a/tests/cases/debugger_hlsl_shadowed_vars.amber
+++ b/tests/cases/debugger_hlsl_shadowed_vars.amber
@@ -99,7 +99,8 @@
 PIPELINE graphics pipeline
   ATTACH vtex_shader
   SHADER_OPTIMIZATION vtex_shader
-    --legalize-hlsl
+    --inline-entry-points-exhaustive
+    --eliminate-dead-functions
   END
 
   ATTACH frag_shader
@@ -117,6 +118,10 @@
 
 DEBUG pipeline DRAW_ARRAY AS TRIANGLE_LIST START_IDX 0 COUNT 6
     THREAD VERTEX_INDEX 0
+        EXPECT LOCATION "vs.hlsl" 5 "VS_OUTPUT main(float4 pos : POSITION,"
+        STEP_IN
+        EXPECT LOCATION "vs.hlsl" 6 "               float4 color : COLOR) {"
+        STEP_IN
         EXPECT LOCATION "vs.hlsl" 7 "  float a = 1.0;"
         STEP_IN
         EXPECT LOCATION "vs.hlsl" 8 "  float b = 2.0;"
diff --git a/tests/cases/debugger_spirv_line_stepping.amber b/tests/cases/debugger_spirv_line_stepping.amber
index 1d3e33b..478e4fd 100644
--- a/tests/cases/debugger_spirv_line_stepping.amber
+++ b/tests/cases/debugger_spirv_line_stepping.amber
@@ -94,10 +94,16 @@
         END
         EXPECT LOCATION "ComputeShader0.spvasm" 20 "%5 = OpVariable %11 Uniform"
         STEP_IN
+        EXPECT LOCATION "ComputeShader0.spvasm" 21 "%12 = OpConstant %9 0"
+        STEP_IN
+        EXPECT LOCATION "ComputeShader0.spvasm" 22 "%13 = OpConstant %10 0"
+        STEP_IN
         EXPECT LOCATION "ComputeShader0.spvasm" 25 "%2 = OpVariable %15 Input"
         STEP_IN
         EXPECT LOCATION "ComputeShader0.spvasm" 27 "%6 = OpVariable %11 Uniform"
         STEP_IN
+        EXPECT LOCATION "ComputeShader0.spvasm" 29 "%1 = OpFunction %7 None %8"
+        STEP_IN
         EXPECT LOCATION "ComputeShader0.spvasm" 31 "%19 = OpAccessChain %16 %2 %13"
         STEP_IN
         EXPECT LOCATION "ComputeShader0.spvasm" 32 "%20 = OpLoad %10 %19"
diff --git a/tests/cases/draw_rect_blend.amber b/tests/cases/draw_rect_blend.amber
new file mode 100644
index 0000000..c47b93f
--- /dev/null
+++ b/tests/cases/draw_rect_blend.amber
@@ -0,0 +1,46 @@
+#!amber
+# Copyright 2021 The Amber Authors.
+#
+# 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
+#
+#     https://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.
+
+SHADER vertex vert_shader PASSTHROUGH
+SHADER fragment frag_shader GLSL
+#version 430
+layout(location = 0) out vec4 color_out;
+void main()
+{
+  color_out = vec4(1.0, 0.0, 0.0, 0.5);
+}
+END
+
+BUFFER framebuffer FORMAT B8G8R8A8_UNORM
+
+PIPELINE graphics pipeline
+  ATTACH vert_shader
+  ATTACH frag_shader
+  BIND BUFFER framebuffer AS color LOCATION 0
+
+  BLEND
+    SRC_COLOR_FACTOR src_alpha
+    DST_COLOR_FACTOR one_minus_src_alpha
+    COLOR_OP add
+    SRC_ALPHA_FACTOR one
+    DST_ALPHA_FACTOR one
+    ALPHA_OP max
+  END
+END
+ 
+CLEAR_COLOR pipeline 0 255 0 255
+CLEAR pipeline
+RUN pipeline DRAW_RECT POS 0 0 SIZE 250 250
+EXPECT framebuffer IDX 0 0 SIZE 250 250 EQ_RGBA 128 128 0 255 TOLERANCE 5%
diff --git a/tests/cases/float16.amber b/tests/cases/float16.amber
index 88baf13..0a8fd04 100644
--- a/tests/cases/float16.amber
+++ b/tests/cases/float16.amber
@@ -13,9 +13,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-INSTANCE_EXTENSION VK_KHR_get_physical_device_properties2
 DEVICE_EXTENSION VK_KHR_shader_float16_int8
+DEVICE_EXTENSION VK_KHR_16bit_storage
+DEVICE_EXTENSION VK_KHR_storage_buffer_storage_class
 DEVICE_FEATURE Float16Int8Features.shaderFloat16
+DEVICE_FEATURE Storage16BitFeatures.storageBuffer16BitAccess
 
 SHADER compute f16 GLSL
 #version 450
diff --git a/tests/cases/multisample_resolve.amber b/tests/cases/multisample_resolve.amber
new file mode 100644
index 0000000..594f9bb
--- /dev/null
+++ b/tests/cases/multisample_resolve.amber
@@ -0,0 +1,50 @@
+#!amber
+# Copyright 2021 The Amber Authors.
+#
+# 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
+#
+#     https://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.
+
+DEVICE_FEATURE sampleRateShading
+
+SHADER vertex vert_shader PASSTHROUGH
+
+SHADER fragment frag_shader GLSL
+#version 440
+layout(location = 0) out vec4 color;
+
+void main (void)
+{
+    if (gl_SampleID == 0)
+        color = vec4(1, 0, 0, 1);
+    else if (gl_SampleID == 1)
+        color = vec4(0, 1, 0, 1);
+    else if (gl_SampleID == 2)
+        color = vec4(0, 0, 1, 1);
+    else 
+        color = vec4(1, 1, 1, 1);
+}
+END
+
+IMAGE framebuffer_ms FORMAT R8G8B8A8_UNORM DIM_2D WIDTH 64 HEIGHT 64 SAMPLES 4
+IMAGE framebuffer FORMAT R8G8B8A8_UNORM DIM_2D WIDTH 64 HEIGHT 64
+
+PIPELINE graphics pipeline
+  ATTACH vert_shader
+  ATTACH frag_shader
+  FRAMEBUFFER_SIZE 64 64
+  BIND BUFFER framebuffer_ms AS color LOCATION 0
+  BIND BUFFER framebuffer AS resolve
+END
+
+RUN pipeline DRAW_RECT POS 0 0 SIZE 64 64
+
+EXPECT framebuffer IDX 0 0 SIZE 64 64 EQ_RGBA 128 128 128 255 TOLERANCE 5%
diff --git a/tests/cases/tessellation_isolines.amber b/tests/cases/tessellation_isolines.amber
new file mode 100644
index 0000000..dd3f298
--- /dev/null
+++ b/tests/cases/tessellation_isolines.amber
@@ -0,0 +1,130 @@
+#!amber
+# Copyright 2021 The Amber Authors.
+#
+# 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
+#
+#     https://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.
+
+DEVICE_FEATURE tessellationShader
+
+SHADER vertex vert GLSL
+#version 450
+
+layout (location = 0) in vec3 inPosition;
+
+void main(void)
+{
+    gl_Position = vec4(inPosition, 1.0);
+}
+END
+
+SHADER tessellation_control tesc GLSL
+#version 450
+
+layout (vertices = 4) out;
+
+void main(void)
+{
+    gl_TessLevelOuter[0] = 6.0;
+    gl_TessLevelOuter[1] = 2.0;
+
+    gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
+}
+END
+
+SHADER tessellation_evaluation tese GLSL
+#version 450
+
+layout (isolines, equal_spacing, cw) in;
+
+void main(void)
+{
+    vec4 p1 = mix(gl_in[0].gl_Position,
+            gl_in[1].gl_Position,
+            gl_TessCoord.x);
+
+    vec4 p2 = mix(gl_in[2].gl_Position,
+            gl_in[3].gl_Position,
+            gl_TessCoord.x);
+
+    gl_Position = mix(p1, p2, gl_TessCoord.y);
+}
+END
+
+SHADER fragment frag GLSL
+#version 450
+
+layout (location = 0) out vec4 outColor;
+
+void main(void)
+{
+    outColor = vec4(1, 0, 0, 1);
+}
+END
+
+SHADER compute comp_shader GLSL
+#version 450
+layout(local_size_x=10,local_size_y=10) in;
+uniform layout(set=0, binding=0, rgba8) image2D resultImage;
+
+layout(set = 0, binding = 1) buffer block0
+{
+    int counter;
+};
+
+void main()
+{
+    ivec2 uv = ivec2(gl_GlobalInvocationID.xy);
+    vec4 color = imageLoad(resultImage, uv);
+    if(color.r > 0.0) atomicAdd(counter, 1);
+}
+END
+
+BUFFER vertexPosition DATA_TYPE vec3<float> DATA
+-1.0 -1.0  0.0
+ 1.0 -1.0  0.0
+-1.0  1.0  0.0
+ 1.0  1.0  0.0
+END
+
+BUFFER counter DATA_TYPE int32 DATA 0 END
+
+BUFFER framebuffer FORMAT B8G8R8A8_UNORM
+
+PIPELINE graphics pipeline
+  ATTACH vert
+  ATTACH tesc
+  ATTACH tese
+  ATTACH frag
+
+  PATCH_CONTROL_POINTS 4
+
+  FRAMEBUFFER_SIZE 100 100
+  VERTEX_DATA vertexPosition LOCATION 0
+  BIND BUFFER framebuffer AS color LOCATION 0
+END
+
+CLEAR_COLOR pipeline 0 0 0 255
+CLEAR pipeline
+
+RUN pipeline DRAW_ARRAY AS PATCH_LIST START_IDX 0 COUNT 4
+
+PIPELINE compute verify_pipeline
+  ATTACH comp_shader
+  BIND BUFFER framebuffer AS storage_image DESCRIPTOR_SET 0 BINDING 0
+  BIND BUFFER counter AS storage DESCRIPTOR_SET 0 BINDING 1
+  FRAMEBUFFER_SIZE 100 100
+END
+
+# Count the number of red pixels as the line position might differ between implementations.
+RUN verify_pipeline 10 10 1
+
+EXPECT counter IDX 0 TOLERANCE 50 EQ 500
diff --git a/tests/run_tests.py b/tests/run_tests.py
index 1fb7eb6..6cd8fc3 100755
--- a/tests/run_tests.py
+++ b/tests/run_tests.py
@@ -96,6 +96,8 @@
   # Unsupported depth/stencil formats
   "draw_rectangles_depth_test_d24s8.amber",
   "draw_rectangles_depth_test_x8d24.amber",
+  # Tessellation not supported
+  "tessellation_isolines.amber",
 ]
 
 OPENCL_CASES = [
diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt
index 4319556..4668223 100644
--- a/third_party/CMakeLists.txt
+++ b/third_party/CMakeLists.txt
@@ -52,19 +52,30 @@
 if (${AMBER_USE_LOCAL_VULKAN})
   add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/vulkan-headers)
 
-  set(BUILD_TESTS FALSE)
+  # Skip adding the validation layers and the Vulkan loader on Android.
+  if (NOT ANDROID)
 
-  # The vulkan-loader CMake file assumes that directory exists if
-  # Wayland support is to be built.
-  if(NOT EXISTS ${WAYLAND_CLIENT_INCLUDE_DIR})
-    message(STATUS "Amber: Disabling Wayland support in Vulkan-Loader")
-    set(BUILD_WSI_WAYLAND_SUPPORT OFF CACHE BOOL "" FORCE)
+    set(BUILD_TESTS FALSE)
+
+    # The vulkan-loader CMake file assumes that directory exists if
+    # Wayland support is to be built.
+    if(NOT EXISTS ${WAYLAND_CLIENT_INCLUDE_DIR})
+      message(STATUS "Amber: Disabling Wayland support in Vulkan-Loader")
+      set(BUILD_WSI_WAYLAND_SUPPORT OFF CACHE BOOL "" FORCE)
+    endif()
+    message(STATUS "Amber: Disabling X11 support in Vulkan-Loader")
+    set(BUILD_WSI_XLIB_SUPPORT OFF CACHE BOOL "" FORCE)
+
+    set(ROBIN_HOOD_HASHING_INSTALL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/robin-hood-hashing" CACHE STRING "" FORCE)
+    add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/robin-hood-hashing)
+    set(SPIRV_HEADERS_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/spirv-headers/include" CACHE STRING "" FORCE)
+
+    add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/vulkan-loader)
+    if (MSVC)
+      option(BUILD_WERROR "Treat compiler warnings as errors" OFF)
+    endif()
+    add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/vulkan-validationlayers)
   endif()
-  message(STATUS "Amber: Disabling X11 support in Vulkan-Loader")
-  set(BUILD_WSI_XLIB_SUPPORT OFF CACHE BOOL "" FORCE)
-
-  add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/vulkan-loader)
-  add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/vulkan-validationlayers)
 endif()
 
 if (${AMBER_ENABLE_VK_DEBUGGING})
@@ -152,6 +163,8 @@
     set(AMBER_CLSPV_LLVM_DIR "${CMAKE_CURRENT_SOURCE_DIR}/clspv-llvm")
   endif()
 
+  list(APPEND CMAKE_MODULE_PATH ${AMBER_CLSPV_LLVM_DIR}/llvm/cmake/modules/)
+
   set(CLSPV_LLVM_SOURCE_DIR "${AMBER_CLSPV_LLVM_DIR}/llvm" CACHE STRING "")
   set(CLSPV_CLANG_SOURCE_DIR "${AMBER_CLSPV_LLVM_DIR}/clang" CACHE STRING "")
   set(SPIRV_HEADERS_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/spirv-headers" CACHE STRING "")
@@ -168,4 +181,8 @@
   set(LLVM_INCLUDE_UTILS OFF CACHE BOOL "")
 
   add_subdirectory(${AMBER_CLSPV_DIR} ${CMAKE_CURRENT_BINARY_DIR}/clspv)
+
+  # TODO(asuonpaa): Using the latest LLVM gives multiple warnings of using deprecated
+  # arguments. Remove this when CLSPV doesn't use them.
+  add_compile_options(clspv, NO_DEPRECATED_FLAGS)
 endif()
diff --git a/tools/roll-all b/tools/roll-all
index 97f00c3..48557e8 100755
--- a/tools/roll-all
+++ b/tools/roll-all
@@ -15,7 +15,6 @@
 
 # Defined to use origin/master instead of origin/main
 clspv=1
-clspv_llvm=1
 cpplint=1
 dxc=1
 glslang=1
@@ -25,7 +24,6 @@
 spirv_headers=1
 spirv_tools=1
 swiftshader=1
-vulkan_headers=1
 vulkan_loader=1
 vulkan_validationlayers=1