Initial commit of Amber for open source

Amber is a multi-API shader test framework.

Amber lets you capture and communicate shader bugs with the fluidity and ease of a scripting flow:

* No graphics API programming is required.
  * WIP: Supports Vulkan and [Dawn][Dawn] graphics APIs.
* A single text string (or file) maps to a single graphics API pipeline test case.  The text includes:
  * Input data, including buffers and images.
  * Shaders.
  * Expectations for the result of running the pipeline.
* Shaders can be expressed in binary form (as hex), in SPIR-V assembly, or in a higher level shader language.
* After executing the pipeline, result buffers and images can be saved to output files.

This is not an officially supported Google product.
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..2fb833a
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,2 @@
+# http://clang.llvm.org/docs/ClangFormatStyleOptions.html
+BasedOnStyle: Chromium
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..96b3bed
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+out
+third_party/glslang
+third_party/googletest
+third_party/shaderc
+third_party/spirv-tools
+third_party/spirv-headers
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..8097dd7
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,7 @@
+# This is the list of Amber authors for copyright purposes.
+#
+# This does not necessarily list everyone who has contributed code, since in
+# some cases, their employer may be the copyright holder.  To see the full list
+# of contributors, see the revision history in source control.
+
+Google LLC
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..c7c2bd9
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,105 @@
+# Copyright 2018 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 implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+cmake_minimum_required(VERSION 2.8.12)
+
+project(amber)
+enable_testing()
+
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
+
+include(CheckIncludeFile)
+include(GNUInstallDirs)
+
+include_directories("${PROJECT_SOURCE_DIR}/include")
+include_directories("${PROJECT_SOURCE_DIR}")
+include_directories("${PROJECT_SOURCE_DIR}/third_party/spirv-tools/include")
+
+include(src/dawn/find_dawn.cmake)
+include(src/vulkan/find_vulkan.cmake)
+
+add_definitions(-DAMBER_ENGINE_VULKAN=$<BOOL:${Vulkan_FOUND}>)
+add_definitions(-DAMBER_ENGINE_DAWN=$<BOOL:${Dawn_FOUND}>)
+
+
+if ("${CMAKE_BUILD_TYPE}" STREQUAL "")
+  message(STATUS "No build type selected, default to Debug")
+  set(CMAKE_BUILD_TYPE "Debug")
+endif()
+
+set(CUSTOM_CXX_FLAGS
+    -std=c++11
+    -Wall
+    -Werror
+    -Wextra)
+
+if(("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") OR
+    (("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") AND
+     (NOT CMAKE_CXX_SIMULATE_ID STREQUAL "MSVC")))
+  set(COMPILER_IS_LIKE_GNU TRUE)
+endif()
+
+if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
+  set(CUSTOM_CXX_FLAGS
+      ${CUSTOM_CXX_FLAGS}
+      -Weverything
+      -Wno-c++98-compat
+      -Wno-c++98-compat-pedantic
+      -Wno-padded
+      -Wno-switch-enum
+      -Wno-unknown-pragmas
+      -Wno-unknown-warning-option)
+
+elseif("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
+  set(CUSTOM_CXX_FLAGS
+      ${CUSTOM_CXX_FLAGS}
+      -Wno-unknown-pragmas
+      -Wpedantic
+      -pedantic-errors)
+elseif(MSVC)
+  set(CUSTOM_CXX_FLAGS
+      ${CUSTOM_CXX_FLAGS}
+      /WX)
+endif()
+
+SET(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} "${CUSTOM_CXX_FLAGS}")
+STRING(REGEX REPLACE ";" " " CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
+
+function(amber_default_compile_options TARGET)
+  if (${COMPILER_IS_LIKE_GNU})
+    target_compile_options(${TARGET} PRIVATE
+        -fno-exceptions
+        -fno-rtti)
+  endif()
+
+  if (MSVC)
+    # Specify /EHs for exception handling.
+    target_compile_options(${TARGET} PRIVATE /EHs)
+  endif()
+
+  # For MinGW cross compile, statically link to the C++ runtime.
+  # But it still depends on MSVCRT.dll.
+  if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+    if (${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU")
+      set_target_properties(${TARGET} PROPERTIES LINK_FLAGS
+          -static
+          -static-libgcc
+          -static-libstdc++)
+    endif()
+  endif()
+endfunction()
+
+add_subdirectory(third_party)
+add_subdirectory(src)
+add_subdirectory(samples)
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..954915d
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,93 @@
+# Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level of
+experience, education, socio-economic status, nationality, personal appearance,
+race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+*   Using welcoming and inclusive language
+*   Being respectful of differing viewpoints and experiences
+*   Gracefully accepting constructive criticism
+*   Focusing on what is best for the community
+*   Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+*   The use of sexualized language or imagery and unwelcome sexual attention or
+    advances
+*   Trolling, insulting/derogatory comments, and personal or political attacks
+*   Public or private harassment
+*   Publishing others' private information, such as a physical or electronic
+    address, without explicit permission
+*   Other conduct which could reasonably be considered inappropriate in a
+    professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, or to ban temporarily or permanently any
+contributor for other behaviors that they deem inappropriate, threatening,
+offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+This Code of Conduct also applies outside the project spaces when the Project
+Steward has a reasonable belief that an individual's behavior may have a
+negative impact on the project or its community.
+
+## Conflict Resolution
+
+We do not believe that all conflict is bad; healthy debate and disagreement
+often yield positive results. However, it is never okay to be disrespectful or
+to engage in behavior that violates the project’s code of conduct.
+
+If you see someone violating the code of conduct, you are encouraged to address
+the behavior directly with those involved. Many issues can be resolved quickly
+and easily, and this gives people more control over the outcome of their
+dispute. If you are unable to resolve the matter for any reason, or if the
+behavior is threatening or harassing, report it. We are dedicated to providing
+an environment where participants feel welcome and safe.
+
+Reports should be directed to dan sinclair <dsinclair@google.com>, the
+Project Steward(s) for Amber. It is the Project Steward’s duty to
+receive and address reported violations of the code of conduct. They will then
+work with a committee consisting of representatives from the Open Source
+Programs Office and the Google Open Source Strategy team. If for any reason you
+are uncomfortable reaching out the Project Steward, please email
+opensource@google.com.
+
+We will investigate every complaint, but you may not receive a direct response.
+We will use our discretion in determining when and how to follow up on reported
+incidents, which may range from not taking action to permanent expulsion from
+the project and project-sponsored spaces. We will notify the accused of the
+report and provide them an opportunity to discuss it before any action is taken.
+The identity of the reporter will be omitted from the details of the report
+supplied to the accused. In potentially harmful situations, such as ongoing
+harassment or threats to anyone's safety, we may take action without notice.
+
+## Attribution
+
+This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
+available at
+https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..db177d4
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,28 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution;
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to <https://cla.developers.google.com/> to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
+
+## Community Guidelines
+
+This project follows
+[Google's Open Source Community Guidelines](https://opensource.google.com/conduct/).
diff --git a/DEPS b/DEPS
new file mode 100644
index 0000000..d28821b
--- /dev/null
+++ b/DEPS
@@ -0,0 +1,29 @@
+use_relative_paths = True
+
+vars = {
+  'google_git':  'https://github.com/google',
+  'khronos_git': 'https://github.com/KhronosGroup',
+
+  'glslang_revision': 'a08f465d5398518e9a6aeebd4775604a4c10e381',
+  'googletest_revision': '6463ee81ae7ea8ee3dcaf341cb727d278f8cfe6b',
+  'shaderc_revision': '92efe4583fcd936252eae61684bcecde8627c9fc',
+  'spirv_headers_revision': '801cca8104245c07e8cc53292da87ee1b76946fe',
+  'spirv_tools_revision': '18fe6d59e5e8dd8c5ccf8baa40f57bda838e7dfa',
+}
+
+deps = {
+  'third_party/googletest': vars['google_git'] + '/googletest.git@' +
+      vars['googletest_revision'],
+
+  'third_party/glslang': vars['khronos_git'] + '/glslang.git@' +
+      vars['glslang_revision'],
+
+  'third_party/shaderc': vars['google_git'] + '/shaderc.git@' +
+      vars['shaderc_revision'],
+
+  'third_party/spirv-headers': vars['khronos_git'] + '/SPIRV-Headers.git@' +
+      vars['spirv_headers_revision'],
+
+  'third_party/spirv-tools': vars['khronos_git'] + '/SPIRV-Tools.git@' +
+      vars['spirv_tools_revision'],
+}
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a538c6f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,97 @@
+# Amber
+
+Amber is a multi-API shader test framework.
+
+Amber lets you capture and communicate shader bugs with the fluidity and ease of a scripting flow:
+
+* No graphics API programming is required.
+  * WIP: Supports Vulkan and [Dawn][Dawn] graphics APIs.
+* A single text string (or file) maps to a single graphics API pipeline test case.  The text includes:
+  * Input data, including buffers and images.
+  * Shaders.
+  * Expectations for the result of running the pipeline.
+* Shaders can be expressed in binary form (as hex), in SPIR-V assembly, or in a higher level shader language.
+* After executing the pipeline, result buffers and images can be saved to output files.
+
+Amber is influenced by [Talvos][Talvos] and [VkRunner][VkRunner].
+The [VkScript](docs/vk_script.md) syntax matches the format used by VkRunner.
+
+This is not an officially supported Google product.
+
+## Requirements
+
+ * Recommended: Configure at least one target graphics API.  See below.
+ * Git
+ * CMake
+ * Ninja (or other build tool)
+ * Recommended: Python, for fetching dependencies
+
+
+## Building
+```
+git clone git@github.com:google/amber
+cd amber
+./tools/git-sync-deps
+mkdir -p out/Debug
+cd out/Debug
+cmake -GNinja ../..
+ninja
+```
+
+## Backends
+
+Amber is designed to run against different graphics APIs.
+Amber will build if no graphics API is found, but will only allow verifying the
+syntax of the amber script files.
+
+Currently the Vulkan and Dawn graphics APIs are supported.
+
+### Using Vulkan as a backend
+
+A Vulkan implementation is found by CMake in the following priority order:
+
+ * First: If an enclosing CMake project includes the
+   [Vulkan-Headers][Vulkan-Headers]
+   CMake project, then headers will be picked up from there.
+
+   In this case the CMake variable `Vulkan_LIBRARIES` can name the
+   Vulkan library, or a default of `vulkan` will be used.
+
+ * Second: If you have CMake 3.7 or later, then the Vulkan implementation will
+   be found from a Vulkan SDK as published by LunarG.
+
+   Environment variables:
+   * `VULKAN_SDK` should point to the platform-specific SDK directory
+     that contains the `include` and `lib` directories.
+     Example: `VULKAN_SDK=$HOME/vulkan-macos-1.1.85.0/macOS`
+   * `VK_ICD_FILENAMES` should point to the ICD JSON file.
+     Example: `VK_ICD_FILENAMES=$VULKAN_SDK/etc/vulkan/icd/MoltenVK_icd.json`
+
+### Using Dawn as a backend
+
+We assume you have built [Dawn][Dawn] from source, and have access to both the source
+and build trees.  To build a Dawn backend for Amber, set the following CMake variables
+when configuring Amber:
+
+  * `Dawn_INCLUDE_DIR`: The directory containing `dawn/dawn_export.h` (in the source tree).
+  * `Dawn_GEN_INCLUDE_DIR`: The directory containing generated header `dawn/dawncpp.h` (in the build output tree).
+  * `Dawn_LIBRARY_DIR`: The directory containing the `dawn_native` library (in the build output tree).
+
+## Amber Sample
+
+The build will generate an `out/Debug/amber` executable which can be used to
+run amber scripts. The script can be used as
+`out/Debug/amber <path to amber file>`. Where, currently, the amber file is
+in the [VkScript](docs/vk_script.md) format.
+
+## Contributing
+
+Please see the [CONTRIBUTING](CONTRIBUTING.md) and
+[CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) files on how to contribute to Amber.
+
+
+
+[Dawn](https://dawn.googlesource.com/dawn/)
+[Talvos](https://talvos.github.io/)
+[Vulkan-Headers](https://github.com/KhronosGroup/Vulkan-Headers)
+[VkRunner](https://github.com/igalia/vkrunner)
diff --git a/docs/amber_script.md b/docs/amber_script.md
new file mode 100644
index 0000000..b998377
--- /dev/null
+++ b/docs/amber_script.md
@@ -0,0 +1,409 @@
+# AmberScript
+ * DRAFT
+
+This document defines the script input language for the Amber system. The format
+is based on the Talvos format, VkRunner format, and VkScript proposed format.
+
+## Specification
+All amber scripts must start with `#!amber` as the first line. Comments are
+specified by a # character and continue to the end of the line. Keywords are
+case sensitive. All names are made up of ASCII characters, and delimited by
+whitespace.
+
+TODO(dneto): What characters are valid in a name?
+
+### Number literals
+
+Literal numbers are normally presented in decimal form.  They are interpreted
+as integers or floating point depending on context: a command parameter is predefined
+as either integral or floating point, or the data type is user-specified (such
+as for buffer data).
+
+Hex values: Whenever an integer is expected, you may use a hexadecimal number, which
+is the characters `0x` followed by hexadecimal digits.
+
+### Shaders
+
+#### Shader Type
+ * vertex
+ * fragment
+ * geometry
+ * tessellation\_evaluation
+ * tessellation\_control
+ * compute
+
+The compute pipeline can only contain compute shaders. The graphics pipeline
+can not contain compute shaders, and must contain a vertex shader and a fragment
+shader.
+
+#### Shader Format
+ * GLSL  (with glslang)
+ * HLSL  (with dxc or glslang if dxc disabled)  -- future
+ * SPIRV-ASM (with spirv-as)
+ * SPIRV-HEX (decoded straight to spv)
+ * OPENCL-C (with clspv)  --- potentially?  -- future
+
+```
+SHADER vertex <shader_name> PASSTHROUGH
+
+SHADER <shader_type> <shader_name> <shader_format>
+...
+END
+```
+
+### Buffers
+
+#### Data Types
+ * int8
+ * int16
+ * int32
+ * int64
+ * uint8
+ * uint16
+ * uint32
+ * uint64
+ * float
+ * double
+ * vec[2,3,4]\<type>
+ * mat[2,3,4]\<type>    -- useful?
+
+TODO(dneto): Support half-precision floating point.
+
+Sized arrays and structures are not currently representable.
+
+#### Buffer Types
+ * uniform
+ * storage
+ * vertex
+ * index
+ * sampled
+ * storage
+ * color
+ * depth
+
+```
+// Filling the buffer with a given set of data. The values must provide
+// <size_in_bytes> of <type> data. The data can be provided as the type or
+// as a hex value.
+
+BUFFER <buffer_type> <name> DATA_TYPE <type> DATA
+<value>+
+END
+
+BUFFER <buffer_type> <name> DATA_TYPE <type> SIZE <size_in_bytes> <initializer>
+
+BUFFER framebuffer <name> DIMS <width_in_pixels> <height_in_pixels>
+```
+
+#### Buffer Initializers
+Fill the buffer with a single value.
+
+```
+FILL <value>
+```
+
+Fill the buffer with an increasing value from \<start> increasing by \<inc>.
+Floating point data uses floating point addition to generate increasting values.
+Likewise, integer data uses integer addition to generate increasing values.
+
+```
+SERIES <start> <inc>
+```
+
+### Pipelines
+
+#### Pipeline type
+ * compute
+ * graphics
+
+The PIPELINE command creates a pipeline. This can be either compute or graphics.
+Shaders are attached to the pipeline at pipeline creation time.
+
+```
+PIPELINE <pipeline_type> <pipeline_name>
+...
+END
+```
+
+### Pipeline Content
+
+The following commands are all specified within the `PIPELINE` command. If you
+have multiple entry points for a given shader, you'd create multiple pipelines
+each with a different `ENTRY_POINT`.
+
+Bind the entry point to use for a given shader. The default entry point is main.
+
+```
+  ENTRY_POINT <shader_name> <entry_point_name>
+```
+
+Shaders can be added into pipelines with the `ATTACH` call. Shaders may be
+attached to multiple pipelines at the same time.
+
+```
+  ATTACH <name_of_vertex_shader>
+  ATTACH <name_of_fragment_shader>
+```
+
+Set the SPIRV-Tools optimization passes to use for a given shader. The default
+is to run no optimization passes.
+```
+  SHADER_OPTIMIZATION <shader_name>
+    <optimization_name>+
+  END
+```
+
+#### Bindings
+
+##### Topologies
+ * point\_list
+ * line\_list
+ * line\_list\_with\_adjacency
+ * line\_strip
+ * line\_strip\_with\_adjacency
+ * triangle\_list
+ * triangle\_list\_with\_adjacency
+ * triangle\_strip
+ * triangle\_strip\_with\_adjacency
+ * triangle\_fan
+ * patch\_list
+
+Bind a provided framebuffer.
+
+```
+  FRAMEBUFFER <buffer_of_type_framebuffer_name>
+```
+
+Descriptor sets can be bound as:
+
+```
+  DESCRIPTOR_SET <id> BINDING <id> IDX <0> TO <buffer_name>
+```
+
+### Run a pipeline.
+
+```
+RUN <pipeline_name> <x> <y> <z>
+
+RUN <pipeline_name> \
+  DRAW_RECT POS <x_in_pixels> <y_in_pixels> \
+  SIZE <width_in_pixels> <height_in_pixels>
+
+RUN <pipeline_name> \
+  DRAW_ARRAY <indices_buffer> IN <data_buffer> \
+  AS <topology> START_IDX <value> COUNT <value>
+```
+
+### Commands
+```
+CLEAR_COLOR <pipeline> <r (0 - 255)> <g (0 - 255)> <b (0 - 255)>
+
+CLEAR <pipeline>
+```
+
+### Expectations
+
+#### Comparators
+ * EQ
+ * NE
+ * LT
+ * LE
+ * GT
+ * GE
+ * EQ\_RGB
+ * EQ\_RGBA
+
+```
+EXPECT <buffer_name> IDX <x> <y> <comparator> <value>+
+
+EXPECT <framebuffer_name> IDX <x_in_pixels> <y_in_pixels> \
+  SIZE <width_in_pixels> <height_in_pixels> \
+  EQ_RGB <r (0 - 255)> <g (0 - 255)> <b (0 - 255)>
+```
+
+## Examples
+
+### Compute Shader
+```
+#!amber
+# Simple amber compute shader.
+
+SHADER compute kComputeShader GLSL
+#version 450
+
+layout(binding = 3) buffer block {
+  vec2 values[];
+};
+
+void main() {
+  values[gl_WorkGroupID.x + gl_WorkGroupID.y * gl_NumWorkGroups.x] =
+                gl_WorkGroupID.xy;
+}
+END  # shader
+
+BUFFER storage kComputeBuffer TYPE vec2<int32> SIZE 524288 FILL 0
+
+PIPELINE compute kComputePipeline
+  ATTACH kComputeShader
+  DESCRIPTOR_SET 0 BINDING 3 IDX 0 TO kComputeBuffer
+END  # pipeline
+
+RUN kComputePipeline 256 256 1
+
+# Four corners
+EXPECT kComputeBuffer IDX 0 EQ 0 0
+EXPECT kComputeBuffer IDX 2040 EQ 255 0
+EXPECT kComputeBuffer IDX 522240 EQ 0 255
+EXPECT kComputeBuffer IDX 524280 EQ 255 255
+
+# Center
+EXPECT kComputeBuffer IDX 263168 EQ 128 128
+```
+
+### Entry Points
+```
+#!amber
+
+SHADER vertex kVertexShader PASSTHROUGH
+
+SHADER fragment kFragmentShader SPIRV-ASM
+              OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+
+; two entrypoints
+               OpEntryPoint Fragment %red "red" %color
+               OpEntryPoint Fragment %green "green" %color
+
+               OpExecutionMode %red OriginUpperLeft
+               OpExecutionMode %green OriginUpperLeft
+               OpSource GLSL 430
+               OpName %red "red"
+               OpDecorate %color Location 0
+       %void = OpTypeVoid
+          %3 = OpTypeFunction %void
+      %float = OpTypeFloat 32
+    %v4float = OpTypeVector %float 4
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+      %color = OpVariable %_ptr_Output_v4float Output
+    %float_1 = OpConstant %float 1
+    %float_0 = OpConstant %float 0
+  %red_color = OpConstantComposite %v4float %float_1 %float_0 %float_0 %float_1
+%green_color = OpConstantComposite %v4float %float_0 %float_1 %float_0 %float_1
+
+; this entrypoint outputs a red color
+        %red = OpFunction %void None %3
+          %5 = OpLabel
+               OpStore %color %red_color
+               OpReturn
+               OpFunctionEnd
+
+; this entrypoint outputs a green color
+      %green = OpFunction %void None %3
+          %6 = OpLabel
+               OpStore %color %green_color
+               OpReturn
+               OpFunctionEnd
+END  # shader
+
+BUFFER framebuffer kFrameBuffer DIMS 256 256
+
+PIPELINE graphics kRedPipeline
+  ATTACH kVertexShader
+  SHADER_OPTIMIZATION kVertexShader
+    eliminate-dead-branches
+    merge-return
+    eliminate-dead-code-aggressive
+  END
+
+  ATTACH kFragmentShader
+
+  FRAMEBUFFER kFrameBuffer
+  ENTRY_POINT kFragmentShader red
+END  # pipeline
+
+PIPELINE graphics kGreenPipeline
+  ATTACH kVertexShader
+  ATTACH kFragmentShader
+
+  FRAMEBUFFER kFrameBuffer
+  ENTRY_POINT kFragmentShader green
+END  # pipeline
+
+RUN kRedPipeline DRAW_RECT POS 0 0  SIZE 256 256
+RUN kGreenPipeline DRAW_RECT POS 128 128 SIZE 256 256
+
+EXPECT kFrameBuffer IDX 0 0 SIZE 127 127 EQ_RGB 255 0 0
+EXPECT kFrameBuffer IDX 128 128 SIZE 128 128 EQ_RGB 0 255 0
+```
+
+### Buffers
+```
+#!amber
+
+SHADER vertex kVertexShader GLSL
+  #version 430
+
+  layout(location = 0) in vec4 position;
+  layout(location = 1) in vec4 color_in;
+  layout(location = 0) out vec4 color_out;
+
+  void main() {
+    gl_Position = position;
+    color_out = color_in;
+  }
+END  # shader
+
+SHADER fragment kFragmentShader GLSL
+  #version 430
+
+  layout(location = 0) in vec4 color_in;
+  layout(location = 0) out vec4 color_out;
+
+  void main() {
+    color_out = color_in;
+  }
+END  # shader
+
+PIPELINE graphics kGraphicsPipeline
+  ATTACH kVertexShader
+  ATTACH kFragmentShader
+END  # pipeline
+
+BUFFER vertex kData TYPE vec3<int32> DATA
+# Top-left red
+-1 -1  0xff0000ff
+ 0 -1  0xff0000ff
+-1  0  0xff0000ff
+ 0  0  0xff0000ff
+# Top-right green
+ 0 -1  0xff00ff00
+ 1 -1  0xff00ff00
+ 0  0  0xff00ff00
+ 1  0  0xff00ff00
+# Bottom-left blue
+-1  0  0xffff0000
+ 0  0  0xffff0000
+-1  1  0xffff0000
+ 0  1  0xffff0000
+# Bottom-right purple
+ 0  0  0xff800080
+ 1  0  0xff800080
+ 0  1  0xff800080
+ 1  1  0xff800080
+END
+
+BUFFER index kIndices TYPE int32 DATA
+0  1  2    2  1  3
+4  5  6    6  5  7
+8  9  10   10 9  11
+12 13 14   14 13 15
+END
+
+CLEAR_COLOR kGraphicsPipeline 255 0 0 255
+CLEAR kGraphicsPipeline
+
+RUN kGraphicsPipeline \
+  DRAW_ARRAY kIndices IN kBuffer AS triangle_list \
+  START_IDX 0 COUNT 24
+ ```
diff --git a/docs/vk_script.md b/docs/vk_script.md
new file mode 100644
index 0000000..9448b56
--- /dev/null
+++ b/docs/vk_script.md
@@ -0,0 +1,609 @@
+# VkScript
+
+The VkScript format is a clone of the format used by VkRunner as described in
+[1].
+
+# General
+## Comments
+The # symbol can be used to start a comment which extends to the end of the
+line.
+
+## Continuations
+The \ can be used at the end of a line to signal a continuation, the
+new line will be skipped and parsing will treat the following line as a
+continuation of the current line.
+
+## Descriptor Sets and Bindings
+Any command below which accepts a binding will accept either a single integer
+value which will have a descriptor set of 0 and a binding of the value give or
+a string can be provided of the form <set integer>:<binding integer> in which
+case the descriptor set value will be `set` and the binding value will be
+`binding`.
+
+# Sections
+
+The format is broken down into five main sections:
+ * require
+ * shaders
+ * indices
+ * vertex data
+ * test
+
+## Require
+The `require` section lists all of the requirements for the testing environment.
+There are four types of information that can be encoded in the require section.
+
+The _feature_ list contains a list of features that are required in
+order for the test to execute. If a feature is missing an error will be reported
+and the test will fail. The _features_ are listed below in the
+*Available Require Features* section.
+
+The _framebuffer_ and _depthstencil_ commands allow setting the format for the
+given buffer. The valid values are listed below in the *Image Formats*
+section.
+
+The last option is _extensions_. Any string which isn't a _feature_,
+_framebuffer_ or _depthstencil_ is assumed to be an _extension_. The extensions
+must be of the format [a-zA-Z0-9_]+. If the device extension is not available
+we will report it is not available and the test will continue.
+
+#### Require Examples
+```
+[require]
+independentBlend
+VK_KHR_storage_buffer_storage_class
+```
+
+## Shaders
+The shader section allows you to specify the content of the shaders under test.
+This can be done as GLSL, SPIRV-ASM or SPIRV-Hex depending on how the shader is
+formatted. There is also a special *passthrough* vertex shader which can be
+used which just passes the vec4 input location 0 through to the `gl_Position`.
+The shader format is specified in the header after the word `shader`. The
+default is `GLSL`, SPIRV-ASM is specified as `spirv` and SPIRV-Hex as
+`spirv hex`.
+
+The shaders accepted are:
+ * compute
+ * fragment
+ * geometry
+ * tessellation control
+ * tessellation evaulation
+ * vertex
+
+#### Shader examples
+```
+[fragment shader]
+#version 430
+
+layout(location = 0) out vec4 color_out;
+
+void main() {
+  color_out = vec4(1, 2, 3, 4);
+}
+
+```
+
+Other example shader header lines are:
+ * `[fragment shader spirv hex]` -- a hex encoded SPIRV binary fragment shader
+ * `[tessellation evaluation shader spirv]` -- a spirv-asm tessellation evaluation shader
+ * `[vertex shader passthrough]`
+
+## Vertex Data
+The `vertex data` section provides vertex attributes and data for `draw array`
+commands. The data is formated with a header row followed by data rows.
+
+The headers can be provided in one of two forms. The first,
+`attribute_location/format` where `attribute_location` is the location of the
+attribute to be bound. The format is one of the *Image Formats* listed below.
+The second, `attribute_location/gl_type/glsl_type`. The `gl_type` is one of
+the types listed in the *GL Types* section below. The `glsl_type` is one listed
+in the *GLSL Types* section below.
+
+#### Vertex Data example
+```
+[vertex data]
+0/R32G32B32_SFLOAT  1/R8G8B8_UNORM
+-1    -1 0.25       255 0 0  # ending comment
+# Another Row
+0.25  -1 0.25       255 0 255
+```
+
+## Indices
+The `indices` section contains the list of indices to use along with the
+provided `vertex data`. The `indices` are used if the `indexed` option is
+provided to the `draw arrays` command. The indices themselves are a list of
+integer indexes to use.
+
+#### Indices Example
+```
+[indices]
+# comment line
+1 2 3   4 5 6
+# another comment
+7 8 9  10 11 12
+```
+
+## Test
+The test section contains a list of commands which can be executed to perform
+the actual testing. The commands range from setting up pipeline parameters,
+executing compute shaders and probing buffers to verify results.
+
+
+### Draw Rect
+ * `draw rect [ortho] [patch] _x_ _y_ _width_ _height_`
+
+The `draw rect` command draws a rectangle at the given coordinates. The vertices
+are uploaded at location 0 as a `vec3`. The `ortho` modifier scales the
+coordinates to be in the range of [-1,1] instead of [0,window size]. The `patch`
+modifier sets the draw call to use a given topology. Accepted possible
+topology value are listed in *Topologies*. The patch size will be set to 4.
+
+
+### Draw Arrays
+ * `draw arrays [indexed] [instanced] _topology_ _first_vertex_ _vertex_count_ [instance_count]`
+
+The `draw arrays` command uses data from the `vertex data` section when executing
+the draw command. The `topology` is from the *Topologies* list. If the `indexed`
+modifier is provided then the `indices` section data will be used as well.
+
+
+### Compute
+ * `compute _x_ _y_ _z_`
+
+Executes the compute shader with the given `x`, `y`, and `z` parameters.
+
+
+### Shader Entry Point
+ * `_stage_ entrypoint _name_`
+
+Sets the `stage` shader to use the entry point of `name` for subsequent
+executions.
+
+
+### Probe all
+ * `probe all (rgb|rgba) _r_ _g_ _b_ [_a_]
+
+Probes the entire window to verify all pixels are of color r,g,b and optionally
+a. If `rgba` is specified then the `a` parameter is required. If `rgb` is
+specified then the `a` parameter is dis-allowed.
+
+
+### Probe
+ * `[relative] probe [rect] (rgb|rgba) (_x_, _y_[, _width_, _height_]) (_r_, _g_, _b_[, _a_])`
+
+Probes a portion of the window to verify the pixes are of color r,g,b and
+optionally a. If `rgba` is specifed then the `a` parameter is required. If
+`rgb` is specified then the `a` parameter is dis-allowed. If `rect` is specified
+then `width` and `height` are required. If `rect` is not specified then `width`
+and `height` are dis-allowed and a value of 1 will be used for each parameter.
+If the `relative` parameter is provided the coordinates are normalized to
+be in the range [0.0, 1.0].
+
+
+### Probe SSBO
+* `probe ssbo _type_ _binding_ _offset_ _comparison_ _values_+`
+
+Probes the value in the storage buffer at `binding` and `offset` within that
+binding. The `type` is the data type to be probed as seen in the *Data Types*
+section below.
+
+The comparison operators are:
+ * `==`   (equal)
+ * `!=`   (not equal)
+ * `<`    (less than)
+ * '>'    (greater than)
+ * `<=`   (less or equal)
+ * `>=`   (greater or equal)
+ * `~=`   (fuzzy equal, for floating point comparisons using `tolerances`)
+
+The `values` provided must be a non-zero multiple of the `type`.
+
+
+### Uniform
+ * `uniform _type_ _offset _values_+`
+
+Sets the push constants at `offset`. The `type` is from the *Data Types*
+section below. The `values` must be a non-zero multiple of the requested
+`type`.
+
+
+### Unifom UBO
+ * `uniform ubo _binding_ _type_ _offset_ _values_+`
+
+Sets the values in the uniform buffer at `binding` and `offset`. The `type`
+is from the *Data Types* section below. The `values` must be a non-zero
+multiple of the requested `type`.
+
+
+### SSBO size
+ * `ssbo _binding_ _size_`
+
+Sets the size of the SSBO at `binding` to `size`.
+
+
+### SSBO subdata
+ * `ssbo _binding_ subdata _type_ _offset_ _values_+`
+
+Sets the value of the buffer at `binding` and `offset`. The `type` is from the
+*Data Types* section below. The `values` must be a non-zero multiple of the
+requested `type`.
+
+
+### Patch Parameters
+ * `patch parameter vertices _count_`
+
+Sets the number of control points for tessellation patches to `count`. Defaults
+to 3.
+
+
+### Tolerance
+ * `tolerance tolerance0 [tolerance1 tolerance2 tolerance3]
+
+The `tolerance` command sets the amount of fuzzyness used when using the `~=`
+comparator. If a single tolerance value is set it is used for every comparison.
+If all four values are set then each `vecN` command will use the first `N`
+tolerance values. Each column of a `matMxN` will also use the first `N`
+tolerances. A tolerance maybe either a number or a percentage `0.01%`.
+
+
+### Clear Color
+ * `clear color _r_ _g_ _b_ _a_`
+
+Sets the clear color. Defaults to (0, 0, 0, 0).
+
+
+### Clear Depth
+ * `clear depth _value_`
+
+Sets the depth clear value. Defaults to 1.0.
+
+
+### Clear Stencil
+ * `clear stencil _value_`
+
+Sets the stencil clear value. Defaults to 0.0.
+
+
+### Clear
+ * `clear`
+
+Clears the framebuffer.
+
+### Pipeline Configuration
+There are a number of pipeline flags which can be set to alter execution. Each
+draw call uses the pipeline configuration that was specified prior to the draw
+call.
+
+The pipeline commands with their accepted data are:
+ * `primitiveRestartEnable <bool>`
+ * `depthClampEnable <bool>`
+ * `rasterizerDiscardEnable <bool>`
+ * `depthBiasEnable <bool>`
+ * `logicOpEnable <bool>`
+ * `blendEnable <bool>`
+ * `depthTestEnable <bool>`
+ * `depthWriteEnable <bool>`
+ * `depthBoundsTestEnable <bool>`
+ * `stencilTestEnable <bool>`
+ * `topology <VkPrimitiveTopology>`
+ * `polygonMode <VkPolygonMode>`
+ * `logicOp <VkLogicOp>`
+ * `frontFace <VkFrontFace>`
+ * `cullMode <VkCullMode>`
+ * `depthBiasConstantFactor <float>`
+ * `depthBiasClamp <float>`
+ * `depthBiasSlopeFactor <float>`
+ * `lineWidth <float>`
+ * `minDepthBounds <float>`
+ * `maxDepthBounds <float>`
+ * `srcColorBlendFactor <VkBlendFactor>`
+ * `dstColorBlendFactor <VkBlendFactor>`
+ * `srcAlphaBlendFactor <VkBlendFactor>`
+ * `dstAlphaBlendFactor <VkBlendFactor>`
+ * `colorBlendOp <VkBlendOp>`
+ * `alphaBlendOp <VkBlendOp>`
+ * `depthCompareOp <VkCompareOp>`
+ * `front.compareOp <VkCompareOp>`
+ * `back.compareOp <VkCompareOp>`
+ * `front.failOp <VkStencilOp>`
+ * `front.passOp <VkStencilOp>`
+ * `front.depthFailOp <VkStencilOp>`
+ * `back.failOp <VkStencilOp>`
+ * `back.passOp <VkStencilOp>`
+ * `back.depthFailOp <VkStencilOp>`
+ * `front.reference <uint32_t>`
+ * `back.reference <uint32_t>`
+ * `colorWriteMask <VkColorComponent bitmask>`
+
+#### Test Example
+```
+[test]
+clear color 1 0.4 0.5 0.2
+clear
+relative probe rect rgba (0.0, 0.0, 1.0, 1.0) (1.0, 0.4, 0.5, 0.2)
+```
+
+### Data Types
+ * int
+ * uint
+ * int8_t
+ * uint8_t
+ * int16_t
+ * uint16_t
+ * int64_t
+ * uint64_t
+ * float
+ * double
+ * vec
+ * vec[234]
+ * dvec
+ * dvec[234]
+ * ivec
+ * ivec[234]
+ * uvec
+ * uvec[234]
+ * i8vec
+ * i8vec[234]
+ * u8vec
+ * u8vec[234]
+ * i16vec
+ * i16vec[234]
+ * u16vec
+ * u16vec[234]
+ * i64vec
+ * i64vec[234]
+ * u64vec
+ * u64vec[234]
+ * mat
+ * mat[234]x[234]
+ * dmat
+ * dmat[234]x[234]
+
+### Topologies
+ * PATCH_LIST
+ * POINT_LIST
+ * GL_LINE_STRIP_ADJACENCY
+ * GL_LINE_STRIP
+ * GL_LINES
+ * GL_LINES_ADJACENCY
+ * GL_PATCHES
+ * GL_POINTS
+ * GL_TRIANGLE_STRIP
+ * GL_TRIANGLE_FAN
+ * GL_TRIANGLES
+ * GL_TRIANGLES_ADJACENCY
+ * GL_TRIANGLE_STRIP_ADJACENCY
+ * LINE_LIST
+ * LINE_LIST_WITH_ADJACENCY
+ * LINE_STRIP
+ * LINE_STRIP_WITH_ADJACENCY
+ * TRIANGLE_FAN
+ * TRIANGLE_LIST
+ * TRIANGLE_LIST_WITH_ADJACENCY
+ * TRIANGLE_STRIP
+ * TRIANGLE_STRIP_WITH_ADJACENCY
+
+
+### GL Types
+ * byte
+ * ubyte
+ * short
+ * ushort
+ * int
+ * uint
+ * half
+ * float
+ * double
+
+### GLSL Types
+ * int
+ * uint
+ * float
+ * double
+ * vec
+ * vec2
+ * vec3
+ * vec4
+ * dvec
+ * dvec2
+ * dvec3
+ * dvec4
+ * uvec
+ * uvec2
+ * uvec3
+ * uvec4
+ * ivec
+ * ivec2
+ * ivec3
+ * ivec4
+
+### Available Require Features
+  * robustBufferAccess
+  * fullDrawIndexUint32
+  * imageCubeArray
+  * independentBlend
+  * geometryShader
+  * tessellationShader
+  * sampleRateShading
+  * dualSrcBlend
+  * logicOp
+  * multiDrawIndirect
+  * drawIndirectFirstInstance
+  * depthClamp
+  * depthBiasClamp
+  * fillModeNonSolid
+  * depthBounds
+  * wideLines
+  * largePoints
+  * alphaToOne
+  * multiViewport
+  * samplerAnisotropy
+  * textureCompressionETC2
+  * textureCompressionASTC_LDR
+  * textureCompressionBC
+  * occlusionQueryPrecise
+  * pipelineStatisticsQuery
+  * vertexPipelineStoresAndAtomics
+  * fragmentStoresAndAtomics
+  * shaderTessellationAndGeometryPointSize
+  * shaderImageGatherExtended
+  * shaderStorageImageExtendedFormats
+  * shaderStorageImageMultisample
+  * shaderStorageImageReadWithoutFormat
+  * shaderStorageImageWriteWithoutFormat
+  * shaderUniformBufferArrayDynamicIndexing
+  * shaderSampledImageArrayDynamicIndexing
+  * shaderStorageBufferArrayDynamicIndexing
+  * shaderStorageImageArrayDynamicIndexing
+  * shaderClipDistance
+  * shaderCullDistance
+  * shaderFloat64
+  * shaderInt64
+  * shaderInt16
+  * shaderResourceResidency
+  * shaderResourceMinLod
+  * sparseBinding
+  * sparseResidencyBuffer
+  * sparseResidencyImage2D
+  * sparseResidencyImage3D
+  * sparseResidency2Samples
+  * sparseResidency4Samples
+  * sparseResidency8Samples
+  * sparseResidency16Samples
+  * sparseResidencyAliased
+  * variableMultisampleRate
+  * inheritedQueries
+
+### Image Formats
+  * A1R5G5B5_UNORM_PACK16
+  * A2B10G10R10_SINT_PACK32
+  * A2B10G10R10_SNORM_PACK32
+  * A2B10G10R10_SSCALED_PACK32
+  * A2B10G10R10_UINT_PACK32
+  * A2B10G10R10_UNORM_PACK32
+  * A2B10G10R10_USCALED_PACK32
+  * A2R10G10B10_SINT_PACK32
+  * A2R10G10B10_SNORM_PACK32
+  * A2R10G10B10_SSCALED_PACK32
+  * A2R10G10B10_UINT_PACK32
+  * A2R10G10B10_UNORM_PACK32
+  * A2R10G10B10_USCALED_PACK32
+  * A8B8G8R8_SINT_PACK32
+  * A8B8G8R8_SNORM_PACK32
+  * A8B8G8R8_SRGB_PACK32
+  * A8B8G8R8_SSCALED_PACK32
+  * A8B8G8R8_UINT_PACK32
+  * A8B8G8R8_UNORM_PACK32
+  * A8B8G8R8_USCALED_PACK32
+  * B10G11R11_UFLOAT_PACK32
+  * B4G4R4A4_UNORM_PACK16
+  * B5G5R5A1_UNORM_PACK16
+  * B5G6R5_UNORM_PACK16
+  * B8G8R8A8_SINT
+  * B8G8R8A8_SNORM
+  * B8G8R8A8_SRGB
+  * B8G8R8A8_SSCALED
+  * B8G8R8A8_UINT
+  * B8G8R8A8_UNORM
+  * B8G8R8A8_USCALED
+  * B8G8R8_SINT
+  * B8G8R8_SNORM
+  * B8G8R8_SRGB
+  * B8G8R8_SSCALED
+  * B8G8R8_UINT
+  * B8G8R8_UNORM
+  * B8G8R8_USCALED
+  * D16_UNORM
+  * D16_UNORM_S8_UINT
+  * D24_UNORM_S8_UINT
+  * D32_SFLOAT
+  * D32_SFLOAT_S8_UINT
+  * R16G16B16A16_SFLOAT
+  * R16G16B16A16_SINT
+  * R16G16B16A16_SNORM
+  * R16G16B16A16_SSCALED
+  * R16G16B16A16_UINT
+  * R16G16B16A16_UNORM
+  * R16G16B16A16_USCALED
+  * R16G16B16_SFLOAT
+  * R16G16B16_SINT
+  * R16G16B16_SNORM
+  * R16G16B16_SSCALED
+  * R16G16B16_UINT
+  * R16G16B16_UNORM
+  * R16G16B16_USCALED
+  * R16G16_SFLOAT
+  * R16G16_SINT
+  * R16G16_SNORM
+  * R16G16_SSCALED
+  * R16G16_UINT
+  * R16G16_UNORM
+  * R16G16_USCALED
+  * R16_SFLOAT
+  * R16_SINT
+  * R16_SNORM
+  * R16_SSCALED
+  * R16_UINT
+  * R16_UNORM
+  * R16_USCALED
+  * R32G32B32A32_SFLOAT
+  * R32G32B32A32_SINT
+  * R32G32B32A32_UINT
+  * R32G32B32_SFLOAT
+  * R32G32B32_SINT
+  * R32G32B32_UINT
+  * R32G32_SFLOAT
+  * R32G32_SINT
+  * R32G32_UINT
+  * R32_SFLOAT
+  * R32_SINT
+  * R32_UINT
+  * R4G4B4A4_UNORM_PACK16
+  * R4G4_UNORM_PACK8
+  * R5G5B5A1_UNORM_PACK16
+  * R5G6B5_UNORM_PACK16
+  * R64G64B64A64_SFLOAT
+  * R64G64B64A64_SINT
+  * R64G64B64A64_UINT
+  * R64G64B64_SFLOAT
+  * R64G64B64_SINT
+  * R64G64B64_UINT
+  * R64G64_SFLOAT
+  * R64G64_SINT
+  * R64G64_UINT
+  * R64_SFLOAT
+  * R64_SINT
+  * R64_UINT
+  * R8G8B8A8_SINT
+  * R8G8B8A8_SNORM
+  * R8G8B8A8_SRGB
+  * R8G8B8A8_SSCALED
+  * R8G8B8A8_UINT
+  * R8G8B8A8_UNORM
+  * R8G8B8A8_USCALED
+  * R8G8B8_SINT
+  * R8G8B8_SNORM
+  * R8G8B8_SRGB
+  * R8G8B8_SSCALED
+  * R8G8B8_UINT
+  * R8G8B8_UNORM
+  * R8G8B8_USCALED
+  * R8G8_SINT
+  * R8G8_SNORM
+  * R8G8_SRGB
+  * R8G8_SSCALED
+  * R8G8_UINT
+  * R8G8_UNORM
+  * R8G8_USCALED
+  * R8_SINT
+  * R8_SNORM
+  * R8_SRGB
+  * R8_SSCALED
+  * R8_UINT
+  * R8_UNORM
+  * R8_USCALED
+  * S8_UINT
+  * X8_D24_UNORM_PACK32
+
+1- https://github.com/Igalia/vkrunner/blob/d817f8b186cccebed89471580a685dc80a330946/README.md
diff --git a/docs/vulkan_resource_and_descriptor.md b/docs/vulkan_resource_and_descriptor.md
new file mode 100644
index 0000000..a8439da
--- /dev/null
+++ b/docs/vulkan_resource_and_descriptor.md
@@ -0,0 +1,66 @@
+# Classes for Vulkan resources and descriptors
+ * DRAFT
+
+Vulkan has many resource and descriptor types.
+Since it is complicated to manage them e.g.,
+create/allocate/map/read/write/destory, we create several classes to
+provide an abstraction. This document briefly explains those classes.
+
+
+### Resource class
+Represents a main resource i.e., VkBuffer or VkImage (See
+[Resources in Vulkan spec](
+https://www.khronos.org/registry/vulkan/specs/1.1/html/vkspec.html#resources))
+in GPU device and an additional VkBuffer to allow read/write to the
+main resource from CPU.
+
+If the main resource is accessible from CPU, the additional
+VkBuffer is not needed and it will be `VK_NULL_HANDLE`.
+Otherwise, the additional VkBuffer has the same size with the main
+resource and we must copy the main resource to the VkBuffer or
+copy the VkBuffer to the main resource when reading from/write to
+the main resource.
+
+The Resource class has Buffer and Image sub-classes.
+
+#### Buffer class
+Abstracts VkBuffer and creates/allocates/maps/destorys
+VkBuffer and VkBufferView resources.
+
+#### Image class
+Abstracts VkImage and creates/allocates/maps/destorys
+VkImage and VkImageView resources.
+
+
+### Sampler class
+ * TODO: Not implementated yet
+ * Represent [VkSampler](
+   https://www.khronos.org/registry/vulkan/specs/1.1/html/vkspec.html#samplers)
+
+
+### Descriptor class
+Represents [Descriptor Types](
+https://www.khronos.org/registry/vulkan/specs/1.1/html/vkspec.html#descriptorsets-types).
+There are 11 Descriptor Types and they need different resources.
+For example, a Combined Image Sampler needs both Image and
+Sampler objects while Storage Buffer needs only a Buffer object.
+
+* TODO: Describe 11 sub-classes of Descriptor for those Descriptor Types.
+
+
+### FrameBuffer class
+Abstracts [VkFrameBuffer](
+https://www.khronos.org/registry/vulkan/specs/1.1/html/vkspec.html#_framebuffers)
+for attachments and an Image for the FrameBuffer.
+The usage of the Image is `VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT`.
+
+
+### VertexBuffer class
+Manages vertices data and a Buffer whose usage is
+`VK_BUFFER_USAGE_VERTEX_BUFFER_BIT`.
+
+
+### IndexBuffer class
+ * TODO: Not implementated yet
+ * Manages indices data and a Buffer whose usage is
+   `VK_BUFFER_USAGE_INDEX_BUFFER_BIT`.
diff --git a/include/amber/amber.h b/include/amber/amber.h
new file mode 100644
index 0000000..f723c1b
--- /dev/null
+++ b/include/amber/amber.h
@@ -0,0 +1,44 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef AMBER_AMBER_H_
+#define AMBER_AMBER_H_
+
+#include "amber/result.h"
+
+#include <string>
+
+namespace amber {
+
+enum class EngineType : uint8_t {
+  kVulkan = 0,
+};
+
+struct Options {
+  EngineType engine = EngineType::kVulkan;
+  void* default_device = nullptr;
+  bool parse_only = false;
+};
+
+class Amber {
+ public:
+  Amber();
+  ~Amber();
+
+  amber::Result Execute(const std::string& data, const Options& opts);
+};
+
+}  // namespace amber
+
+#endif  // AMBER_AMBER_H_
diff --git a/include/amber/result.h b/include/amber/result.h
new file mode 100644
index 0000000..ec57289
--- /dev/null
+++ b/include/amber/result.h
@@ -0,0 +1,50 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef AMBER_RESULT_H_
+#define AMBER_RESULT_H_
+
+#include <string>
+#include <vector>
+
+namespace amber {
+
+class Result {
+ public:
+  Result();
+  Result(const std::string& err);
+  Result(const Result&);
+  ~Result();
+
+  Result& operator=(const Result&);
+
+  bool IsSuccess() const { return succeeded_; }
+  const std::string& Error() const { return error_; }
+
+  void SetImageData(const std::vector<uint8_t>& data) { image_data_ = data; }
+  const std::vector<uint8_t>& ImageData() const { return image_data_; }
+
+  void SetBufferData(const std::vector<uint8_t>& data) { buffer_data_ = data; }
+  const std::vector<uint8_t>& BufferData() const { return buffer_data_; }
+
+ private:
+  bool succeeded_;
+  std::string error_;
+  std::vector<uint8_t> image_data_;
+  std::vector<uint8_t> buffer_data_;
+};
+
+}  // namespace amber
+
+#endif  // AMBER_RESULT_H_
diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt
new file mode 100644
index 0000000..c5142c5
--- /dev/null
+++ b/samples/CMakeLists.txt
@@ -0,0 +1,40 @@
+# Copyright 2018 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 implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+include_directories("${PROJECT_SOURCE_DIR}/include")
+
+set(AMBER_SOURCES
+    amber.cc
+    ${CMAKE_BINARY_DIR}/src/build-versions.h
+)
+
+add_executable(amber ${AMBER_SOURCES})
+target_include_directories(amber PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/..")
+set_target_properties(amber PROPERTIES OUTPUT_NAME "amber")
+target_link_libraries(amber libamber)
+
+add_custom_command(
+    OUTPUT ${CMAKE_BINARY_DIR}/src/build-versions.h
+    COMMAND
+      ${PYTHON_EXE}
+        ${CMAKE_CURRENT_SOURCE_DIR}/../tools/update_build_version.py
+        ${CMAKE_BINARY_DIR}
+        ${CMAKE_CURRENT_SOURCE_DIR}
+        ${spirv-tools_SOURCE_DIR}
+        ${CMAKE_CURRENT_SOURCE_DIR}/../third_party/spirv-headers
+        ${glslang_SOURCE_DIR}
+        ${shaderc_SOURCE_DIR}
+    WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/.."
+    COMMENT "Update build-versions.h in the build directory"
+)
diff --git a/samples/amber.cc b/samples/amber.cc
new file mode 100644
index 0000000..9d76f5f
--- /dev/null
+++ b/samples/amber.cc
@@ -0,0 +1,177 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "amber/amber.h"
+
+#include <cassert>
+#include <cstdlib>
+#include <iostream>
+#include <vector>
+
+#include "src/build-versions.h"
+
+namespace {
+
+struct Options {
+  std::string input_filename;
+
+  std::string image_filename;
+  std::string buffer_filename;
+  long buffer_binding_index = 0;
+  bool parse_only = false;
+  bool show_help = false;
+  bool show_version_info = false;
+};
+
+const char kUsage[] = R"(Usage: amber [options] SCRIPT
+
+ options:
+  -p             -- Parse input files only; Don't execute
+  -i <filename>  -- Write rendering to <filename> as a PPM image.
+  -b <filename>  -- Write contents of a UBO or SSBO to <filename>.
+  -B <buffer>    -- Index of buffer to write. Defaults buffer 0.
+  -V, --version  -- Output version information for Amber and libraries.
+  -h             -- This help text.
+)";
+
+bool ParseArgs(const std::vector<std::string>& args, Options* opts) {
+  for (size_t i = 1; i < args.size(); ++i) {
+    const std::string& arg = args[i];
+    if (arg == "-i") {
+      ++i;
+      if (i >= args.size()) {
+        std::cerr << "Missing value for -i argument." << std::endl;
+        return false;
+      }
+      opts->image_filename = args[i];
+
+    } else if (arg == "-b") {
+      ++i;
+      if (i >= args.size()) {
+        std::cerr << "Missing value for -b argument." << std::endl;
+        return false;
+      }
+      opts->buffer_filename = args[i];
+
+    } else if (arg == "-B") {
+      ++i;
+      if (i >= args.size()) {
+        std::cerr << "Missing value for -B argument." << std::endl;
+        return false;
+      }
+      opts->buffer_binding_index = strtol(args[i].c_str(), nullptr, 10);
+
+      if (opts->buffer_binding_index < 0) {
+        std::cerr << "Invalid value for -B, must be 0 or greater." << std::endl;
+        return false;
+      }
+
+    } else if (arg == "-h" || arg == "--help") {
+      opts->show_help = true;
+    } else if (arg == "-V" || arg == "--version") {
+      opts->show_version_info = true;
+    } else if (arg == "-p") {
+      opts->parse_only = true;
+    } else {
+      opts->input_filename = args[i];
+    }
+  }
+
+  return true;
+}
+
+std::string ReadFile(const std::string& input_file) {
+  FILE* file = fopen(input_file.c_str(), "rb");
+  if (!file) {
+    std::cerr << "Failed to open " << input_file << std::endl;
+    return {};
+  }
+
+  fseek(file, 0, SEEK_END);
+  long tell_file_size = ftell(file);
+  if (tell_file_size <= 0) {
+    std::cerr << "Input file of incorrect size: " << input_file << std::endl;
+    return {};
+  }
+  fseek(file, 0, SEEK_SET);
+
+  size_t file_size = static_cast<size_t>(tell_file_size);
+
+  std::vector<char> data;
+  data.resize(file_size);
+
+  size_t bytes_read = fread(data.data(), sizeof(char), file_size, file);
+  fclose(file);
+  if (bytes_read != file_size) {
+    std::cerr << "Failed to read " << input_file << std::endl;
+    return {};
+  }
+
+  return std::string(data.begin(), data.end());
+}
+
+}  // namespace
+
+int main(int argc, const char** argv) {
+  std::vector<std::string> args(argv, argv + argc);
+  Options options;
+
+  if (!ParseArgs(args, &options)) {
+    std::cerr << "Failed to parse arguments." << std::endl;
+    return 1;
+  }
+
+  if (options.show_version_info) {
+    std::cout << "Amber        : " << AMBER_VERSION << std::endl;
+    std::cout << "SPIRV-Tools  : " << SPIRV_TOOLS_VERSION << std::endl;
+    std::cout << "SPIRV-Headers: " << SPIRV_HEADERS_VERSION << std::endl;
+    std::cout << "GLSLang      : " << GLSLANG_VERSION << std::endl;
+    std::cout << "Shaderc      : " << SHADERC_VERSION << std::endl;
+  }
+
+  if (options.show_help) {
+    std::cout << kUsage << std::endl;
+    return 0;
+  }
+
+  if (options.input_filename.empty()) {
+    std::cerr << "Input file must be provided." << std::endl;
+    return 2;
+  }
+
+  auto data = ReadFile(options.input_filename);
+  if (data.empty())
+    return 1;
+
+  amber::Amber vk;
+  amber::Options amber_options;
+  amber_options.parse_only = options.parse_only;
+  amber::Result result = vk.Execute(data, amber_options);
+  if (!result.IsSuccess()) {
+    std::cerr << result.Error() << std::endl;
+    return 1;
+  }
+
+  if (!options.buffer_filename.empty()) {
+    // TOOD(dsinclair): Write buffer file
+    assert(false);
+  }
+
+  if (!options.image_filename.empty()) {
+    // TODO(dsinclair): Write image file
+    assert(false);
+  }
+
+  return 0;
+}
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 0000000..50531e8
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,84 @@
+# Copyright 2018 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 implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set(AMBER_SOURCES
+    amber.cc
+    amber_impl.cc
+    amberscript/executor.cc
+    amberscript/parser.cc
+    amberscript/pipeline.cc
+    amberscript/script.cc
+    amberscript/shader.cc
+    command.cc
+    command_data.cc
+    datum_type.cc
+    engine.cc
+    executor.cc
+    format.cc
+    parser.cc
+    pipeline_data.cc
+    result.cc
+    script.cc
+    shader_compiler.cc
+    tokenizer.cc
+    value.cc
+    vkscript/command_parser.cc
+    vkscript/datum_type_parser.cc
+    vkscript/executor.cc
+    vkscript/format_parser.cc
+    vkscript/nodes.cc
+    vkscript/parser.cc
+    vkscript/script.cc
+    vkscript/section_parser.cc
+)
+
+add_library(libamber ${AMBER_SOURCES})
+amber_default_compile_options(libamber)
+set_target_properties(libamber PROPERTIES OUTPUT_NAME "amber")
+# TODO(dsinclair): Remove pthread when building on windows.
+target_link_libraries(libamber SPIRV-Tools shaderc SPIRV pthread)
+
+if (${Vulkan_FOUND})
+  target_link_libraries(libamber libamberenginevulkan)
+  add_subdirectory(vulkan)
+endif()
+
+set(TEST_SRCS
+    amberscript/parser_test.cc
+    amberscript/pipeline_test.cc
+    command_data_test.cc
+    result_test.cc
+    shader_compiler_test.cc
+    tokenizer_test.cc
+    vkscript/command_parser_test.cc
+    vkscript/datum_type_parser_test.cc
+    vkscript/executor_test.cc
+    vkscript/format_parser_test.cc
+    vkscript/parser_test.cc
+    vkscript/section_parser_test.cc
+)
+
+if (${Vulkan_FOUND})
+    list(APPEND TEST_SRCS vulkan/bit_copy_test.cc)
+endif()
+
+add_executable(amber_unittests ${TEST_SRCS})
+target_compile_options(amber_unittests PRIVATE
+    -Wno-global-constructors)
+
+target_include_directories(amber_unittests PRIVATE
+    ${gtest_SOURCE_DIR}/include)
+target_link_libraries(amber_unittests libamber gtest_main)
+amber_default_compile_options(amber_unittests)
+add_test(NAME amber_unittests COMMAND amber_unittests)
diff --git a/src/amber.cc b/src/amber.cc
new file mode 100644
index 0000000..dda019e
--- /dev/null
+++ b/src/amber.cc
@@ -0,0 +1,30 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "amber/amber.h"
+
+#include "src/amber_impl.h"
+
+namespace amber {
+
+Amber::Amber() = default;
+
+Amber::~Amber() = default;
+
+amber::Result Amber::Execute(const std::string& input, const Options& opts) {
+  AmberImpl impl;
+  return impl.Execute(input, opts);
+}
+
+}  // namespace amber
diff --git a/src/amber_impl.cc b/src/amber_impl.cc
new file mode 100644
index 0000000..93e61a7
--- /dev/null
+++ b/src/amber_impl.cc
@@ -0,0 +1,70 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/amber_impl.h"
+
+#include "src/amberscript/executor.h"
+#include "src/amberscript/parser.h"
+#include "src/engine.h"
+#include "src/executor.h"
+#include "src/make_unique.h"
+#include "src/parser.h"
+#include "src/vkscript/executor.h"
+#include "src/vkscript/parser.h"
+
+namespace amber {
+
+AmberImpl::AmberImpl() = default;
+
+AmberImpl::~AmberImpl() = default;
+
+amber::Result AmberImpl::Execute(const std::string& input,
+                                 const Options& opts) {
+  std::unique_ptr<Parser> parser;
+  std::unique_ptr<Executor> executor;
+  if (input.substr(0, 7) == "#!amber") {
+    parser = MakeUnique<amberscript::Parser>();
+    executor = MakeUnique<amberscript::Executor>();
+  } else {
+    parser = MakeUnique<vkscript::Parser>();
+    executor = MakeUnique<vkscript::Executor>();
+  }
+
+  Result r = parser->Parse(input);
+  if (!r.IsSuccess())
+    return r;
+
+  if (opts.parse_only)
+    return {};
+
+  auto engine = Engine::Create(opts.engine);
+  if (!engine)
+    return Result("Failed to create engine");
+
+  if (opts.default_device)
+    r = engine->InitializeWithDevice(opts.default_device);
+  else
+    r = engine->Initialize();
+
+  if (!r.IsSuccess())
+    return r;
+
+  r = executor->Execute(engine.get(), parser->GetScript());
+  if (!r.IsSuccess())
+    return r;
+
+  return engine->Shutdown();
+}
+
+}  // namespace amber
diff --git a/src/amber_impl.h b/src/amber_impl.h
new file mode 100644
index 0000000..5b962a8
--- /dev/null
+++ b/src/amber_impl.h
@@ -0,0 +1,33 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_AMBER_IMPL_H_
+#define SRC_AMBER_IMPL_H_
+
+#include "amber/amber.h"
+#include "amber/result.h"
+
+namespace amber {
+
+class AmberImpl {
+ public:
+  AmberImpl();
+  ~AmberImpl();
+
+  Result Execute(const std::string& data, const Options& opts);
+};
+
+}  // namespace amber
+
+#endif  // SRC_AMBER_IMPL_H_
diff --git a/src/amberscript/executor.cc b/src/amberscript/executor.cc
new file mode 100644
index 0000000..dba14e3
--- /dev/null
+++ b/src/amberscript/executor.cc
@@ -0,0 +1,34 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/amberscript/executor.h"
+
+namespace amber {
+namespace amberscript {
+
+Executor::Executor() : amber::Executor() {}
+
+Executor::~Executor() = default;
+
+Result Executor::Execute(Engine*, const amber::Script* src_script) {
+  if (!src_script->IsAmberScript())
+    return Result("AmberScript executor called with non-amber script source");
+
+  // const amberscript::Script* script = ToAmberScript(src_script);
+
+  return {};
+}
+
+}  // namespace amberscript
+}  // namespace amber
diff --git a/src/amberscript/executor.h b/src/amberscript/executor.h
new file mode 100644
index 0000000..27be740
--- /dev/null
+++ b/src/amberscript/executor.h
@@ -0,0 +1,37 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_AMBERSCRIPT_EXECUTOR_H_
+#define SRC_AMBERSCRIPT_EXECUTOR_H_
+
+#include "amber/result.h"
+#include "src/engine.h"
+#include "src/executor.h"
+#include "src/script.h"
+
+namespace amber {
+namespace amberscript {
+
+class Executor : public amber::Executor {
+ public:
+  Executor();
+  ~Executor() override;
+
+  Result Execute(Engine*, const amber::Script*) override;
+};
+
+}  // namespace amberscript
+}  // namespace amber
+
+#endif  // SRC_AMBERSCRIPT_EXECUTOR_H_
diff --git a/src/amberscript/parser.cc b/src/amberscript/parser.cc
new file mode 100644
index 0000000..6535520
--- /dev/null
+++ b/src/amberscript/parser.cc
@@ -0,0 +1,309 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/amberscript/parser.h"
+
+#include <cassert>
+
+#include "src/make_unique.h"
+#include "src/tokenizer.h"
+
+namespace amber {
+namespace amberscript {
+
+Parser::Parser() : amber::Parser() {}
+
+Parser::~Parser() = default;
+
+std::string Parser::make_error(const std::string& err) {
+  return std::to_string(tokenizer_->GetCurrentLine()) + ": " + err;
+}
+
+Result Parser::Parse(const std::string& data) {
+  tokenizer_ = MakeUnique<Tokenizer>(data);
+
+  for (auto token = tokenizer_->NextToken(); !token->IsEOS();
+       token = tokenizer_->NextToken()) {
+    if (token->IsEOL())
+      continue;
+    if (!token->IsString())
+      return Result(make_error("expected string"));
+
+    Result r;
+    std::string tok = token->AsString();
+    if (tok == "SHADER") {
+      r = ParseShaderBlock();
+    } else if (tok == "PIPELINE") {
+      r = ParsePipelineBlock();
+    } else {
+      r = Result("unknown token: " + tok);
+    }
+    if (!r.IsSuccess())
+      return Result(make_error(r.Error()));
+  }
+  return {};
+}
+
+Result Parser::ToShaderType(const std::string& str, ShaderType* type) {
+  assert(type);
+
+  if (str == "vertex")
+    *type = ShaderType::kVertex;
+  else if (str == "fragment")
+    *type = ShaderType::kFragment;
+  else if (str == "geometry")
+    *type = ShaderType::kGeometry;
+  else if (str == "tessellation_evaluation")
+    *type = ShaderType::kTessellationEvaluation;
+  else if (str == "tessellation_control")
+    *type = ShaderType::kTessellationControl;
+  else if (str == "compute")
+    *type = ShaderType::kCompute;
+  else
+    return Result("unknown shader type: " + str);
+  return {};
+}
+
+Result Parser::ToShaderFormat(const std::string& str, ShaderFormat* fmt) {
+  assert(fmt);
+
+  if (str == "GLSL")
+    *fmt = ShaderFormat::kGlsl;
+  else if (str == "SPIRV-ASM")
+    *fmt = ShaderFormat::kSpirvAsm;
+  else if (str == "SPIRV-HEX")
+    *fmt = ShaderFormat::kSpirvHex;
+  else
+    return Result("unknown shader format: " + str);
+  return {};
+}
+
+Result Parser::ToPipelineType(const std::string& str, PipelineType* type) {
+  assert(type);
+
+  if (str == "compute")
+    *type = PipelineType::kCompute;
+  else if (str == "graphics")
+    *type = PipelineType::kGraphics;
+  else
+    return Result("unknown pipeline type: " + str);
+  return {};
+}
+
+Result Parser::ValidateEndOfStatement(const std::string& name) {
+  auto token = tokenizer_->NextToken();
+  if (token->IsEOL() || token->IsEOS())
+    return {};
+  return Result("extra parameters after " + name);
+}
+
+Result Parser::ParseShaderBlock() {
+  auto token = tokenizer_->NextToken();
+  if (!token->IsString())
+    return Result("invalid token when looking for shader type");
+
+  ShaderType type = ShaderType::kVertex;
+  Result r = ToShaderType(token->AsString(), &type);
+  if (!r.IsSuccess())
+    return r;
+
+  auto shader = MakeUnique<Shader>(type);
+
+  token = tokenizer_->NextToken();
+  if (!token->IsString())
+    return Result("invalid token when looking for shader name");
+
+  shader->SetName(token->AsString());
+
+  token = tokenizer_->NextToken();
+  if (!token->IsString())
+    return Result("invalid token when looking for shader format");
+
+  std::string fmt = token->AsString();
+  if (fmt == "PASSTHROUGH") {
+    if (type != ShaderType::kVertex) {
+      return Result(
+          "invalid shader type for PASSTHROUGH. Only vertex "
+          "PASSTHROUGH allowed");
+    }
+    shader->SetFormat(ShaderFormat::kSpirvAsm);
+    shader->SetData(kPassThroughShader);
+
+    r = script_.AddShader(std::move(shader));
+    if (!r.IsSuccess())
+      return r;
+
+    return ValidateEndOfStatement("SHADER PASSTHROUGH");
+  }
+
+  ShaderFormat format = ShaderFormat::kGlsl;
+  r = ToShaderFormat(fmt, &format);
+  if (!r.IsSuccess())
+    return r;
+
+  shader->SetFormat(format);
+
+  r = ValidateEndOfStatement("SHADER command");
+  if (!r.IsSuccess())
+    return r;
+
+  std::string data = tokenizer_->ExtractToNext("END");
+  if (data.empty())
+    return Result("SHADER must not be empty");
+
+  shader->SetData(data);
+
+  token = tokenizer_->NextToken();
+  if (!token->IsString() || token->AsString() != "END")
+    return Result("SHADER missing END command");
+
+  r = script_.AddShader(std::move(shader));
+  if (!r.IsSuccess())
+    return r;
+
+  return ValidateEndOfStatement("END");
+}
+
+Result Parser::ParsePipelineBlock() {
+  auto token = tokenizer_->NextToken();
+  if (!token->IsString())
+    return Result("invalid token when looking for pipeline type");
+
+  PipelineType type = PipelineType::kCompute;
+  Result r = ToPipelineType(token->AsString(), &type);
+  if (!r.IsSuccess())
+    return r;
+
+  auto pipeline = MakeUnique<Pipeline>(type);
+
+  token = tokenizer_->NextToken();
+  if (!token->IsString())
+    return Result("invalid token when looking for pipeline name");
+
+  pipeline->SetName(token->AsString());
+
+  r = ValidateEndOfStatement("PIPELINE command");
+  if (!r.IsSuccess())
+    return r;
+
+  for (token = tokenizer_->NextToken(); !token->IsEOS();
+       token = tokenizer_->NextToken()) {
+    if (token->IsEOL())
+      continue;
+    if (!token->IsString())
+      return Result("expected string");
+
+    std::string tok = token->AsString();
+    if (tok == "END") {
+      break;
+    } else if (tok == "ATTACH") {
+      r = ParsePipelineAttach(pipeline.get());
+    } else if (tok == "ENTRY_POINT") {
+      r = ParsePipelineEntryPoint(pipeline.get());
+    } else if (tok == "SHADER_OPTIMIZATION") {
+      r = ParsePipelineShaderOptimizations(pipeline.get());
+    } else {
+      r = Result("unknown token in pipeline block: " + tok);
+    }
+    if (!r.IsSuccess())
+      return r;
+  }
+
+  if (!token->IsString() || token->AsString() != "END")
+    return Result("PIPELINE missing END command");
+
+  r = pipeline->Validate();
+  if (!r.IsSuccess())
+    return r;
+
+  r = script_.AddPipeline(std::move(pipeline));
+  if (!r.IsSuccess())
+    return r;
+
+  return ValidateEndOfStatement("END");
+}
+
+Result Parser::ParsePipelineAttach(Pipeline* pipeline) {
+  auto token = tokenizer_->NextToken();
+  if (!token->IsString())
+    return Result("invalid token in ATTACH command");
+
+  auto* shader = script_.GetShader(token->AsString());
+  if (!shader)
+    return Result("unknown shader in ATTACH command");
+
+  Result r = pipeline->AddShader(shader);
+  if (!r.IsSuccess())
+    return r;
+
+  return ValidateEndOfStatement("ATTACH command");
+}
+
+Result Parser::ParsePipelineEntryPoint(Pipeline* pipeline) {
+  auto token = tokenizer_->NextToken();
+  if (!token->IsString())
+    return Result("missing shader name in ENTRY_POINT command");
+
+  auto* shader = script_.GetShader(token->AsString());
+  if (!shader)
+    return Result("unknown shader in ENTRY_POINT command");
+
+  token = tokenizer_->NextToken();
+  if (!token->IsString())
+    return Result("invalid value in ENTRY_POINT command");
+
+  Result r = pipeline->SetShaderEntryPoint(shader, token->AsString());
+  if (!r.IsSuccess())
+    return r;
+
+  return ValidateEndOfStatement("ENTRY_POINT command");
+}
+
+Result Parser::ParsePipelineShaderOptimizations(Pipeline* pipeline) {
+  auto token = tokenizer_->NextToken();
+  if (!token->IsString())
+    return Result("missing shader name in SHADER_OPTIMIZATION command");
+
+  auto* shader = script_.GetShader(token->AsString());
+  if (!shader)
+    return Result("unknown shader in SHADER_OPTIMIZATION command");
+
+  token = tokenizer_->NextToken();
+  if (!token->IsEOL())
+    return Result("extra parameters after SHADER_OPTIMIZATION command");
+
+  std::vector<std::string> optimizations;
+  while (true) {
+    token = tokenizer_->NextToken();
+    if (token->IsEOL())
+      continue;
+    if (token->IsEOS())
+      return Result("SHADER_OPTIMIZATION missing END command");
+    if (!token->IsString())
+      return Result("SHADER_OPTIMIZATION options must be strings");
+    if (token->AsString() == "END")
+      break;
+
+    optimizations.push_back(token->AsString());
+  }
+
+  Result r = pipeline->SetShaderOptimizations(shader, optimizations);
+  if (!r.IsSuccess())
+    return r;
+
+  return ValidateEndOfStatement("SHADER_OPTIMIZATION command");
+}
+
+}  // namespace amberscript
+}  // namespace amber
diff --git a/src/amberscript/parser.h b/src/amberscript/parser.h
new file mode 100644
index 0000000..5239c0f
--- /dev/null
+++ b/src/amberscript/parser.h
@@ -0,0 +1,61 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_AMBERSCRIPT_PARSER_H_
+#define SRC_AMBERSCRIPT_PARSER_H_
+
+#include <memory>
+#include <string>
+
+#include "amber/result.h"
+#include "src/amberscript/script.h"
+#include "src/parser.h"
+#include "src/script.h"
+
+namespace amber {
+
+class Tokenizer;
+
+namespace amberscript {
+
+class Parser : public amber::Parser {
+ public:
+  Parser();
+  ~Parser() override;
+
+  // amber::Parser
+  Result Parse(const std::string& data) override;
+  const amber::Script* GetScript() const override { return &script_; }
+
+ private:
+  std::string make_error(const std::string& err);
+  Result ToShaderType(const std::string& str, ShaderType* type);
+  Result ToShaderFormat(const std::string& str, ShaderFormat* fmt);
+  Result ToPipelineType(const std::string& str, PipelineType* type);
+  Result ValidateEndOfStatement(const std::string& name);
+
+  Result ParseShaderBlock();
+  Result ParsePipelineBlock();
+  Result ParsePipelineAttach(Pipeline*);
+  Result ParsePipelineEntryPoint(Pipeline*);
+  Result ParsePipelineShaderOptimizations(Pipeline*);
+
+  amberscript::Script script_;
+  std::unique_ptr<Tokenizer> tokenizer_;
+};
+
+}  // namespace amberscript
+}  // namespace amber
+
+#endif  // SRC_AMBERSCRIPT_PARSER_H_
diff --git a/src/amberscript/parser_test.cc b/src/amberscript/parser_test.cc
new file mode 100644
index 0000000..fafb261
--- /dev/null
+++ b/src/amberscript/parser_test.cc
@@ -0,0 +1,912 @@
+// Copyright 2018 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 "src/amberscript/parser.h"
+#include "gtest/gtest.h"
+
+namespace amber {
+namespace amberscript {
+namespace {
+
+struct NameData {
+  const char* name;
+};
+
+struct ShaderTypeData {
+  const char* name;
+  ShaderType type;
+};
+
+struct ShaderFormatData {
+  const char* name;
+  ShaderFormat format;
+};
+
+}  // namespace
+
+using AmberScriptParserTest = testing::Test;
+
+TEST_F(AmberScriptParserTest, EmptyInput) {
+  std::string in = "";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto script = parser.GetScript();
+  ASSERT_TRUE(script->IsAmberScript());
+}
+
+TEST_F(AmberScriptParserTest, InvalidStartToken) {
+  std::string in = R"(#!amber
+# Start comment
+1234)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("3: expected string", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, UnknownStartToken) {
+  std::string in = "INVALID token";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("1: unknown token: INVALID", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, ShaderPassThrough) {
+  std::string in = "SHADER vertex my_shader1 PASSTHROUGH";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto script = ToAmberScript(parser.GetScript());
+  const auto& shaders = script->GetShaders();
+  ASSERT_EQ(1U, shaders.size());
+
+  const auto* shader = shaders[0].get();
+  EXPECT_EQ("my_shader1", shader->GetName());
+  EXPECT_EQ(ShaderType::kVertex, shader->GetType());
+  EXPECT_EQ(ShaderFormat::kSpirvAsm, shader->GetFormat());
+  EXPECT_EQ(kPassThroughShader, shader->GetData());
+}
+
+TEST_F(AmberScriptParserTest, ShaderInvalidShaderTypeToken) {
+  std::string in = "SHADER 1234 my_shader PASSTHROUGH";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("1: invalid token when looking for shader type", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, ShaderInvalidShaderNameToken) {
+  std::string in = "SHADER vertex 12345 PASSTHROUGH";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("1: invalid token when looking for shader name", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, ShaderInvalidShaderFormatToken) {
+  std::string in = "SHADER vertex my_shader 1234";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("1: invalid token when looking for shader format", r.Error());
+}
+
+using AmberScriptParserShaderPassThroughTest = testing::TestWithParam<NameData>;
+TEST_P(AmberScriptParserShaderPassThroughTest, ShaderPassThroughWithoutVertex) {
+  auto test_data = GetParam();
+
+  std::string in =
+      "SHADER " + std::string(test_data.name) + " my_shader PASSTHROUGH";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(
+      "1: invalid shader type for PASSTHROUGH. Only vertex PASSTHROUGH "
+      "allowed",
+      r.Error());
+}
+INSTANTIATE_TEST_CASE_P(AmberScriptParserShaderPassThroughTests,
+                        AmberScriptParserShaderPassThroughTest,
+                        testing::Values(NameData{"fragment"},
+                                        NameData{"geometry"},
+                                        NameData{"tessellation_evaluation"},
+                                        NameData{"tessellation_control"},
+                                        NameData{"compute"}), );
+
+TEST_F(AmberScriptParserTest, ShaderPassThroughUnknownShaderType) {
+  std::string in = "SHADER UNKNOWN my_shader PASSTHROUGH";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("1: unknown shader type: UNKNOWN", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, ShaderPassThroughMissingName) {
+  std::string in = "SHADER vertex PASSTHROUGH";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("1: invalid token when looking for shader format", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, ShaderPassThroughExtraParameters) {
+  std::string in = "SHADER vertex my_shader PASSTHROUGH INVALID";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("1: extra parameters after SHADER PASSTHROUGH", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, Shader) {
+  std::string shader_result = R"(
+# Shader has a comment in it.
+void main() {
+  gl_FragColor = vec3(2, 3, 4);
+}
+)";
+
+  std::string in = R"(#!amber
+SHADER geometry shader_name GLSL
+)" + shader_result +
+                   "END";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto script = ToAmberScript(parser.GetScript());
+  const auto& shaders = script->GetShaders();
+  ASSERT_EQ(1U, shaders.size());
+
+  const auto* shader = shaders[0].get();
+  EXPECT_EQ("shader_name", shader->GetName());
+  EXPECT_EQ(ShaderType::kGeometry, shader->GetType());
+  EXPECT_EQ(ShaderFormat::kGlsl, shader->GetFormat());
+  EXPECT_EQ(shader_result, shader->GetData());
+}
+
+TEST_F(AmberScriptParserTest, ShaderInvalidFormat) {
+  std::string in = R"(#!amber
+SHADER geometry shader_name INVALID
+# Shader has a comment in it.
+void main() {
+  gl_FragColor = vec3(2, 3, 4);
+}
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("2: unknown shader format: INVALID", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, ShaderMissingFormat) {
+  std::string in = R"(#!amber
+SHADER geometry shader_name
+# Shader has a comment in it.
+void main() {
+  gl_FragColor = vec3(2, 3, 4);
+}
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("3: invalid token when looking for shader format", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, ShaderEmpty) {
+  std::string in = R"(#!amber
+SHADER geometry shader_name GLSL
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("3: SHADER must not be empty", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, ShaderMissingName) {
+  std::string in = R"(#!amber
+SHADER geometry GLSL
+# Shader has a comment in it.
+void main() {
+  gl_FragColor = vec3(2, 3, 4);
+}
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("3: invalid token when looking for shader format", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, ShaderMissingEnd) {
+  std::string in = R"(#!amber
+SHADER geometry shader_name GLSL
+# Shader has a comment in it.
+void main() {
+  gl_FragColor = vec3(2, 3, 4);
+})";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("6: SHADER missing END command", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, ShaderExtraParameter) {
+  std::string in = R"(#!amber
+SHADER geometry shader_name GLSL INVALID
+# Shader has a comment in it.
+void main() {
+  gl_FragColor = vec3(2, 3, 4);
+}
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("2: extra parameters after SHADER command", r.Error());
+}
+
+using AmberScriptParserShaderTypeTest = testing::TestWithParam<ShaderTypeData>;
+TEST_P(AmberScriptParserShaderTypeTest, ShaderFormats) {
+  auto test_data = GetParam();
+
+  std::string shader_result = R"(
+void main() {
+  gl_FragColor = vec3(2, 3, 4);
+}
+)";
+
+  std::string in = "SHADER " + std::string(test_data.name) + R"( my_shader GLSL
+)" + shader_result +
+                   "END";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto script = ToAmberScript(parser.GetScript());
+  const auto& shaders = script->GetShaders();
+  ASSERT_EQ(1U, shaders.size());
+
+  const auto* shader = shaders[0].get();
+  EXPECT_EQ("my_shader", shader->GetName());
+  EXPECT_EQ(test_data.type, shader->GetType());
+  EXPECT_EQ(ShaderFormat::kGlsl, shader->GetFormat());
+  EXPECT_EQ(shader_result, shader->GetData());
+}
+INSTANTIATE_TEST_CASE_P(
+    AmberScriptParserTestsShaderType,
+    AmberScriptParserShaderTypeTest,
+    testing::Values(ShaderTypeData{"vertex", ShaderType::kVertex},
+                    ShaderTypeData{"fragment", ShaderType::kFragment},
+                    ShaderTypeData{"geometry", ShaderType::kGeometry},
+                    ShaderTypeData{"tessellation_evaluation",
+                                   ShaderType::kTessellationEvaluation},
+                    ShaderTypeData{"tessellation_control",
+                                   ShaderType::kTessellationControl},
+                    ShaderTypeData{"compute", ShaderType::kCompute}), );
+
+using AmberScriptParserShaderFormatTest =
+    testing::TestWithParam<ShaderFormatData>;
+TEST_P(AmberScriptParserShaderFormatTest, ShaderFormats) {
+  auto test_data = GetParam();
+
+  std::string shader_result = R"(void main() {
+  gl_FragColor = vec3(2, 3, 4);
+}
+)";
+
+  std::string in = "SHADER vertex my_shader " + std::string(test_data.name) +
+                   "\n" + shader_result + "END";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto script = ToAmberScript(parser.GetScript());
+  const auto& shaders = script->GetShaders();
+  ASSERT_EQ(1U, shaders.size());
+
+  const auto* shader = shaders[0].get();
+  EXPECT_EQ("my_shader", shader->GetName());
+  EXPECT_EQ(ShaderType::kVertex, shader->GetType());
+  EXPECT_EQ(test_data.format, shader->GetFormat());
+  EXPECT_EQ(shader_result, shader->GetData());
+}
+INSTANTIATE_TEST_CASE_P(
+    AmberScriptParserTestsShaderFormat,
+    AmberScriptParserShaderFormatTest,
+    testing::Values(ShaderFormatData{"GLSL", ShaderFormat::kGlsl},
+                    ShaderFormatData{"SPIRV-ASM", ShaderFormat::kSpirvAsm},
+                    ShaderFormatData{"SPIRV-HEX", ShaderFormat::kSpirvHex}), );
+
+TEST_F(AmberScriptParserTest, DuplicateShaderName) {
+  std::string in = R"(
+SHADER vertex my_shader GLSL
+# shader
+END
+SHADER fragment my_shader GLSL
+# another shader
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("7: duplicate shader name provided", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, Pipeline) {
+  std::string in = R"(
+SHADER vertex my_shader PASSTHROUGH
+SHADER fragment my_fragment GLSL
+# GLSL Shader
+END
+
+PIPELINE graphics my_pipeline
+  ATTACH my_shader
+  ATTACH my_fragment
+END
+)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto script = ToAmberScript(parser.GetScript());
+  EXPECT_EQ(2U, script->GetShaders().size());
+
+  const auto& pipelines = script->GetPipelines();
+  ASSERT_EQ(1U, pipelines.size());
+
+  const auto* pipeline = pipelines[0].get();
+  EXPECT_EQ("my_pipeline", pipeline->GetName());
+  EXPECT_EQ(PipelineType::kGraphics, pipeline->GetType());
+
+  const auto& shaders = pipeline->GetShaders();
+  ASSERT_EQ(2U, shaders.size());
+
+  ASSERT_TRUE(shaders[0].GetShader() != nullptr);
+  EXPECT_EQ("my_shader", shaders[0].GetShader()->GetName());
+  EXPECT_EQ(ShaderType::kVertex, shaders[0].GetShader()->GetType());
+  EXPECT_EQ(static_cast<uint32_t>(0),
+            shaders[0].GetShaderOptimizations().size());
+
+  ASSERT_TRUE(shaders[1].GetShader() != nullptr);
+  EXPECT_EQ("my_fragment", shaders[1].GetShader()->GetName());
+  EXPECT_EQ(ShaderType::kFragment, shaders[1].GetShader()->GetType());
+  EXPECT_EQ(static_cast<uint32_t>(0),
+            shaders[1].GetShaderOptimizations().size());
+}
+
+TEST_F(AmberScriptParserTest, PipelineMissingEnd) {
+  std::string in = R"(
+SHADER vertex my_shader PASSTHROUGH
+PIPELINE graphics my_pipeline
+  ATTACH my_shader
+)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("5: PIPELINE missing END command", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, PipelineWithExtraParams) {
+  std::string in = R"(
+PIPELINE graphics my_pipeline INVALID
+  ATTACH my_shader
+END
+)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("2: extra parameters after PIPELINE command", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, PipelineInvalidType) {
+  std::string in = "PIPELINE my_name\nEND";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("1: unknown pipeline type: my_name", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, PipelineMissingName) {
+  std::string in = "PIPELINE compute\nEND";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("2: invalid token when looking for pipeline name", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, PipelineWithInvalidTokenType) {
+  std::string in = "PIPELINE 123 my_pipeline\nEND";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("1: invalid token when looking for pipeline type", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, PipelineWithInvalidTokenName) {
+  std::string in = "PIPELINE compute 123\nEND";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("1: invalid token when looking for pipeline name", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, PipelineEmpty) {
+  std::string in = "PIPELINE compute my_pipeline\nEND";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("2: compute pipeline requires a compute shader", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, PipelineWithUnknownShader) {
+  std::string in = R"(
+PIPELINE graphics my_pipeline
+  ATTACH my_shader
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("3: unknown shader in ATTACH command", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, PipelineWithUnknownCommand) {
+  std::string in = R"(
+PIPELINE compute my_pipeline
+  SHADER vertex my_shader PASSTHROUGH
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("3: unknown token in pipeline block: SHADER", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, DuplicatePipelineName) {
+  std::string in = R"(
+SHADER vertex my_shader PASSTHROUGH
+SHADER fragment my_fragment GLSL
+# Fragment shader
+END
+
+PIPELINE graphics my_pipeline
+  ATTACH my_shader
+  ATTACH my_fragment
+END
+PIPELINE graphics my_pipeline
+  ATTACH my_shader
+  ATTACH my_fragment
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("14: duplicate pipeline name provided", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, DuplicateShadersInAPipeline) {
+  std::string in = R"(
+SHADER vertex my_shader PASSTHROUGH
+PIPELINE graphics my_pipeline
+  ATTACH my_shader
+  ATTACH my_shader
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("5: can not add duplicate shader to pipeline", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, AttachInvalidToken) {
+  std::string in = R"(PIPELINE graphics my_pipeline
+  ATTACH 1234
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("2: invalid token in ATTACH command", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, AttachExtraParameter) {
+  std::string in = R"(
+SHADER vertex my_shader PASSTHROUGH
+PIPELINE graphics my_pipeline
+  ATTACH my_shader INVALID
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("4: extra parameters after ATTACH command", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, AttachMissingValue) {
+  std::string in = R"(
+SHADER vertex my_shader PASSTHROUGH
+PIPELINE graphics my_pipeline
+  ATTACH
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("5: invalid token in ATTACH command", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, ComputeShaderInGraphicsPipeline) {
+  std::string in = R"(SHADER compute my_shader GLSL
+void main() {
+  gl_FragColor = vec3(2, 3, 4);
+}
+END
+
+PIPELINE graphics my_pipeline
+  ATTACH my_shader
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ("8: can not add a compute shader to a graphics pipeline",
+            r.Error());
+}
+
+using AmberScriptParserPipelineAttachTest =
+    testing::TestWithParam<ShaderTypeData>;
+TEST_P(AmberScriptParserPipelineAttachTest, GraphicsShaderInComputePipeline) {
+  auto test_data = GetParam();
+
+  std::string in = "SHADER " + std::string(test_data.name) + R"( my_shader GLSL
+void main() {
+  gl_FragColor = vec3(2, 3, 4);
+}
+END
+
+PIPELINE compute my_pipeline
+  ATTACH my_shader
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ("8: only compute shaders allowed in a compute pipeline", r.Error());
+}
+INSTANTIATE_TEST_CASE_P(
+    AmberScriptParserPipelineAttachTests,
+    AmberScriptParserPipelineAttachTest,
+    testing::Values(ShaderTypeData{"vertex", ShaderType::kVertex},
+                    ShaderTypeData{"fragment", ShaderType::kFragment},
+                    ShaderTypeData{"geometry", ShaderType::kGeometry},
+                    ShaderTypeData{"tessellation_evaluation",
+                                   ShaderType::kTessellationEvaluation},
+                    ShaderTypeData{"tessellation_control",
+                                   ShaderType::kTessellationControl}), );
+
+TEST_F(AmberScriptParserTest, PipelineEntryPoint) {
+  std::string in = R"(
+SHADER vertex my_shader PASSTHROUGH
+SHADER fragment my_fragment GLSL
+# GLSL Shader
+END
+
+PIPELINE graphics my_pipeline
+  ATTACH my_shader
+  ATTACH my_fragment
+
+  ENTRY_POINT my_shader green
+END
+)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto script = ToAmberScript(parser.GetScript());
+  const auto& pipelines = script->GetPipelines();
+  ASSERT_EQ(1U, pipelines.size());
+
+  const auto* pipeline = pipelines[0].get();
+  const auto& shaders = pipeline->GetShaders();
+  ASSERT_EQ(2U, shaders.size());
+
+  ASSERT_TRUE(shaders[0].GetShader() != nullptr);
+  EXPECT_EQ(ShaderType::kVertex, shaders[0].GetShader()->GetType());
+  EXPECT_EQ("green", shaders[0].GetEntryPoint());
+
+  ASSERT_TRUE(shaders[1].GetShader() != nullptr);
+  EXPECT_EQ(ShaderType::kFragment, shaders[1].GetShader()->GetType());
+  EXPECT_EQ("main", shaders[1].GetEntryPoint());
+}
+
+TEST_F(AmberScriptParserTest, PipelineEntryPointWithInvalidValue) {
+  std::string in = R"(
+SHADER compute my_compute GLSL
+# Compute Shader
+END
+PIPELINE compute my_pipeline
+  ATTACH my_compute
+  ENTRY_POINT my_compute 1234
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ("7: invalid value in ENTRY_POINT command", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, PipelineEntryPointMissingValue) {
+  std::string in = R"(
+SHADER compute my_compute GLSL
+# Compute Shader
+END
+PIPELINE compute my_pipeline
+  ATTACH my_compute
+  ENTRY_POINT
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ("8: missing shader name in ENTRY_POINT command", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, PipelineEntryPointMissingEntryPointName) {
+  std::string in = R"(
+SHADER compute my_compute GLSL
+# Compute Shader
+END
+PIPELINE compute my_pipeline
+  ATTACH my_compute
+  ENTRY_POINT my_compute
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ("8: invalid value in ENTRY_POINT command", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, PipelineEntryPointExtraParameter) {
+  std::string in = R"(
+SHADER compute my_compute GLSL
+# Compute Shader
+END
+PIPELINE compute my_pipeline
+  ATTACH my_compute
+  ENTRY_POINT my_compute green INVALID
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ("7: extra parameters after ENTRY_POINT command", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, PipelineMultipleEntryPointsForOneShader) {
+  std::string in = R"(
+SHADER compute my_compute GLSL
+# Compute Shader
+END
+PIPELINE compute my_pipeline
+  ATTACH my_compute
+  ENTRY_POINT my_compute green
+  ENTRY_POINT my_compute red
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ("8: multiple entry points given for the same shader", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, PipelineEntryPointForInvalidShader) {
+  std::string in = R"(
+SHADER compute my_compute GLSL
+# Compute Shader
+END
+PIPELINE compute my_pipeline
+  ATTACH my_compute
+  ENTRY_POINT INVALID green
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ("7: unknown shader in ENTRY_POINT command", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, PipelineShaderOptimization) {
+  std::string in = R"(
+SHADER vertex my_shader PASSTHROUGH
+SHADER fragment my_fragment GLSL
+# GLSL Shader
+END
+SHADER geometry my_geom GLSL
+# Geom shader
+END
+PIPELINE graphics my_pipeline
+  ATTACH my_shader
+  SHADER_OPTIMIZATION my_shader
+    opt1
+    opt_second
+  END
+
+  ATTACH my_fragment
+  SHADER_OPTIMIZATION my_fragment
+    another_optimization
+    third
+  END
+
+  ATTACH my_geom
+  SHADER_OPTIMIZATION my_geom
+  END
+END
+)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto script = ToAmberScript(parser.GetScript());
+  const auto& pipelines = script->GetPipelines();
+  ASSERT_EQ(1U, pipelines.size());
+
+  const auto* pipeline = pipelines[0].get();
+  const auto& shaders = pipeline->GetShaders();
+  ASSERT_EQ(3U, shaders.size());
+
+  ASSERT_TRUE(shaders[0].GetShader() != nullptr);
+  EXPECT_EQ(ShaderType::kVertex, shaders[0].GetShader()->GetType());
+  std::vector<std::string> my_shader_opts = {"opt1", "opt_second"};
+  EXPECT_EQ(my_shader_opts, shaders[0].GetShaderOptimizations());
+
+  ASSERT_TRUE(shaders[1].GetShader() != nullptr);
+  EXPECT_EQ(ShaderType::kFragment, shaders[1].GetShader()->GetType());
+  std::vector<std::string> my_fragment_opts = {"another_optimization", "third"};
+  EXPECT_EQ(my_fragment_opts, shaders[1].GetShaderOptimizations());
+
+  ASSERT_TRUE(shaders[2].GetShader() != nullptr);
+  EXPECT_EQ(ShaderType::kGeometry, shaders[2].GetShader()->GetType());
+  std::vector<std::string> my_geom_opts = {};
+  EXPECT_EQ(my_geom_opts, shaders[2].GetShaderOptimizations());
+}
+
+TEST_F(AmberScriptParserTest, PipelineShaderOptmizationInvalidShader) {
+  std::string in = R"(
+PIPELINE graphics my_pipeline
+SHADER_OPTIMIZATION invalid_shader
+  opt1
+  opt_second
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("3: unknown shader in SHADER_OPTIMIZATION command", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, PipelineShaderOptmizationMissingShader) {
+  std::string in = R"(
+PIPELINE graphics my_pipeline
+SHADER_OPTIMIZATION
+  opt1
+  opt_second
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("4: missing shader name in SHADER_OPTIMIZATION command", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, PipelineShaderOptmizationnUnAttachedShader) {
+  std::string in = R"(
+SHADER vertex my_vertex PASSTHROUGH
+PIPELINE graphics my_pipeline
+  SHADER_OPTIMIZATION my_vertex
+    opt1
+    opt_second
+  END
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("7: unknown shader specified for optimizations: my_vertex",
+            r.Error());
+}
+
+TEST_F(AmberScriptParserTest, PipelineShaderOptimizationMissingEnd) {
+  std::string in = R"(
+SHADER vertex my_shader PASSTHROUGH
+PIPELINE graphics my_pipeline
+  ATTACH my_shader
+  SHADER_OPTIMIZATION my_shader
+    opt1
+    opt_second)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("7: SHADER_OPTIMIZATION missing END command", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, PipelineShaderOptimizationExtraParams) {
+  std::string in = R"(
+SHADER vertex my_shader PASSTHROUGH
+PIPELINE graphics my_pipeline
+  ATTACH my_shader
+  SHADER_OPTIMIZATION my_shader EXTRA
+    opt1
+    opt_second
+  END
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("5: extra parameters after SHADER_OPTIMIZATION command", r.Error());
+}
+
+TEST_F(AmberScriptParserTest, PipelineShaderOptimizationNonStringParam) {
+  std::string in = R"(
+SHADER vertex my_shader PASSTHROUGH
+PIPELINE graphics my_pipeline
+  ATTACH my_shader
+  SHADER_OPTIMIZATION my_shader
+    123
+    opt
+  END
+END)";
+
+  Parser parser;
+  Result r = parser.Parse(in);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("6: SHADER_OPTIMIZATION options must be strings", r.Error());
+}
+
+}  // namespace amberscript
+}  // namespace amber
diff --git a/src/amberscript/pipeline.cc b/src/amberscript/pipeline.cc
new file mode 100644
index 0000000..94f498b
--- /dev/null
+++ b/src/amberscript/pipeline.cc
@@ -0,0 +1,144 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/amberscript/pipeline.h"
+
+#include <algorithm>
+#include <set>
+
+namespace amber {
+namespace amberscript {
+
+Pipeline::ShaderInfo::ShaderInfo(const Shader* shader)
+    : shader_(shader), entry_point_("main") {}
+
+Pipeline::ShaderInfo::ShaderInfo(const ShaderInfo&) = default;
+
+Pipeline::ShaderInfo::~ShaderInfo() = default;
+
+Pipeline::Pipeline(PipelineType type) : pipeline_type_(type) {}
+
+Pipeline::~Pipeline() = default;
+
+Result Pipeline::AddShader(const Shader* shader) {
+  if (!shader)
+    return Result("shader can not be null when attached to pipeline");
+
+  if (pipeline_type_ == PipelineType::kCompute &&
+      shader->GetType() != ShaderType::kCompute) {
+    return Result("only compute shaders allowed in a compute pipeline");
+  }
+  if (pipeline_type_ == PipelineType::kGraphics &&
+      shader->GetType() == ShaderType::kCompute) {
+    return Result("can not add a compute shader to a graphics pipeline");
+  }
+
+  for (const auto& info : shaders_) {
+    const auto* is = info.GetShader();
+    if (is == shader)
+      return Result("can not add duplicate shader to pipeline");
+    if (is->GetType() == shader->GetType())
+      return Result("can not add duplicate shader type to pipeline");
+  }
+
+  shaders_.emplace_back(shader);
+  return {};
+}
+
+Result Pipeline::SetShaderOptimizations(const Shader* shader,
+                                        const std::vector<std::string>& opts) {
+  if (!shader)
+    return Result("invalid shader specified for optimizations");
+
+  std::set<std::string> seen;
+  for (const auto& opt : opts) {
+    if (seen.count(opt) != 0)
+      return Result("duplicate optimization flag (" + opt + ") set on shader");
+
+    seen.insert(opt);
+  }
+
+  for (auto& info : shaders_) {
+    const auto* is = info.GetShader();
+    if (is == shader) {
+      info.SetShaderOptimizations(opts);
+      return {};
+    }
+  }
+
+  return Result("unknown shader specified for optimizations: " +
+                shader->GetName());
+}
+
+Result Pipeline::SetShaderEntryPoint(const Shader* shader,
+                                     const std::string& name) {
+  if (!shader)
+    return Result("invalid shader specified for entry point");
+  if (name.empty())
+    return Result("entry point should not be blank");
+
+  for (auto& info : shaders_) {
+    if (info.GetShader() == shader) {
+      if (info.GetEntryPoint() != "main")
+        return Result("multiple entry points given for the same shader");
+
+      info.SetEntryPoint(name);
+      return {};
+    }
+  }
+
+  return Result("unknown shader specified for entry point: " +
+                shader->GetName());
+}
+
+Result Pipeline::Validate() const {
+  if (pipeline_type_ == PipelineType::kGraphics)
+    return ValidateGraphics();
+  return ValidateCompute();
+}
+
+Result Pipeline::ValidateGraphics() const {
+  if (shaders_.empty())
+    return Result("graphics pipeline requires vertex and fragment shaders");
+
+  bool found_vertex = false;
+  bool found_fragment = false;
+  for (const auto& info : shaders_) {
+    const auto* is = info.GetShader();
+    if (is->GetType() == ShaderType::kVertex)
+      found_vertex = true;
+    if (is->GetType() == ShaderType::kFragment)
+      found_fragment = true;
+    if (found_vertex && found_fragment)
+      break;
+  }
+
+  if (!found_vertex && !found_fragment)
+    return Result("graphics pipeline requires vertex and fragment shaders");
+  if (!found_vertex)
+    return Result("graphics pipeline requires a vertex shader");
+  if (!found_fragment)
+    return Result("graphics pipeline requires a fragment shader");
+  return {};
+}
+
+Result Pipeline::ValidateCompute() const {
+  if (shaders_.empty())
+    return Result("compute pipeline requires a compute shader");
+
+  return {};
+}
+
+}  // namespace amberscript
+}  // namespace amber
diff --git a/src/amberscript/pipeline.h b/src/amberscript/pipeline.h
new file mode 100644
index 0000000..3bf5d68
--- /dev/null
+++ b/src/amberscript/pipeline.h
@@ -0,0 +1,85 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_AMBERSCRIPT_PIPELINE_H_
+#define SRC_AMBERSCRIPT_PIPELINE_H_
+
+#include <string>
+#include <vector>
+
+#include "amber/result.h"
+#include "src/amberscript/shader.h"
+
+namespace amber {
+namespace amberscript {
+
+enum class PipelineType { kCompute = 0, kGraphics };
+
+class Pipeline {
+ public:
+  class ShaderInfo {
+   public:
+    ShaderInfo(const Shader*);
+    ShaderInfo(const ShaderInfo&);
+    ~ShaderInfo();
+
+    void SetShaderOptimizations(const std::vector<std::string>& opts) {
+      shader_optimizations_ = opts;
+    }
+    const std::vector<std::string>& GetShaderOptimizations() const {
+      return shader_optimizations_;
+    }
+
+    const Shader* GetShader() const { return shader_; }
+
+    void SetEntryPoint(const std::string& ep) { entry_point_ = ep; }
+    std::string GetEntryPoint() const { return entry_point_; }
+
+   private:
+    const Shader* shader_ = nullptr;
+    std::vector<std::string> shader_optimizations_;
+    std::string entry_point_;
+  };
+
+  Pipeline(PipelineType type);
+  ~Pipeline();
+
+  PipelineType GetType() const { return pipeline_type_; }
+
+  void SetName(const std::string& name) { name_ = name; }
+  const std::string& GetName() const { return name_; }
+
+  Result AddShader(const Shader*);
+  const std::vector<ShaderInfo>& GetShaders() const { return shaders_; }
+
+  Result SetShaderEntryPoint(const Shader* shader, const std::string& name);
+  Result SetShaderOptimizations(const Shader* shader,
+                                const std::vector<std::string>& opts);
+
+  // Validates that the pipeline has been created correctly.
+  Result Validate() const;
+
+ private:
+  Result ValidateGraphics() const;
+  Result ValidateCompute() const;
+
+  PipelineType pipeline_type_ = PipelineType::kCompute;
+  std::string name_;
+  std::vector<ShaderInfo> shaders_;
+};
+
+}  // namespace amberscript
+}  // namespace amber
+
+#endif  // SRC_AMBERSCRIPT_PIPELINE_H_
diff --git a/src/amberscript/pipeline_test.cc b/src/amberscript/pipeline_test.cc
new file mode 100644
index 0000000..4d6f366
--- /dev/null
+++ b/src/amberscript/pipeline_test.cc
@@ -0,0 +1,331 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/amberscript/pipeline.h"
+#include "gtest/gtest.h"
+
+namespace amber {
+namespace amberscript {
+namespace {
+
+struct ShaderTypeData {
+  ShaderType type;
+};
+
+}  // namespace
+
+using AmberScriptPipelineTest = testing::Test;
+
+TEST_F(AmberScriptPipelineTest, AddShader) {
+  Shader v(ShaderType::kVertex);
+  Shader f(ShaderType::kFragment);
+
+  Pipeline p(PipelineType::kGraphics);
+  Result r = p.AddShader(&v);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  r = p.AddShader(&f);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  const auto& shaders = p.GetShaders();
+  EXPECT_EQ(2U, shaders.size());
+
+  EXPECT_EQ(&v, shaders[0].GetShader());
+  EXPECT_EQ(&f, shaders[1].GetShader());
+}
+
+TEST_F(AmberScriptPipelineTest, MissingShader) {
+  Pipeline p(PipelineType::kGraphics);
+  Result r = p.AddShader(nullptr);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("shader can not be null when attached to pipeline", r.Error());
+}
+
+TEST_F(AmberScriptPipelineTest, DuplicateShaders) {
+  Shader v(ShaderType::kVertex);
+  Shader f(ShaderType::kFragment);
+
+  Pipeline p(PipelineType::kGraphics);
+  Result r = p.AddShader(&v);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  r = p.AddShader(&f);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  r = p.AddShader(&v);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("can not add duplicate shader to pipeline", r.Error());
+}
+
+TEST_F(AmberScriptPipelineTest, DuplicateShaderType) {
+  Shader v(ShaderType::kVertex);
+  Shader f(ShaderType::kVertex);
+
+  Pipeline p(PipelineType::kGraphics);
+  Result r = p.AddShader(&v);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  r = p.AddShader(&f);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("can not add duplicate shader type to pipeline", r.Error());
+}
+
+using AmberScriptPipelineComputePipelineTest =
+    testing::TestWithParam<ShaderTypeData>;
+TEST_P(AmberScriptPipelineComputePipelineTest,
+       SettingGraphicsShaderToComputePipeline) {
+  const auto test_data = GetParam();
+
+  Shader s(test_data.type);
+
+  Pipeline p(PipelineType::kCompute);
+  Result r = p.AddShader(&s);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("only compute shaders allowed in a compute pipeline", r.Error());
+}
+INSTANTIATE_TEST_CASE_P(
+    AmberScriptPipelineComputePipelineTests,
+    AmberScriptPipelineComputePipelineTest,
+    testing::Values(ShaderTypeData{ShaderType::kVertex},
+                    ShaderTypeData{ShaderType::kFragment},
+                    ShaderTypeData{ShaderType::kGeometry},
+                    ShaderTypeData{ShaderType::kTessellationEvaluation},
+                    ShaderTypeData{ShaderType::kTessellationControl}), );
+
+TEST_F(AmberScriptPipelineTest, SettingComputeShaderToGraphicsPipeline) {
+  Shader c(ShaderType::kCompute);
+
+  Pipeline p(PipelineType::kGraphics);
+  Result r = p.AddShader(&c);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("can not add a compute shader to a graphics pipeline", r.Error());
+}
+
+TEST_F(AmberScriptPipelineTest, SetShaderOptimizations) {
+  Shader v(ShaderType::kVertex);
+  Shader f(ShaderType::kFragment);
+
+  Pipeline p(PipelineType::kGraphics);
+  Result r = p.AddShader(&v);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  r = p.AddShader(&f);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  std::vector<std::string> first = {"First", "Second"};
+  std::vector<std::string> second = {"Third", "Forth"};
+
+  r = p.SetShaderOptimizations(&f, first);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  r = p.SetShaderOptimizations(&v, second);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  const auto& shaders = p.GetShaders();
+  EXPECT_EQ(2U, shaders.size());
+  EXPECT_EQ(second, shaders[0].GetShaderOptimizations());
+  EXPECT_EQ(first, shaders[1].GetShaderOptimizations());
+}
+
+TEST_F(AmberScriptPipelineTest, DuplicateShaderOptimizations) {
+  Shader v(ShaderType::kVertex);
+
+  Pipeline p(PipelineType::kGraphics);
+  Result r = p.AddShader(&v);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  std::vector<std::string> data = {"One", "One"};
+  r = p.SetShaderOptimizations(&v, data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("duplicate optimization flag (One) set on shader", r.Error());
+}
+
+TEST_F(AmberScriptPipelineTest, SetOptimizationForMissingShader) {
+  Pipeline p(PipelineType::kGraphics);
+  Result r = p.SetShaderOptimizations(nullptr, {"One", "Two"});
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("invalid shader specified for optimizations", r.Error());
+}
+
+TEST_F(AmberScriptPipelineTest, SetOptimizationForInvalidShader) {
+  Shader v(ShaderType::kVertex);
+  v.SetName("my_shader");
+
+  Pipeline p(PipelineType::kGraphics);
+  Result r = p.SetShaderOptimizations(&v, {"One", "Two"});
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("unknown shader specified for optimizations: my_shader", r.Error());
+}
+
+TEST_F(AmberScriptPipelineTest,
+       GraphicsPipelineRequiresVertexAndFragmentShader) {
+  Shader v(ShaderType::kVertex);
+  Shader f(ShaderType::kFragment);
+  Shader g(ShaderType::kGeometry);
+
+  Pipeline p(PipelineType::kGraphics);
+  Result r = p.AddShader(&v);
+  EXPECT_TRUE(r.IsSuccess()) << r.Error();
+
+  r = p.AddShader(&g);
+  EXPECT_TRUE(r.IsSuccess()) << r.Error();
+
+  r = p.AddShader(&f);
+  EXPECT_TRUE(r.IsSuccess()) << r.Error();
+
+  r = p.Validate();
+  EXPECT_TRUE(r.IsSuccess()) << r.Error();
+}
+
+TEST_F(AmberScriptPipelineTest, GraphicsPipelineMissingFragmentShader) {
+  Shader v(ShaderType::kVertex);
+  Shader g(ShaderType::kGeometry);
+
+  Pipeline p(PipelineType::kGraphics);
+  Result r = p.AddShader(&v);
+  EXPECT_TRUE(r.IsSuccess()) << r.Error();
+
+  r = p.AddShader(&g);
+  EXPECT_TRUE(r.IsSuccess()) << r.Error();
+
+  r = p.Validate();
+  EXPECT_FALSE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ("graphics pipeline requires a fragment shader", r.Error());
+}
+
+TEST_F(AmberScriptPipelineTest, GraphicsPipelineMissingVertexShader) {
+  Shader f(ShaderType::kFragment);
+  Shader g(ShaderType::kGeometry);
+
+  Pipeline p(PipelineType::kGraphics);
+  Result r = p.AddShader(&g);
+  EXPECT_TRUE(r.IsSuccess()) << r.Error();
+
+  r = p.AddShader(&f);
+  EXPECT_TRUE(r.IsSuccess()) << r.Error();
+
+  r = p.Validate();
+  EXPECT_FALSE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ("graphics pipeline requires a vertex shader", r.Error());
+}
+
+TEST_F(AmberScriptPipelineTest,
+       GraphicsPipelineMissingVertexAndFragmentShader) {
+  Shader g(ShaderType::kGeometry);
+
+  Pipeline p(PipelineType::kGraphics);
+  Result r = p.AddShader(&g);
+  EXPECT_TRUE(r.IsSuccess()) << r.Error();
+
+  r = p.Validate();
+  EXPECT_FALSE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ("graphics pipeline requires vertex and fragment shaders",
+            r.Error());
+}
+
+TEST_F(AmberScriptPipelineTest, GraphicsPipelineWihoutShaders) {
+  Pipeline p(PipelineType::kGraphics);
+  Result r = p.Validate();
+  EXPECT_FALSE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ("graphics pipeline requires vertex and fragment shaders",
+            r.Error());
+}
+
+TEST_F(AmberScriptPipelineTest, ComputePipelineRequiresComputeShader) {
+  Shader c(ShaderType::kCompute);
+
+  Pipeline p(PipelineType::kCompute);
+  Result r = p.AddShader(&c);
+  EXPECT_TRUE(r.IsSuccess()) << r.Error();
+
+  r = p.Validate();
+  EXPECT_TRUE(r.IsSuccess()) << r.Error();
+}
+
+TEST_F(AmberScriptPipelineTest, ComputePipelineWithoutShader) {
+  Pipeline p(PipelineType::kCompute);
+  Result r = p.Validate();
+  EXPECT_FALSE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ("compute pipeline requires a compute shader", r.Error());
+}
+
+TEST_F(AmberScriptPipelineTest, SetEntryPointForMissingShader) {
+  Shader c(ShaderType::kCompute);
+  c.SetName("my_shader");
+
+  Pipeline p(PipelineType::kCompute);
+  Result r = p.SetShaderEntryPoint(&c, "test");
+  EXPECT_FALSE(r.IsSuccess());
+  EXPECT_EQ("unknown shader specified for entry point: my_shader", r.Error());
+}
+
+TEST_F(AmberScriptPipelineTest, SetEntryPointForNullShader) {
+  Pipeline p(PipelineType::kCompute);
+  Result r = p.SetShaderEntryPoint(nullptr, "test");
+  EXPECT_FALSE(r.IsSuccess());
+  EXPECT_EQ("invalid shader specified for entry point", r.Error());
+}
+
+TEST_F(AmberScriptPipelineTest, SetBlankEntryPoint) {
+  Shader c(ShaderType::kCompute);
+  Pipeline p(PipelineType::kCompute);
+  Result r = p.AddShader(&c);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  r = p.SetShaderEntryPoint(&c, "");
+  EXPECT_FALSE(r.IsSuccess());
+  EXPECT_EQ("entry point should not be blank", r.Error());
+}
+
+TEST_F(AmberScriptPipelineTest, ShaderDefaultEntryPoint) {
+  Shader c(ShaderType::kCompute);
+  Pipeline p(PipelineType::kCompute);
+  Result r = p.AddShader(&c);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  const auto& shaders = p.GetShaders();
+  ASSERT_EQ(1U, shaders.size());
+  EXPECT_EQ("main", shaders[0].GetEntryPoint());
+}
+
+TEST_F(AmberScriptPipelineTest, SetShaderEntryPoint) {
+  Shader c(ShaderType::kCompute);
+  Pipeline p(PipelineType::kCompute);
+  Result r = p.AddShader(&c);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  r = p.SetShaderEntryPoint(&c, "my_main");
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  const auto& shaders = p.GetShaders();
+  ASSERT_EQ(1U, shaders.size());
+  EXPECT_EQ("my_main", shaders[0].GetEntryPoint());
+}
+
+TEST_F(AmberScriptPipelineTest, SetEntryPointMulitpleTimes) {
+  Shader c(ShaderType::kCompute);
+  Pipeline p(PipelineType::kCompute);
+  Result r = p.AddShader(&c);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  r = p.SetShaderEntryPoint(&c, "my_main");
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  r = p.SetShaderEntryPoint(&c, "another_main");
+  EXPECT_FALSE(r.IsSuccess());
+  EXPECT_EQ("multiple entry points given for the same shader", r.Error());
+}
+
+}  // namespace amberscript
+}  // namespace amber
diff --git a/src/amberscript/script.cc b/src/amberscript/script.cc
new file mode 100644
index 0000000..17bfedc
--- /dev/null
+++ b/src/amberscript/script.cc
@@ -0,0 +1,30 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/amberscript/script.h"
+
+namespace amber {
+namespace amberscript {
+
+Script::Script() : amber::Script(ScriptType::kAmberScript) {}
+
+Script::~Script() = default;
+
+}  // namespace amberscript
+
+const amberscript::Script* ToAmberScript(const amber::Script* s) {
+  return static_cast<const amberscript::Script*>(s);
+}
+
+}  // namespace amber
diff --git a/src/amberscript/script.h b/src/amberscript/script.h
new file mode 100644
index 0000000..79eac96
--- /dev/null
+++ b/src/amberscript/script.h
@@ -0,0 +1,81 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_AMBERSCRIPT_SCRIPT_H_
+#define SRC_AMBERSCRIPT_SCRIPT_H_
+
+#include <map>
+#include <memory>
+#include <vector>
+
+#include "src/amberscript/pipeline.h"
+#include "src/amberscript/shader.h"
+#include "src/script.h"
+
+namespace amber {
+namespace amberscript {
+
+class Script : public amber::Script {
+ public:
+  Script();
+  ~Script() override;
+
+  Result AddShader(std::unique_ptr<Shader> shader) {
+    if (name_to_shader_.count(shader->GetName()) > 0)
+      return Result("duplicate shader name provided");
+
+    shaders_.push_back(std::move(shader));
+    name_to_shader_[shaders_.back()->GetName()] = shaders_.back().get();
+    return {};
+  }
+  const std::vector<std::unique_ptr<Shader>>& GetShaders() const {
+    return shaders_;
+  }
+
+  Result AddPipeline(std::unique_ptr<Pipeline> pipeline) {
+    if (name_to_pipeline_.count(pipeline->GetName()) > 0)
+      return Result("duplicate pipeline name provided");
+
+    pipelines_.push_back(std::move(pipeline));
+    name_to_pipeline_[pipelines_.back()->GetName()] = pipelines_.back().get();
+    return {};
+  }
+  const std::vector<std::unique_ptr<Pipeline>>& GetPipelines() const {
+    return pipelines_;
+  }
+
+  Shader* GetShader(const std::string& name) const {
+    auto it = name_to_shader_.find(name);
+    return it == name_to_shader_.end() ? nullptr : it->second;
+  }
+
+  Pipeline* GetPipeline(const std::string& name) const {
+    auto it = name_to_pipeline_.find(name);
+    return it == name_to_pipeline_.end() ? nullptr : it->second;
+  }
+
+ private:
+  std::map<std::string, Shader*> name_to_shader_;
+  std::map<std::string, Pipeline*> name_to_pipeline_;
+  std::vector<std::unique_ptr<Shader>> shaders_;
+  std::vector<std::unique_ptr<Pipeline>> pipelines_;
+};
+
+}  // namespace amberscript
+
+const amberscript::Script* ToAmberScript(const amber::Script* s);
+
+}  // namespace amber
+
+#endif  // SRC_AMBERSCRIPT_SCRIPT_H_
diff --git a/src/amberscript/shader.cc b/src/amberscript/shader.cc
new file mode 100644
index 0000000..7d2cf5f
--- /dev/null
+++ b/src/amberscript/shader.cc
@@ -0,0 +1,25 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/amberscript/shader.h"
+
+namespace amber {
+namespace amberscript {
+
+Shader::Shader(ShaderType type) : shader_type_(type) {}
+
+Shader::~Shader() = default;
+
+}  // namespace amberscript
+}  // namespace amber
diff --git a/src/amberscript/shader.h b/src/amberscript/shader.h
new file mode 100644
index 0000000..eb6e1a2
--- /dev/null
+++ b/src/amberscript/shader.h
@@ -0,0 +1,51 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_AMBERSCRIPT_SHADER_H_
+#define SRC_AMBERSCRIPT_SHADER_H_
+
+#include <string>
+
+#include "src/shader_data.h"
+
+namespace amber {
+namespace amberscript {
+
+class Shader {
+ public:
+  Shader(ShaderType type);
+  ~Shader();
+
+  ShaderType GetType() const { return shader_type_; }
+
+  void SetName(const std::string& name) { name_ = name; }
+  const std::string& GetName() const { return name_; }
+
+  void SetFormat(ShaderFormat fmt) { shader_format_ = fmt; }
+  ShaderFormat GetFormat() const { return shader_format_; }
+
+  void SetData(const std::string& data) { data_ = data; }
+  const std::string& GetData() const { return data_; }
+
+ private:
+  ShaderType shader_type_;
+  ShaderFormat shader_format_;
+  std::string data_;
+  std::string name_;
+};
+
+}  // namespace amberscript
+}  // namespace amber
+
+#endif  // SRC_AMBERSCRIPT_SHADER_H_
diff --git a/src/buffer_data.h b/src/buffer_data.h
new file mode 100644
index 0000000..6b6ada6
--- /dev/null
+++ b/src/buffer_data.h
@@ -0,0 +1,24 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_BUFFER_DATA_H_
+#define SRC_BUFFER_DATA_H_
+
+namespace amber {
+
+enum class BufferType { kFramebuffer = 0, kVertexData, kIndices };
+
+}  // namespace amber
+
+#endif  // SRC_BUFFER_DATA_H_
diff --git a/src/cast_hash.h b/src/cast_hash.h
new file mode 100644
index 0000000..c7afbf0
--- /dev/null
+++ b/src/cast_hash.h
@@ -0,0 +1,33 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_CAST_HASH_H_
+#define SRC_CAST_HASH_H_
+
+namespace amber {
+
+// A hash implementation for types that can trivially be up-cast to a size_t.
+// For example, use this as a hasher for an enum whose underlying type is
+// an integer no wider than size_t.
+//
+// The need for this was a defect in the C++11 library, and has been fixed
+// in C++14.  http://www.open-std.org/jtc1/sc22/wg21/docs/lwg-defects.html#2148
+template <typename T>
+struct CastHash {
+  size_t operator()(const T& value) const { return static_cast<size_t>(value); }
+};
+
+}  // namespace amber
+
+#endif  // SRC_CAST_HASH_H_
diff --git a/src/command.cc b/src/command.cc
new file mode 100644
index 0000000..a89a75d
--- /dev/null
+++ b/src/command.cc
@@ -0,0 +1,132 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/command.h"
+
+namespace amber {
+
+Command::Command(Type type) : command_type_(type) {}
+
+Command::~Command() = default;
+
+ClearCommand* Command::AsClear() {
+  return static_cast<ClearCommand*>(this);
+}
+
+ClearColorCommand* Command::AsClearColor() {
+  return static_cast<ClearColorCommand*>(this);
+}
+
+ClearDepthCommand* Command::AsClearDepth() {
+  return static_cast<ClearDepthCommand*>(this);
+}
+
+ClearStencilCommand* Command::AsClearStencil() {
+  return static_cast<ClearStencilCommand*>(this);
+}
+
+ComputeCommand* Command::AsCompute() {
+  return static_cast<ComputeCommand*>(this);
+}
+
+DrawArraysCommand* Command::AsDrawArrays() {
+  return static_cast<DrawArraysCommand*>(this);
+}
+
+DrawRectCommand* Command::AsDrawRect() {
+  return static_cast<DrawRectCommand*>(this);
+}
+
+EntryPointCommand* Command::AsEntryPoint() {
+  return static_cast<EntryPointCommand*>(this);
+}
+
+PatchParameterVerticesCommand* Command::AsPatchParameterVertices() {
+  return static_cast<PatchParameterVerticesCommand*>(this);
+}
+
+ProbeCommand* Command::AsProbe() {
+  return static_cast<ProbeCommand*>(this);
+}
+
+BufferCommand* Command::AsBuffer() {
+  return static_cast<BufferCommand*>(this);
+}
+
+ProbeSSBOCommand* Command::AsProbeSSBO() {
+  return static_cast<ProbeSSBOCommand*>(this);
+}
+
+ToleranceCommand* Command::AsTolerance() {
+  return static_cast<ToleranceCommand*>(this);
+}
+
+DrawRectCommand::DrawRectCommand(PipelineData data)
+    : Command(Type::kDrawRect), data_(data) {}
+
+DrawRectCommand::~DrawRectCommand() = default;
+
+DrawArraysCommand::DrawArraysCommand(PipelineData data)
+    : Command(Type::kDrawArrays), data_(data) {}
+
+DrawArraysCommand::~DrawArraysCommand() = default;
+
+ComputeCommand::ComputeCommand(PipelineData data)
+    : Command(Type::kCompute), data_(data) {}
+
+ComputeCommand::~ComputeCommand() = default;
+
+ProbeCommand::ProbeCommand() : Command(Type::kProbe) {}
+
+ProbeCommand::~ProbeCommand() = default;
+
+BufferCommand::BufferCommand(BufferType type)
+    : Command(Type::kBuffer), buffer_type_(type) {}
+
+BufferCommand::~BufferCommand() = default;
+
+ProbeSSBOCommand::ProbeSSBOCommand() : Command(Type::kProbeSSBO) {}
+
+ProbeSSBOCommand::~ProbeSSBOCommand() = default;
+
+ToleranceCommand::ToleranceCommand() : Command(Type::kTolerance) {}
+
+ToleranceCommand::~ToleranceCommand() = default;
+
+ClearCommand::ClearCommand() : Command(Type::kClear) {}
+
+ClearCommand::~ClearCommand() = default;
+
+ClearColorCommand::ClearColorCommand() : Command(Type::kClearColor) {}
+
+ClearColorCommand::~ClearColorCommand() = default;
+
+ClearDepthCommand::ClearDepthCommand() : Command(Type::kClearDepth) {}
+
+ClearDepthCommand::~ClearDepthCommand() = default;
+
+ClearStencilCommand::ClearStencilCommand() : Command(Type::kClearStencil) {}
+
+ClearStencilCommand::~ClearStencilCommand() = default;
+
+PatchParameterVerticesCommand::PatchParameterVerticesCommand()
+    : Command(Type::kPatchParameterVertices) {}
+
+PatchParameterVerticesCommand::~PatchParameterVerticesCommand() = default;
+
+EntryPointCommand::EntryPointCommand() : Command(Type::kEntryPoint) {}
+
+EntryPointCommand::~EntryPointCommand() = default;
+
+}  // namespace amber
diff --git a/src/command.h b/src/command.h
new file mode 100644
index 0000000..7186b62
--- /dev/null
+++ b/src/command.h
@@ -0,0 +1,454 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_COMMAND_H_
+#define SRC_COMMAND_H_
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include "src/command_data.h"
+#include "src/datum_type.h"
+#include "src/pipeline_data.h"
+#include "src/shader_data.h"
+#include "src/value.h"
+
+namespace amber {
+
+class ClearCommand;
+class ClearColorCommand;
+class ClearDepthCommand;
+class ClearStencilCommand;
+class ComputeCommand;
+class DrawArraysCommand;
+class DrawRectCommand;
+class EntryPointCommand;
+class PatchParameterVerticesCommand;
+class ProbeCommand;
+class ProbeSSBOCommand;
+class BufferCommand;
+class ToleranceCommand;
+
+class Command {
+ public:
+  enum class Type : uint8_t {
+    kClear = 0,
+    kClearColor,
+    kClearDepth,
+    kClearStencil,
+    kCompute,
+    kDrawArrays,
+    kDrawRect,
+    kEntryPoint,
+    kPatchParameterVertices,
+    kPipelineProperties,
+    kProbe,
+    kProbeSSBO,
+    kBuffer,
+    kTolerance,
+  };
+
+  virtual ~Command();
+
+  bool IsDrawRect() const { return command_type_ == Type::kDrawRect; }
+  bool IsDrawArrays() const { return command_type_ == Type::kDrawArrays; }
+  bool IsCompute() const { return command_type_ == Type::kCompute; }
+  bool IsProbe() const { return command_type_ == Type::kProbe; }
+  bool IsProbeSSBO() const { return command_type_ == Type::kProbeSSBO; }
+  bool IsBuffer() const { return command_type_ == Type::kBuffer; }
+  bool IsTolerance() const { return command_type_ == Type::kTolerance; }
+  bool IsClear() const { return command_type_ == Type::kClear; }
+  bool IsClearColor() const { return command_type_ == Type::kClearColor; }
+  bool IsClearDepth() const { return command_type_ == Type::kClearDepth; }
+  bool IsClearStencil() const { return command_type_ == Type::kClearStencil; }
+  bool IsPatchParameterVertices() const {
+    return command_type_ == Type::kPatchParameterVertices;
+  }
+  bool IsEntryPoint() const { return command_type_ == Type::kEntryPoint; }
+
+  ClearCommand* AsClear();
+  ClearColorCommand* AsClearColor();
+  ClearDepthCommand* AsClearDepth();
+  ClearStencilCommand* AsClearStencil();
+  ComputeCommand* AsCompute();
+  DrawArraysCommand* AsDrawArrays();
+  DrawRectCommand* AsDrawRect();
+  EntryPointCommand* AsEntryPoint();
+  PatchParameterVerticesCommand* AsPatchParameterVertices();
+  ProbeCommand* AsProbe();
+  ProbeSSBOCommand* AsProbeSSBO();
+  BufferCommand* AsBuffer();
+  ToleranceCommand* AsTolerance();
+
+ protected:
+  Command(Type type);
+
+  Type command_type_;
+};
+
+class DrawRectCommand : public Command {
+ public:
+  DrawRectCommand(PipelineData data);
+  ~DrawRectCommand() override;
+
+  const PipelineData* GetPipelineData() const { return &data_; }
+
+  void EnableOrtho() { is_ortho_ = true; }
+  bool IsOrtho() const { return is_ortho_; }
+
+  void EnablePatch() { is_patch_ = true; }
+  bool IsPatch() const { return is_patch_; }
+
+  void SetX(float x) { x_ = x; }
+  float GetX() const { return x_; }
+
+  void SetY(float y) { y_ = y; }
+  float GetY() const { return y_; }
+
+  void SetWidth(float w) { width_ = w; }
+  float GetWidth() const { return width_; }
+
+  void SetHeight(float h) { height_ = h; }
+  float GetHeight() const { return height_; }
+
+ private:
+  PipelineData data_;
+  bool is_ortho_ = false;
+  bool is_patch_ = false;
+  float x_ = 0.0;
+  float y_ = 0.0;
+  float width_ = 0.0;
+  float height_ = 0.0;
+};
+
+class DrawArraysCommand : public Command {
+ public:
+  DrawArraysCommand(PipelineData data);
+  ~DrawArraysCommand() override;
+
+  const PipelineData* GetPipelineData() const { return &data_; }
+
+  void EnableIndexed() { is_indexed_ = true; }
+  bool IsIndexed() const { return is_indexed_; }
+
+  void EnableInstanced() { is_instanced_ = true; }
+  bool IsInstanced() const { return is_instanced_; }
+
+  void SetTopology(Topology topo) { topology_ = topo; }
+  Topology GetTopology() const { return topology_; }
+
+  void SetFirstVertexIndex(uint32_t idx) { first_vertex_index_ = idx; }
+  uint32_t GetFirstVertexIndex() const { return first_vertex_index_; }
+
+  void SetVertexCount(uint32_t count) { vertex_count_ = count; }
+  uint32_t GetVertexCount() const { return vertex_count_; }
+
+  void SetInstanceCount(uint32_t count) { instance_count_ = count; }
+  uint32_t GetInstanceCount() const { return instance_count_; }
+
+ private:
+  PipelineData data_;
+  bool is_indexed_ = false;
+  bool is_instanced_ = false;
+  Topology topology_ = Topology::kUnknown;
+  uint32_t first_vertex_index_ = 0;
+  uint32_t vertex_count_ = 0;
+  uint32_t instance_count_ = 0;
+};
+
+class ComputeCommand : public Command {
+ public:
+  ComputeCommand(PipelineData data);
+  ~ComputeCommand() override;
+
+  const PipelineData* GetPipelineData() const { return &data_; }
+
+  void SetX(uint32_t x) { x_ = x; }
+  uint32_t GetX() const { return x_; }
+
+  void SetY(uint32_t y) { y_ = y; }
+  uint32_t GetY() const { return y_; }
+
+  void SetZ(uint32_t z) { z_ = z; }
+  uint32_t GetZ() const { return z_; }
+
+ private:
+  PipelineData data_;
+
+  uint32_t x_ = 0;
+  uint32_t y_ = 0;
+  uint32_t z_ = 0;
+};
+
+class ProbeCommand : public Command {
+ public:
+  ProbeCommand();
+  ~ProbeCommand() override;
+
+  void SetWholeWindow() { is_whole_window_ = true; }
+  bool IsWholeWindow() const { return is_whole_window_; }
+
+  void SetRelative() { is_relative_ = true; }
+  bool IsRelative() const { return is_relative_; }
+
+  void SetIsRGBA() { color_format_ = ColorFormat::kRGBA; }
+  bool IsRGBA() const { return color_format_ == ColorFormat::kRGBA; }
+
+  void SetX(float x) { x_ = x; }
+  float GetX() const { return x_; }
+
+  void SetY(float y) { y_ = y; }
+  float GetY() const { return y_; }
+
+  void SetWidth(float w) { width_ = w; }
+  float GetWidth() const { return width_; }
+
+  void SetHeight(float h) { height_ = h; }
+  float GetHeight() const { return height_; }
+
+  void SetR(float r) { r_ = r; }
+  float GetR() const { return r_; }
+
+  void SetG(float g) { g_ = g; }
+  float GetG() const { return g_; }
+
+  void SetB(float b) { b_ = b; }
+  float GetB() const { return b_; }
+
+  void SetA(float a) { a_ = a; }
+  float GetA() const { return a_; }
+
+ private:
+  enum class ColorFormat {
+    kRGB = 0,
+    kRGBA,
+  };
+
+  bool is_whole_window_ = false;
+  bool is_relative_ = false;
+  ColorFormat color_format_ = ColorFormat::kRGB;
+
+  float x_ = 0.0;
+  float y_ = 0.0;
+  float width_ = 1.0;
+  float height_ = 1.0;
+
+  float r_ = 0.0;
+  float g_ = 0.0;
+  float b_ = 0.0;
+  float a_ = 0.0;
+};
+
+class ProbeSSBOCommand : public Command {
+ public:
+  enum class Comparator {
+    kEqual,
+    kNotEqual,
+    kFuzzyEqual,
+    kLess,
+    kLessOrEqual,
+    kGreater,
+    kGreaterOrEqual
+  };
+
+  ProbeSSBOCommand();
+  ~ProbeSSBOCommand() override;
+
+  void SetComparator(Comparator comp) { comparator_ = comp; }
+  Comparator GetComparator() const { return comparator_; }
+
+  void SetDescriptorSet(uint32_t id) { descriptor_set_id_ = id; }
+  uint32_t GetDescriptorSet() const { return descriptor_set_id_; }
+
+  void SetBinding(uint32_t id) { binding_num_ = id; }
+  uint32_t GetBinding() const { return binding_num_; }
+
+  void SetOffset(uint32_t offset) { offset_ = offset; }
+  uint32_t GetOffset() const { return offset_; }
+
+  void SetDatumType(const DatumType& type) { datum_type_ = type; }
+  const DatumType& GetDatumType() const { return datum_type_; }
+
+  void SetValues(std::vector<Value>&& values) { values_ = std::move(values); }
+  const std::vector<Value>& GetValues() const { return values_; }
+
+ private:
+  Comparator comparator_ = Comparator::kEqual;
+  uint32_t descriptor_set_id_ = 0;
+  uint32_t binding_num_ = 0;
+  uint32_t offset_ = 0;
+  DatumType datum_type_;
+  std::vector<Value> values_;
+};
+
+class BufferCommand : public Command {
+ public:
+  enum class BufferType {
+    kSSBO,
+    kUniform,
+    kPushConstant,
+  };
+
+  BufferCommand(BufferType type);
+  ~BufferCommand() override;
+
+  bool IsSSBO() const { return buffer_type_ == BufferType::kSSBO; }
+  bool IsUniform() const { return buffer_type_ == BufferType::kUniform; }
+  bool IsPushConstant() const {
+    return buffer_type_ == BufferType::kPushConstant;
+  }
+
+  void SetIsSubdata() { is_subdata_ = true; }
+  bool IsSubdata() const { return is_subdata_; }
+
+  void SetDescriptorSet(uint32_t set) { descriptor_set_ = set; }
+  uint32_t GetDescriptorSet() const { return descriptor_set_; }
+
+  void SetBinding(uint32_t num) { binding_num_ = num; }
+  uint32_t GetBinding() const { return binding_num_; }
+
+  void SetOffset(uint32_t offset) { offset_ = offset; }
+  uint32_t GetOffset() const { return offset_; }
+
+  void SetSize(uint32_t size) { size_ = size; }
+  uint32_t GetSize() const { return size_; }
+
+  void SetDatumType(const DatumType& type) { datum_type_ = type; }
+  const DatumType& GetDatumType() const { return datum_type_; }
+
+  void SetValues(std::vector<Value>&& values) { values_ = std::move(values); }
+  const std::vector<Value>& GetValues() const { return values_; }
+
+ private:
+  BufferType buffer_type_;
+  bool is_subdata_ = false;
+  uint32_t descriptor_set_ = 0;
+  uint32_t binding_num_ = 0;
+  uint32_t size_ = 0;
+  uint32_t offset_ = 0;
+  DatumType datum_type_;
+  std::vector<Value> values_;
+};
+
+class ToleranceCommand : public Command {
+ public:
+  struct Tolerance {
+    Tolerance(bool percent, double val) : is_percent(percent), value(val) {}
+
+    bool is_percent = false;
+    double value = 0.0;
+  };
+
+  ToleranceCommand();
+  ~ToleranceCommand() override;
+
+  void AddPercentTolerance(double value) {
+    tolerances_.emplace_back(Tolerance{true, value});
+  }
+  void AddValueTolerance(double value) {
+    tolerances_.emplace_back(Tolerance{false, value});
+  }
+
+  const std::vector<Tolerance>& GetTolerances() const { return tolerances_; }
+
+ private:
+  std::vector<Tolerance> tolerances_;
+};
+
+class ClearCommand : public Command {
+ public:
+  ClearCommand();
+  ~ClearCommand() override;
+};
+
+class ClearColorCommand : public Command {
+ public:
+  ClearColorCommand();
+  ~ClearColorCommand() override;
+
+  void SetR(float r) { r_ = r; }
+  float GetR() const { return r_; }
+
+  void SetG(float g) { g_ = g; }
+  float GetG() const { return g_; }
+
+  void SetB(float b) { b_ = b; }
+  float GetB() const { return b_; }
+
+  void SetA(float a) { a_ = a; }
+  float GetA() const { return a_; }
+
+ private:
+  float r_ = 0.0;
+  float g_ = 0.0;
+  float b_ = 0.0;
+  float a_ = 0.0;
+};
+
+class ClearDepthCommand : public Command {
+ public:
+  ClearDepthCommand();
+  ~ClearDepthCommand() override;
+
+  void SetValue(float val) { value_ = val; }
+  float GetValue() const { return value_; }
+
+ private:
+  float value_ = 0.0;
+};
+
+class ClearStencilCommand : public Command {
+ public:
+  ClearStencilCommand();
+  ~ClearStencilCommand() override;
+
+  void SetValue(uint32_t val) { value_ = val; }
+  uint32_t GetValue() const { return value_; }
+
+ private:
+  uint32_t value_ = 0;
+};
+
+class PatchParameterVerticesCommand : public Command {
+ public:
+  PatchParameterVerticesCommand();
+  ~PatchParameterVerticesCommand() override;
+
+  void SetControlPointCount(uint32_t count) { control_point_count_ = count; }
+  uint32_t GetControlPointCount() const { return control_point_count_; }
+
+ private:
+  uint32_t control_point_count_ = 0;
+};
+
+class EntryPointCommand : public Command {
+ public:
+  EntryPointCommand();
+  ~EntryPointCommand() override;
+
+  void SetShaderType(ShaderType type) { shader_type_ = type; }
+  ShaderType GetShaderType() const { return shader_type_; }
+
+  void SetEntryPointName(const std::string& name) { entry_point_name_ = name; }
+  std::string GetEntryPointName() const { return entry_point_name_; }
+
+ private:
+  ShaderType shader_type_ = ShaderType::kVertex;
+  std::string entry_point_name_;
+};
+
+}  // namespace amber
+
+#endif  // SRC_COMMAND_H_
diff --git a/src/command_data.cc b/src/command_data.cc
new file mode 100644
index 0000000..6037907
--- /dev/null
+++ b/src/command_data.cc
@@ -0,0 +1,57 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/command_data.h"
+
+namespace amber {
+
+Topology NameToTopology(const std::string& name) {
+  const static struct {
+    const char* name;
+    Topology val;
+  } topologies[] = {
+      {"PATCH_LIST", Topology::kPatchList},
+      {"POINT_LIST", Topology::kPointList},
+      {"GL_LINE_STRIP_ADJACENCY", Topology::kLineStripWithAdjacency},
+      {"GL_LINE_STRIP", Topology::kLineStrip},
+      {"GL_LINES", Topology::kLineList},
+      {"GL_LINES_ADJACENCY", Topology::kLineListWithAdjacency},
+      {"GL_PATCHES", Topology::kPatchList},
+      {"GL_POINTS", Topology::kPointList},
+      {"GL_TRIANGLE_STRIP", Topology::kTriangleStrip},
+      {"GL_TRIANGLE_FAN", Topology::kTriangleFan},
+      {"GL_TRIANGLES", Topology::kTriangleList},
+      {"GL_TRIANGLES_ADJACENCY", Topology::kTriangleListWithAdjacency},
+      {"GL_TRIANGLE_STRIP_ADJACENCY", Topology::kTriangleStripWithAdjacency},
+      {"LINE_LIST", Topology::kLineList},
+      {"LINE_LIST_WITH_ADJACENCY", Topology::kLineListWithAdjacency},
+      {"LINE_STRIP", Topology::kLineStrip},
+      {"LINE_STRIP_WITH_ADJACENCY", Topology::kLineStripWithAdjacency},
+      {"TRIANGLE_FAN", Topology::kTriangleFan},
+      {"TRIANGLE_LIST", Topology::kTriangleList},
+      {"TRIANGLE_LIST_WITH_ADJACENCY", Topology::kTriangleListWithAdjacency},
+      {"TRIANGLE_STRIP", Topology::kTriangleStrip},
+      {"TRIANGLE_STRIP_WITH_ADJACENCY", Topology::kTriangleStripWithAdjacency},
+  };
+
+  // TODO(dsinclair): Make smarter if needed
+  for (auto& topo : topologies) {
+    if (topo.name == name)
+      return topo.val;
+  }
+
+  return Topology::kUnknown;
+}
+
+}  // namespace amber
diff --git a/src/command_data.h b/src/command_data.h
new file mode 100644
index 0000000..430533c
--- /dev/null
+++ b/src/command_data.h
@@ -0,0 +1,184 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_COMMAND_DATA_H_
+#define SRC_COMMAND_DATA_H_
+
+#include <cstdint>
+#include <string>
+
+namespace amber {
+
+enum class Topology : uint8_t {
+  kUnknown = 0,
+  kPointList,
+  kLineList,
+  kLineStrip,
+  kTriangleList,
+  kTriangleStrip,
+  kTriangleFan,
+  kLineListWithAdjacency,
+  kLineStripWithAdjacency,
+  kTriangleListWithAdjacency,
+  kTriangleStripWithAdjacency,
+  kPatchList,
+};
+
+enum class PolygonMode : uint8_t {
+  kFill = 0,
+  kLine,
+  kPoint,
+};
+
+enum class CullMode : uint8_t {
+  kNone = 0,
+  kFront,
+  kBack,
+  kFrontAndBack,
+};
+
+enum class FrontFace : uint8_t {
+  kCounterClockwise = 0,
+  kClockwise,
+};
+
+enum ColorMask {
+  kColorMaskR = 1 << 0,
+  kColorMaskG = 1 << 1,
+  kColorMaskB = 1 << 2,
+  kColorMaskA = 1 << 3,
+};
+
+enum class CompareOp : uint8_t {
+  kNever = 0,
+  kLess,
+  kEqual,
+  kLessOrEqual,
+  kGreater,
+  kNotEqual,
+  kGreaterOrEqual,
+  kAlways,
+};
+
+enum class StencilOp : uint8_t {
+  kKeep = 0,
+  kZero,
+  kReplace,
+  kIncrementAndClamp,
+  kDecrementAndClamp,
+  kInvert,
+  kIncrementAndWrap,
+  kDecrementAndWrap,
+};
+
+enum class LogicOp : uint8_t {
+  kClear = 0,
+  kAnd,
+  kAndReverse,
+  kCopy,
+  kAndInverted,
+  kNoOp,
+  kXor,
+  kOr,
+  kNor,
+  kEquivalent,
+  kInvert,
+  kOrReverse,
+  kCopyInverted,
+  kOrInverted,
+  kNand,
+  kSet,
+};
+
+enum class BlendOp : uint8_t {
+  kAdd = 0,
+  kSubtract,
+  kReverseSubtract,
+  kMin,
+  kMax,
+  kZero,
+  kSrc,
+  kDst,
+  kSrcOver,
+  kDstOver,
+  kSrcIn,
+  kDstIn,
+  kSrcOut,
+  kDstOut,
+  kSrcAtop,
+  kDstAtop,
+  kXor,
+  kMultiply,
+  kScreen,
+  kOverlay,
+  kDarken,
+  kLighten,
+  kColorDodge,
+  kColorBurn,
+  kHardLight,
+  kSoftLight,
+  kDifference,
+  kExclusion,
+  kInvert,
+  kInvertRGB,
+  kLinearDodge,
+  kLinearBurn,
+  kVividLight,
+  kLinearLight,
+  kPinLight,
+  kHardMix,
+  kHslHue,
+  kHslSaturation,
+  kHslColor,
+  kHslLuminosity,
+  kPlus,
+  kPlusClamped,
+  kPlusClampedAlpha,
+  kPlusDarker,
+  kMinus,
+  kMinusClamped,
+  kContrast,
+  kInvertOvg,
+  kRed,
+  kGreen,
+  kBlue,
+};
+
+enum class BlendFactor : uint8_t {
+  kZero = 0,
+  kOne,
+  kSrcColor,
+  kOneMinusSrcColor,
+  kDstColor,
+  kOneMinusDstColor,
+  kSrcAlpha,
+  kOneMinusSrcAlpha,
+  kDstAlpha,
+  kOneMinusDstAlpha,
+  kConstantColor,
+  kOneMinusConstantColor,
+  kConstantAlpha,
+  kOneMinusConstantAlpha,
+  kSrcAlphaSaturate,
+  kSrc1Color,
+  kOneMinusSrc1Color,
+  kSrc1Alpha,
+  kOneMinusSrc1Alpha,
+};
+
+Topology NameToTopology(const std::string& name);
+
+}  // namespace amber
+
+#endif  // SRC_COMMAND_DATA_H_
diff --git a/src/command_data_test.cc b/src/command_data_test.cc
new file mode 100644
index 0000000..c465a17
--- /dev/null
+++ b/src/command_data_test.cc
@@ -0,0 +1,61 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/command_data.h"
+#include "gtest/gtest.h"
+
+namespace amber {
+
+using CommandDataTest = testing::Test;
+
+TEST_F(CommandDataTest, TopologyConversions) {
+  const struct {
+    const char* name;
+    Topology val;
+  } topologies[] = {
+      {"PATCH_LIST", Topology::kPatchList},
+      {"POINT_LIST", Topology::kPointList},
+      {"GL_LINE_STRIP_ADJACENCY", Topology::kLineStripWithAdjacency},
+      {"GL_LINE_STRIP", Topology::kLineStrip},
+      {"GL_LINES", Topology::kLineList},
+      {"GL_LINES_ADJACENCY", Topology::kLineListWithAdjacency},
+      {"GL_PATCHES", Topology::kPatchList},
+      {"GL_POINTS", Topology::kPointList},
+      {"GL_TRIANGLE_STRIP", Topology::kTriangleStrip},
+      {"GL_TRIANGLE_FAN", Topology::kTriangleFan},
+      {"GL_TRIANGLES", Topology::kTriangleList},
+      {"GL_TRIANGLES_ADJACENCY", Topology::kTriangleListWithAdjacency},
+      {"GL_TRIANGLE_STRIP_ADJACENCY", Topology::kTriangleStripWithAdjacency},
+      {"LINE_LIST", Topology::kLineList},
+      {"LINE_LIST_WITH_ADJACENCY", Topology::kLineListWithAdjacency},
+      {"LINE_STRIP", Topology::kLineStrip},
+      {"LINE_STRIP_WITH_ADJACENCY", Topology::kLineStripWithAdjacency},
+      {"TRIANGLE_FAN", Topology::kTriangleFan},
+      {"TRIANGLE_LIST", Topology::kTriangleList},
+      {"TRIANGLE_LIST_WITH_ADJACENCY", Topology::kTriangleListWithAdjacency},
+      {"TRIANGLE_STRIP", Topology::kTriangleStrip},
+      {"TRIANGLE_STRIP_WITH_ADJACENCY", Topology::kTriangleStripWithAdjacency},
+  };
+
+  for (const auto& topo : topologies) {
+    EXPECT_EQ(topo.val, NameToTopology(topo.name));
+  }
+}
+
+TEST_F(CommandDataTest, TopologyInvalid) {
+  EXPECT_EQ(Topology::kUnknown, NameToTopology(""));
+  EXPECT_EQ(Topology::kUnknown, NameToTopology("Invalid_Topology"));
+}
+
+}  // namespace amber
diff --git a/src/datum_type.cc b/src/datum_type.cc
new file mode 100644
index 0000000..431bb72
--- /dev/null
+++ b/src/datum_type.cc
@@ -0,0 +1,24 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+#include "src/datum_type.h"
+
+namespace amber {
+
+DatumType::DatumType() = default;
+
+DatumType::~DatumType() = default;
+
+DatumType& DatumType::operator=(const DatumType&) = default;
+
+}  // namespace amber
diff --git a/src/datum_type.h b/src/datum_type.h
new file mode 100644
index 0000000..b6c5a6c
--- /dev/null
+++ b/src/datum_type.h
@@ -0,0 +1,70 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_DATUM_TYPE_H_
+#define SRC_DATUM_TYPE_H_
+
+#include <cstdint>
+
+namespace amber {
+
+enum class DataType {
+  kInt8 = 0,
+  kInt16,
+  kInt32,
+  kInt64,
+  kUint8,
+  kUint16,
+  kUint32,
+  kUint64,
+  kFloat,
+  kDouble,
+};
+
+class DatumType {
+ public:
+  DatumType();
+  ~DatumType();
+
+  DatumType& operator=(const DatumType&);
+
+  bool IsInt8() const { return type_ == DataType::kInt8; }
+  bool IsInt16() const { return type_ == DataType::kInt16; }
+  bool IsInt32() const { return type_ == DataType::kInt32; }
+  bool IsInt64() const { return type_ == DataType::kInt64; }
+  bool IsUint8() const { return type_ == DataType::kUint8; }
+  bool IsUint16() const { return type_ == DataType::kUint16; }
+  bool IsUint32() const { return type_ == DataType::kUint32; }
+  bool IsUint64() const { return type_ == DataType::kUint64; }
+  bool IsFloat() const { return type_ == DataType::kFloat; }
+  bool IsDouble() const { return type_ == DataType::kDouble; }
+
+  void SetType(DataType type) { type_ = type; }
+  DataType GetType() const { return type_; }
+
+  void SetColumnCount(uint32_t count) { column_count_ = count; }
+  uint32_t ColumnCount() const { return column_count_; }
+
+  void SetRowCount(uint32_t count) { row_count_ = count; }
+  uint32_t RowCount() const { return row_count_; }
+
+ private:
+  DataType type_ = DataType::kUint8;
+  uint32_t column_count_ = 1;
+  uint32_t row_count_ = 1;
+};
+
+}  // namespace amber
+
+#endif  // SRC_DATUM_TYPE_H_
diff --git a/src/dawn/find_dawn.cmake b/src/dawn/find_dawn.cmake
new file mode 100644
index 0000000..aed0ebc
--- /dev/null
+++ b/src/dawn/find_dawn.cmake
@@ -0,0 +1,68 @@
+# Copyright 2018 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 implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+# Include this file to find Dawn and and set up compilation and linking.
+
+# Exports these settings to the includer:
+#    Boolean Dawn_FOUND indicates whether we found Dawn.
+#    If Dawn was found, then library dependency Dawn::dawn_native will be set up.
+set(Dawn_FOUND FALSE)
+
+# Setup via CMake setting variables:
+#
+#   Separately specify the directory locations of the Dawn headers and
+#   the dawn_native library.
+#
+#     -DDawn_INCLUDE_DIR=<directory containing dawn/dawn_export.h>
+#     -DDawn_GEN_INCLUDE_DIR=<directory containing dawn/dawn.h>
+#     -DDawn_LIBRARY_DIR=<directory containing dawn_native>
+
+
+find_path(Dawn_INCLUDE_DIR
+  NAMES dawn/dawn_export.h
+  PATHS
+    "${Dawn_INCLUDE_DIR}"
+  )
+find_path(Dawn_GEN_INCLUDE_DIR
+  NAMES dawn/dawn.h dawn/dawncpp.h
+  PATHS
+    "${Dawn_GEN_INCLUDE_DIR}"
+  )
+find_library(Dawn_LIBRARY
+  NAMES dawn_native
+  PATHS
+    "${Dawn_LIBRARY_DIR}"
+  )
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(Dawn
+  DEFAULT_MSG
+  Dawn_INCLUDE_DIR Dawn_GEN_INCLUDE_DIR Dawn_LIBRARY)
+
+if(${Dawn_FOUND} AND NOT TARGET Dawn::dawn_native)
+  add_library(Dawn::dawn_native UNKNOWN IMPORTED)
+  set_target_properties(Dawn::dawn_native PROPERTIES
+    IMPORTED_LOCATION "${Dawn_LIBRARY}"
+    INTERFACE_INCLUDE_DIRECTORIES "${Dawn_INCLUDE_DIR}"
+    INTERFACE_INCLUDE_DIRECTORIES "${Dawn_GEN_INCLUDE_DIR}")
+endif()
+
+if (${Dawn_FOUND})
+  message(STATUS "Amber: Using Dawn headers at ${Dawn_INCLUDE_DIR}")
+  message(STATUS "Amber: Using Dawn generated headers at ${Dawn_GEN_INCLUDE_DIR}")
+  message(STATUS "Amber: Using Dawn library ${Dawn_LIBRARY}")
+else()
+  message(STATUS "Amber: Did not find Dawn")
+endif()
diff --git a/src/engine.cc b/src/engine.cc
new file mode 100644
index 0000000..ff90a92
--- /dev/null
+++ b/src/engine.cc
@@ -0,0 +1,45 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/engine.h"
+
+#include "src/make_unique.h"
+
+#if AMBER_ENGINE_VULKAN
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant"
+#include "src/vulkan/engine_vulkan.h"
+#pragma clang diagnostic pop
+#endif  // AMBER_ENGINE_VULKAN
+
+namespace amber {
+
+// static
+std::unique_ptr<Engine> Engine::Create(EngineType type) {
+  std::unique_ptr<Engine> engine;
+  switch (type) {
+    case EngineType::kVulkan:
+#if AMBER_ENGINE_VULKAN
+      engine = MakeUnique<vulkan::EngineVulkan>();
+#endif  // AMBER_ENGINE_VULKAN
+      break;
+  }
+  return engine;
+}
+
+Engine::Engine() = default;
+
+Engine::~Engine() = default;
+
+}  // namespace amber
diff --git a/src/engine.h b/src/engine.h
new file mode 100644
index 0000000..a58a41e
--- /dev/null
+++ b/src/engine.h
@@ -0,0 +1,115 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef ENGINE_H_
+#define ENGINE_H_
+
+#include <memory>
+#include <vector>
+
+#include "amber/amber.h"
+#include "amber/result.h"
+#include "src/buffer_data.h"
+#include "src/command.h"
+#include "src/feature.h"
+#include "src/format.h"
+#include "src/shader_data.h"
+
+namespace amber {
+
+enum class PipelineType : uint8_t {
+  kCompute = 0,
+  kGraphics,
+};
+
+class Engine {
+ public:
+  static std::unique_ptr<Engine> Create(EngineType type);
+
+  virtual ~Engine();
+
+  // Initialize the engine.
+  virtual Result Initialize() = 0;
+
+  // Initialize the engine with the provided device. The device is _not_ owned
+  // by the engine and should not be destroyed.
+  virtual Result InitializeWithDevice(void* default_device) = 0;
+
+  // Shutdown the engine and cleanup any resources.
+  virtual Result Shutdown() = 0;
+
+  // Enable |feature|. If the feature requires a pixel format it will be
+  // provided in |format|, otherwise |format| is a nullptr.
+  virtual Result AddRequirement(Feature feature, const Format* format) = 0;
+
+  // Create graphics pipeline.
+  virtual Result CreatePipeline(PipelineType type) = 0;
+
+  // Set the shader of |type| to the binary |data|.
+  virtual Result SetShader(ShaderType type,
+                           const std::vector<uint32_t>& data) = 0;
+
+  // Provides the data for a given buffer to be bound at the given location.
+  virtual Result SetBuffer(BufferType type,
+                           uint8_t location,
+                           const Format& format,
+                           const std::vector<Value>& data) = 0;
+
+  // Execute the clear color command
+  virtual Result ExecuteClearColor(const ClearColorCommand* cmd) = 0;
+
+  // Execute the clear stencil command
+  virtual Result ExecuteClearStencil(const ClearStencilCommand* cmd) = 0;
+
+  // Execute the clear depth command
+  virtual Result ExecuteClearDepth(const ClearDepthCommand* cmd) = 0;
+
+  // Execute the clear command
+  virtual Result ExecuteClear(const ClearCommand* cmd) = 0;
+
+  // Execute the draw rect command
+  virtual Result ExecuteDrawRect(const DrawRectCommand* cmd) = 0;
+
+  // Execute the draw arrays command
+  virtual Result ExecuteDrawArrays(const DrawArraysCommand* cmd) = 0;
+
+  // Execute the compute command
+  virtual Result ExecuteCompute(const ComputeCommand* cmd) = 0;
+
+  // Execute the entry point command
+  virtual Result ExecuteEntryPoint(const EntryPointCommand* cmd) = 0;
+
+  // Execute the patch command
+  virtual Result ExecutePatchParameterVertices(
+      const PatchParameterVerticesCommand* cmd) = 0;
+
+  // Execute the probe command
+  virtual Result ExecuteProbe(const ProbeCommand* cmd) = 0;
+
+  // Execute the probe ssbo command
+  virtual Result ExecuteProbeSSBO(const ProbeSSBOCommand* cmd) = 0;
+
+  // Execute the buffer command
+  virtual Result ExecuteBuffer(const BufferCommand* cmd) = 0;
+
+  // Execute the tolerance command
+  virtual Result ExecuteTolerance(const ToleranceCommand* cmd) = 0;
+
+ protected:
+  Engine();
+};
+
+}  // namespace amber
+
+#endif  // ENGINE_H_
diff --git a/src/executor.cc b/src/executor.cc
new file mode 100644
index 0000000..249a89f
--- /dev/null
+++ b/src/executor.cc
@@ -0,0 +1,23 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/executor.h"
+
+namespace amber {
+
+Executor::Executor() = default;
+
+Executor::~Executor() = default;
+
+}  // namespace amber
diff --git a/src/executor.h b/src/executor.h
new file mode 100644
index 0000000..a73d5ee
--- /dev/null
+++ b/src/executor.h
@@ -0,0 +1,36 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_EXECUTOR_H_
+#define SRC_EXECUTOR_H_
+
+#include "amber/result.h"
+#include "src/engine.h"
+#include "src/script.h"
+
+namespace amber {
+
+class Executor {
+ public:
+  virtual ~Executor();
+
+  virtual Result Execute(Engine*, const Script*) = 0;
+
+ protected:
+  Executor();
+};
+
+}  // namespace amber
+
+#endif  // SRC_EXECUTOR_H_
diff --git a/src/feature.h b/src/feature.h
new file mode 100644
index 0000000..d6c030c
--- /dev/null
+++ b/src/feature.h
@@ -0,0 +1,83 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_FEATURE_H_
+#define SRC_FEATURE_H_
+
+namespace amber {
+
+enum class Feature {
+  kUnknown = 0,
+  kRobustBufferAccess,
+  kFullDrawIndexUint32,
+  kImageCubeArray,
+  kIndependentBlend,
+  kGeometryShader,
+  kTessellationShader,
+  kSampleRateShading,
+  kDualSrcBlend,
+  kLogicOp,
+  kMultiDrawIndirect,
+  kDrawIndirectFirstInstance,
+  kDepthClamp,
+  kDepthBiasClamp,
+  kFillModeNonSolid,
+  kDepthBounds,
+  kWideLines,
+  kLargePoints,
+  kAlphaToOne,
+  kMultiViewport,
+  kSamplerAnisotropy,
+  kTextureCompressionETC2,
+  kTextureCompressionASTC_LDR,
+  kTextureCompressionBC,
+  kOcclusionQueryPrecise,
+  kPipelineStatisticsQuery,
+  kVertexPipelineStoresAndAtomics,
+  kFragmentStoresAndAtomics,
+  kShaderTessellationAndGeometryPointSize,
+  kShaderImageGatherExtended,
+  kShaderStorageImageExtendedFormats,
+  kShaderStorageImageMultisample,
+  kShaderStorageImageReadWithoutFormat,
+  kShaderStorageImageWriteWithoutFormat,
+  kShaderUniformBufferArrayDynamicIndexing,
+  kShaderSampledImageArrayDynamicIndexing,
+  kShaderStorageBufferArrayDynamicIndexing,
+  kShaderStorageImageArrayDynamicIndexing,
+  kShaderClipDistance,
+  kShaderCullDistance,
+  kShaderFloat64,
+  kShaderInt64,
+  kShaderInt16,
+  kShaderResourceResidency,
+  kShaderResourceMinLod,
+  kSparseBinding,
+  kSparseResidencyBuffer,
+  kSparseResidencyImage2D,
+  kSparseResidencyImage3D,
+  kSparseResidency2Samples,
+  kSparseResidency4Samples,
+  kSparseResidency8Samples,
+  kSparseResidency16Samples,
+  kSparseResidencyAliased,
+  kVariableMultisampleRate,
+  kInheritedQueries,
+  kFramebuffer,
+  kDepthStencil,
+};
+
+}  // namespace amber
+
+#endif  // SRC_FEATURE_H_
diff --git a/src/format.cc b/src/format.cc
new file mode 100644
index 0000000..0881379
--- /dev/null
+++ b/src/format.cc
@@ -0,0 +1,25 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/format.h"
+
+namespace amber {
+
+Format::Format() = default;
+
+Format::Format(const Format&) = default;
+
+Format::~Format() = default;
+
+}  // namespace amber
diff --git a/src/format.h b/src/format.h
new file mode 100644
index 0000000..fd900a8
--- /dev/null
+++ b/src/format.h
@@ -0,0 +1,67 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_FORMAT_H_
+#define SRC_FORMAT_H_
+
+#include <cstdint>
+#include <vector>
+
+#include "src/format_data.h"
+
+namespace amber {
+
+class Format {
+ public:
+  struct Component {
+    Component(FormatComponentType t, FormatMode m, uint8_t bits)
+        : type(t), mode(m), num_bits(bits) {}
+
+    FormatComponentType type;
+    FormatMode mode;
+    uint8_t num_bits;
+  };
+
+  Format();
+  Format(const Format&);
+  ~Format();
+
+  void SetFormatType(FormatType type) { type_ = type; }
+  FormatType GetFormatType() const { return type_; }
+
+  void SetPackSize(uint8_t size) { pack_size_ = size; }
+  uint8_t GetPackSize() const { return pack_size_; }
+
+  void AddComponent(FormatComponentType type, FormatMode mode, uint8_t size) {
+    components_.emplace_back(type, mode, size);
+  }
+  const std::vector<Component>& GetComponents() const { return components_; }
+
+  uint32_t GetByteSize() const {
+    uint32_t bits = 0;
+    for (uint32_t j = 0; j < components_.size(); ++j) {
+      bits += components_[j].num_bits;
+    }
+    return bits / 8;
+  }
+
+ private:
+  FormatType type_;
+  uint8_t pack_size_ = 0;
+  std::vector<Component> components_;
+};
+
+}  // namespace amber
+
+#endif  // SRC_FORMAT_H_
diff --git a/src/format_data.h b/src/format_data.h
new file mode 100644
index 0000000..98237bf
--- /dev/null
+++ b/src/format_data.h
@@ -0,0 +1,173 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_FORMAT_DATA_H_
+#define SRC_FORMAT_DATA_H_
+
+enum class FormatComponentType {
+  kA = 0,
+  kR,
+  kG,
+  kB,
+  kX,
+  kD,
+  kS,
+};
+
+enum class FormatMode {
+  kUNorm = 0,
+  kUInt,
+  kUFloat,
+  kUScaled,
+  kSInt,
+  kSNorm,
+  kSScaled,
+  kSFloat,
+  kSRGB,
+};
+
+enum class FormatType {
+  kUnknown = 0,
+  kA1R5G5B5_UNORM_PACK16,
+  kA2B10G10R10_SINT_PACK32,
+  kA2B10G10R10_SNORM_PACK32,
+  kA2B10G10R10_SSCALED_PACK32,
+  kA2B10G10R10_UINT_PACK32,
+  kA2B10G10R10_UNORM_PACK32,
+  kA2B10G10R10_USCALED_PACK32,
+  kA2R10G10B10_SINT_PACK32,
+  kA2R10G10B10_SNORM_PACK32,
+  kA2R10G10B10_SSCALED_PACK32,
+  kA2R10G10B10_UINT_PACK32,
+  kA2R10G10B10_UNORM_PACK32,
+  kA2R10G10B10_USCALED_PACK32,
+  kA8B8G8R8_SINT_PACK32,
+  kA8B8G8R8_SNORM_PACK32,
+  kA8B8G8R8_SRGB_PACK32,
+  kA8B8G8R8_SSCALED_PACK32,
+  kA8B8G8R8_UINT_PACK32,
+  kA8B8G8R8_UNORM_PACK32,
+  kA8B8G8R8_USCALED_PACK32,
+  kB10G11R11_UFLOAT_PACK32,
+  kB4G4R4A4_UNORM_PACK16,
+  kB5G5R5A1_UNORM_PACK16,
+  kB5G6R5_UNORM_PACK16,
+  kB8G8R8A8_SINT,
+  kB8G8R8A8_SNORM,
+  kB8G8R8A8_SRGB,
+  kB8G8R8A8_SSCALED,
+  kB8G8R8A8_UINT,
+  kB8G8R8A8_UNORM,
+  kB8G8R8A8_USCALED,
+  kB8G8R8_SINT,
+  kB8G8R8_SNORM,
+  kB8G8R8_SRGB,
+  kB8G8R8_SSCALED,
+  kB8G8R8_UINT,
+  kB8G8R8_UNORM,
+  kB8G8R8_USCALED,
+  kD16_UNORM,
+  kD16_UNORM_S8_UINT,
+  kD24_UNORM_S8_UINT,
+  kD32_SFLOAT,
+  kD32_SFLOAT_S8_UINT,
+  kR16G16B16A16_SFLOAT,
+  kR16G16B16A16_SINT,
+  kR16G16B16A16_SNORM,
+  kR16G16B16A16_SSCALED,
+  kR16G16B16A16_UINT,
+  kR16G16B16A16_UNORM,
+  kR16G16B16A16_USCALED,
+  kR16G16B16_SFLOAT,
+  kR16G16B16_SINT,
+  kR16G16B16_SNORM,
+  kR16G16B16_SSCALED,
+  kR16G16B16_UINT,
+  kR16G16B16_UNORM,
+  kR16G16B16_USCALED,
+  kR16G16_SFLOAT,
+  kR16G16_SINT,
+  kR16G16_SNORM,
+  kR16G16_SSCALED,
+  kR16G16_UINT,
+  kR16G16_UNORM,
+  kR16G16_USCALED,
+  kR16_SFLOAT,
+  kR16_SINT,
+  kR16_SNORM,
+  kR16_SSCALED,
+  kR16_UINT,
+  kR16_UNORM,
+  kR16_USCALED,
+  kR32G32B32A32_SFLOAT,
+  kR32G32B32A32_SINT,
+  kR32G32B32A32_UINT,
+  kR32G32B32_SFLOAT,
+  kR32G32B32_SINT,
+  kR32G32B32_UINT,
+  kR32G32_SFLOAT,
+  kR32G32_SINT,
+  kR32G32_UINT,
+  kR32_SFLOAT,
+  kR32_SINT,
+  kR32_UINT,
+  kR4G4B4A4_UNORM_PACK16,
+  kR4G4_UNORM_PACK8,
+  kR5G5B5A1_UNORM_PACK16,
+  kR5G6B5_UNORM_PACK16,
+  kR64G64B64A64_SFLOAT,
+  kR64G64B64A64_SINT,
+  kR64G64B64A64_UINT,
+  kR64G64B64_SFLOAT,
+  kR64G64B64_SINT,
+  kR64G64B64_UINT,
+  kR64G64_SFLOAT,
+  kR64G64_SINT,
+  kR64G64_UINT,
+  kR64_SFLOAT,
+  kR64_SINT,
+  kR64_UINT,
+  kR8G8B8A8_SINT,
+  kR8G8B8A8_SNORM,
+  kR8G8B8A8_SRGB,
+  kR8G8B8A8_SSCALED,
+  kR8G8B8A8_UINT,
+  kR8G8B8A8_UNORM,
+  kR8G8B8A8_USCALED,
+  kR8G8B8_SINT,
+  kR8G8B8_SNORM,
+  kR8G8B8_SRGB,
+  kR8G8B8_SSCALED,
+  kR8G8B8_UINT,
+  kR8G8B8_UNORM,
+  kR8G8B8_USCALED,
+  kR8G8_SINT,
+  kR8G8_SNORM,
+  kR8G8_SRGB,
+  kR8G8_SSCALED,
+  kR8G8_UINT,
+  kR8G8_UNORM,
+  kR8G8_USCALED,
+  kR8_SINT,
+  kR8_SNORM,
+  kR8_SRGB,
+  kR8_SSCALED,
+  kR8_UINT,
+  kR8_UNORM,
+  kR8_USCALED,
+  kS8_UINT,
+  kX8_D24_UNORM_PACK32,
+};
+
+#endif  // SRC_FORMAT_DATA_H_
diff --git a/src/make_unique.h b/src/make_unique.h
new file mode 100644
index 0000000..dff615d
--- /dev/null
+++ b/src/make_unique.h
@@ -0,0 +1,30 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_MAKE_UNIQUE_H_
+#define SRC_MAKE_UNIQUE_H_
+
+#include <memory>
+#include <utility>
+
+namespace amber {
+
+template <typename T, typename... Args>
+std::unique_ptr<T> MakeUnique(Args&&... args) {
+  return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
+}
+
+}  // namespace amber
+
+#endif  // SRC_MAKE_UNIQUE_H_
diff --git a/src/parser.cc b/src/parser.cc
new file mode 100644
index 0000000..33d3d05
--- /dev/null
+++ b/src/parser.cc
@@ -0,0 +1,23 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/parser.h"
+
+namespace amber {
+
+Parser::Parser() = default;
+
+Parser::~Parser() = default;
+
+}  // namespace amber
diff --git a/src/parser.h b/src/parser.h
new file mode 100644
index 0000000..d388f3b
--- /dev/null
+++ b/src/parser.h
@@ -0,0 +1,38 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_PARSER_H_
+#define SRC_PARSER_H_
+
+#include <string>
+
+#include "amber/result.h"
+#include "src/script.h"
+
+namespace amber {
+
+class Parser {
+ public:
+  virtual ~Parser();
+
+  virtual Result Parse(const std::string& data) = 0;
+  virtual const Script* GetScript() const = 0;
+
+ protected:
+  Parser();
+};
+
+}  // namespace amber
+
+#endif  // SRC_PARSER_H_
diff --git a/src/pipeline_data.cc b/src/pipeline_data.cc
new file mode 100644
index 0000000..89f0113
--- /dev/null
+++ b/src/pipeline_data.cc
@@ -0,0 +1,25 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/pipeline_data.h"
+
+namespace amber {
+
+PipelineData::PipelineData() = default;
+
+PipelineData::~PipelineData() = default;
+
+PipelineData::PipelineData(const PipelineData&) = default;
+
+}  // namespace amber
diff --git a/src/pipeline_data.h b/src/pipeline_data.h
new file mode 100644
index 0000000..6e5b9da
--- /dev/null
+++ b/src/pipeline_data.h
@@ -0,0 +1,217 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_PIPELINE_H_
+#define SRC_PIPELINE_H_
+
+#include <limits>
+
+#include "src/command_data.h"
+
+namespace amber {
+
+class PipelineData {
+ public:
+  PipelineData();
+  ~PipelineData();
+  PipelineData(const PipelineData&);
+
+  void SetTopology(Topology topo) { topology_ = topo; }
+  Topology GetTopology() const { return topology_; }
+
+  void SetPolygonMode(PolygonMode mode) { polygon_mode_ = mode; }
+  PolygonMode GetPolygonMode() const { return polygon_mode_; }
+
+  void SetCullMode(CullMode mode) { cull_mode_ = mode; }
+  CullMode GetCullMode() const { return cull_mode_; }
+
+  void SetFrontFace(FrontFace face) { front_face_ = face; }
+  FrontFace GetFrontFace() const { return front_face_; }
+
+  void SetDepthCompareOp(CompareOp op) { depth_compare_op_ = op; }
+  CompareOp GetDepthCompareOp() const { return depth_compare_op_; }
+
+  void SetColorWriteMask(uint8_t mask) { color_write_mask_ = mask; }
+  uint8_t GetColorWriteMask() const { return color_write_mask_; }
+
+  void SetFrontFailOp(StencilOp op) { front_fail_op_ = op; }
+  StencilOp GetFrontFailOp() const { return front_fail_op_; }
+
+  void SetFrontPassOp(StencilOp op) { front_pass_op_ = op; }
+  StencilOp GetFrontPassOp() const { return front_pass_op_; }
+
+  void SetFrontDepthFailOp(StencilOp op) { front_depth_fail_op_ = op; }
+  StencilOp GetFrontDepthFailOp() const { return front_depth_fail_op_; }
+
+  void SetFrontCompareOp(CompareOp op) { front_compare_op_ = op; }
+  CompareOp GetFrontCompareOp() const { return front_compare_op_; }
+
+  void SetFrontCompareMask(uint32_t mask) { front_compare_mask_ = mask; }
+  uint32_t GetFrontCompareMask() const { return front_compare_mask_; }
+
+  void SetFrontWriteMask(uint32_t mask) { front_write_mask_ = mask; }
+  uint32_t GetFrontWriteMask() const { return front_write_mask_; }
+
+  void SetFrontReference(uint32_t ref) { front_reference_ = ref; }
+  uint32_t GetFrontReference() const { return front_reference_; }
+
+  void SetBackFailOp(StencilOp op) { back_fail_op_ = op; }
+  StencilOp GetBackFailOp() const { return back_fail_op_; }
+
+  void SetBackPassOp(StencilOp op) { back_pass_op_ = op; }
+  StencilOp GetBackPassOp() const { return back_pass_op_; }
+
+  void SetBackDepthFailOp(StencilOp op) { back_depth_fail_op_ = op; }
+  StencilOp GetBackDepthFailOp() const { return back_depth_fail_op_; }
+
+  void SetBackCompareOp(CompareOp op) { back_compare_op_ = op; }
+  CompareOp GetBackCompareOp() const { return back_compare_op_; }
+
+  void SetBackCompareMask(uint32_t mask) { back_compare_mask_ = mask; }
+  uint32_t GetBackCompareMask() const { return back_compare_mask_; }
+
+  void SetBackWriteMask(uint32_t mask) { back_write_mask_ = mask; }
+  uint32_t GetbackWriteMask() const { return back_write_mask_; }
+
+  void SetBackReference(uint32_t ref) { back_reference_ = ref; }
+  uint32_t GetBackReference() const { return back_reference_; }
+
+  void SetLineWidth(float width) { line_width_ = width; }
+  float GetLineWidth() const { return line_width_; }
+
+  void SetEnableBlend(bool v) { enable_blend_ = v; }
+  bool GetEnableBlend() const { return enable_blend_; }
+
+  void SetEnableDepthTest(bool v) { enable_depth_test_ = v; }
+  bool GetEnableDepthTest() const { return enable_depth_test_; }
+
+  void SetEnableDepthWrite(bool v) { enable_depth_write_ = v; }
+  bool GetEnableDepthWrite() const { return enable_depth_write_; }
+
+  void SetEnableStencilTest(bool v) { enable_stencil_test_ = v; }
+  bool GetEnableStencilTest() const { return enable_stencil_test_; }
+
+  void SetEnablePrimitiveRestart(bool v) { enable_primitive_restart_ = v; }
+  bool GetEnablePrimitiveRestart() const { return enable_primitive_restart_; }
+
+  void SetEnableDepthClamp(bool v) { enable_depth_clamp_ = v; }
+  bool GetEnableDepthClamp() const { return enable_depth_clamp_; }
+
+  void SetEnableRasterizerDiscard(bool v) { enable_rasterizer_discard_ = v; }
+  bool GetEnableRasterizerDiscard() const { return enable_rasterizer_discard_; }
+
+  void SetEnableDepthBias(bool v) { enable_depth_bias_ = v; }
+  bool GetEnableDepthBias() const { return enable_depth_bias_; }
+
+  void SetEnableLogicOp(bool v) { enable_logic_op_ = v; }
+  bool GetEnableLogicOp() const { return enable_logic_op_; }
+
+  void SetEnableDepthBoundsTest(bool v) { enable_depth_bounds_test_ = v; }
+  bool GetEnableDepthBoundsTest() const { return enable_depth_bounds_test_; }
+
+  void SetDepthBiasConstantFactor(float f) { depth_bias_constant_factor_ = f; }
+  float GetDepthBiasConstantFactor() const {
+    return depth_bias_constant_factor_;
+  }
+
+  void SetDepthBiasClamp(float f) { depth_bias_clamp_ = f; }
+  float GetDepthBiasClamp() const { return depth_bias_clamp_; }
+
+  void SetDepthBiasSlopeFactor(float f) { depth_bias_slope_factor_ = f; }
+  float GetDepthBiasSlopeFactor() const { return depth_bias_slope_factor_; }
+
+  void SetMinDepthBounds(float f) { min_depth_bounds_ = f; }
+  float GetMinDepthBounds() const { return min_depth_bounds_; }
+
+  void SetMaxDepthBounds(float f) { max_depth_bounds_ = f; }
+  float GetMaxDepthBounds() const { return max_depth_bounds_; }
+
+  void SetLogicOp(LogicOp op) { logic_op_ = op; }
+  LogicOp GetLogicOp() const { return logic_op_; }
+
+  void SetSrcColorBlendFactor(BlendFactor f) { src_color_blend_factor_ = f; }
+  BlendFactor GetSrcColorBlendFactor() const { return src_color_blend_factor_; }
+
+  void SetDstColorBlendFactor(BlendFactor f) { dst_color_blend_factor_ = f; }
+  BlendFactor GetDstColorBlendFactor() const { return dst_color_blend_factor_; }
+
+  void SetSrcAlphaBlendFactor(BlendFactor f) { src_alpha_blend_factor_ = f; }
+  BlendFactor GetSrcAlphaBlendFactor() const { return src_alpha_blend_factor_; }
+
+  void SetDstAlphaBlendFactor(BlendFactor f) { dst_alpha_blend_factor_ = f; }
+  BlendFactor GetDstAlphaBlendFactor() const { return dst_alpha_blend_factor_; }
+
+  void SetColorBlendOp(BlendOp op) { color_blend_op_ = op; }
+  BlendOp GetColorBlendOp() const { return color_blend_op_; }
+
+  void SetAlphaBlendOp(BlendOp op) { alpha_blend_op_ = op; }
+  BlendOp GetAlphaBlendOp() const { return alpha_blend_op_; }
+
+ private:
+  StencilOp front_fail_op_ = StencilOp::kKeep;
+  StencilOp front_pass_op_ = StencilOp::kKeep;
+  StencilOp front_depth_fail_op_ = StencilOp::kKeep;
+  CompareOp front_compare_op_ = CompareOp::kAlways;
+
+  StencilOp back_fail_op_ = StencilOp::kKeep;
+  StencilOp back_pass_op_ = StencilOp::kKeep;
+  StencilOp back_depth_fail_op_ = StencilOp::kKeep;
+  CompareOp back_compare_op_ = CompareOp::kAlways;
+
+  Topology topology_ = Topology::kTriangleStrip;
+  PolygonMode polygon_mode_ = PolygonMode::kFill;
+  CullMode cull_mode_ = CullMode::kNone;
+  FrontFace front_face_ = FrontFace::kCounterClockwise;
+  CompareOp depth_compare_op_ = CompareOp::kLess;
+  LogicOp logic_op_ = LogicOp::kClear;
+  BlendFactor src_color_blend_factor_ = BlendFactor::kZero;
+  BlendFactor dst_color_blend_factor_ = BlendFactor::kZero;
+  BlendFactor src_alpha_blend_factor_ = BlendFactor::kZero;
+  BlendFactor dst_alpha_blend_factor_ = BlendFactor::kZero;
+  BlendOp color_blend_op_ = BlendOp::kAdd;
+  BlendOp alpha_blend_op_ = BlendOp::kAdd;
+
+  uint32_t front_compare_mask_ = std::numeric_limits<uint32_t>::max();
+  uint32_t front_write_mask_ = std::numeric_limits<uint32_t>::max();
+  uint32_t front_reference_ = 0;
+
+  uint32_t back_compare_mask_ = std::numeric_limits<uint32_t>::max();
+  uint32_t back_write_mask_ = std::numeric_limits<uint32_t>::max();
+  uint32_t back_reference_ = 0;
+
+  uint8_t color_write_mask_ =
+      kColorMaskR | kColorMaskG | kColorMaskB | kColorMaskA;
+
+  bool enable_blend_ = false;
+  bool enable_depth_test_ = false;
+  bool enable_depth_write_ = false;
+  bool enable_depth_clamp_ = false;
+  bool enable_depth_bias_ = false;
+  bool enable_depth_bounds_test_ = false;
+  bool enable_stencil_test_ = false;
+  bool enable_primitive_restart_ = false;
+  bool enable_rasterizer_discard_ = false;
+  bool enable_logic_op_ = false;
+
+  float line_width_ = 1.0f;
+  float depth_bias_constant_factor_ = 0.0f;
+  float depth_bias_clamp_ = 0.0f;
+  float depth_bias_slope_factor_ = 0.0f;
+  float min_depth_bounds_ = 0.0f;
+  float max_depth_bounds_ = 0.0f;
+};
+
+}  // namespace amber
+
+#endif  // SRC_PIPELINE_H_
diff --git a/src/result.cc b/src/result.cc
new file mode 100644
index 0000000..39b3a85
--- /dev/null
+++ b/src/result.cc
@@ -0,0 +1,29 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "amber/result.h"
+
+namespace amber {
+
+Result::Result() : succeeded_(true) {}
+
+Result::Result(const std::string& err) : succeeded_(false), error_(err) {}
+
+Result::Result(const Result&) = default;
+
+Result::~Result() = default;
+
+Result& Result::operator=(const Result&) = default;
+
+}  // namespace amber
diff --git a/src/result_test.cc b/src/result_test.cc
new file mode 100644
index 0000000..cb14cb3
--- /dev/null
+++ b/src/result_test.cc
@@ -0,0 +1,41 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "amber/result.h"
+#include "gtest/gtest.h"
+
+namespace amber {
+
+using ResultTest = testing::Test;
+
+TEST_F(ResultTest, SuccessByDefault) {
+  Result r;
+  EXPECT_TRUE(r.IsSuccess());
+}
+
+TEST_F(ResultTest, ErrorWithString) {
+  Result r("Test Failed");
+  EXPECT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Test Failed", r.Error());
+}
+
+TEST_F(ResultTest, Copy) {
+  Result r("Testing");
+  Result r2(r);
+
+  EXPECT_FALSE(r2.IsSuccess());
+  EXPECT_EQ("Testing", r2.Error());
+}
+
+}  // namespace amber
diff --git a/src/script.cc b/src/script.cc
new file mode 100644
index 0000000..fa5cbe6
--- /dev/null
+++ b/src/script.cc
@@ -0,0 +1,23 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/script.h"
+
+namespace amber {
+
+Script::Script(ScriptType type) : script_type_(type) {}
+
+Script::~Script() = default;
+
+}  // namespace amber
diff --git a/src/script.h b/src/script.h
new file mode 100644
index 0000000..4680014
--- /dev/null
+++ b/src/script.h
@@ -0,0 +1,42 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_SCRIPT_H_
+#define SRC_SCRIPT_H_
+
+#include <cstdint>
+
+namespace amber {
+
+enum class ScriptType : uint8_t { kVkScript = 0, kAmberScript };
+
+class Script {
+ public:
+  virtual ~Script();
+
+  bool IsVkScript() const { return script_type_ == ScriptType::kVkScript; }
+  bool IsAmberScript() const {
+    return script_type_ == ScriptType::kAmberScript;
+  }
+
+ protected:
+  Script(ScriptType);
+
+ private:
+  ScriptType script_type_;
+};
+
+}  // namespace amber
+
+#endif  // SRC_SCRIPT_H_
diff --git a/src/shader_compiler.cc b/src/shader_compiler.cc
new file mode 100644
index 0000000..a5294c7
--- /dev/null
+++ b/src/shader_compiler.cc
@@ -0,0 +1,150 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/shader_compiler.h"
+
+#include <algorithm>
+#include <cstdlib>
+#include <iterator>
+
+#include "spirv-tools/libspirv.hpp"
+#include "spirv-tools/linker.hpp"
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wold-style-cast"
+#pragma clang diagnostic ignored "-Wshadow-uncaptured-local"
+#pragma clang diagnostic ignored "-Wweak-vtables"
+#include "third_party/shaderc/libshaderc/include/shaderc/shaderc.hpp"
+#pragma clang diagnostic pop
+
+namespace amber {
+
+ShaderCompiler::ShaderCompiler() = default;
+
+ShaderCompiler::~ShaderCompiler() = default;
+
+std::pair<Result, std::vector<uint32_t>> ShaderCompiler::Compile(
+    ShaderType type,
+    ShaderFormat fmt,
+    const std::string& data) const {
+  std::string spv_errors;
+  // TODO(dsinclair): Vulkan env should be an option.
+  spvtools::SpirvTools tools(SPV_ENV_UNIVERSAL_1_0);
+  tools.SetMessageConsumer([&spv_errors](spv_message_level_t level, const char*,
+                                         const spv_position_t& position,
+                                         const char* message) {
+    switch (level) {
+      case SPV_MSG_FATAL:
+      case SPV_MSG_INTERNAL_ERROR:
+      case SPV_MSG_ERROR:
+        spv_errors += "error: line " + std::to_string(position.index) + ": " +
+                      message + "\n";
+        break;
+      case SPV_MSG_WARNING:
+        spv_errors += "warning: line " + std::to_string(position.index) + ": " +
+                      message + "\n";
+        break;
+      case SPV_MSG_INFO:
+        spv_errors += "info: line " + std::to_string(position.index) + ": " +
+                      message + "\n";
+        break;
+      case SPV_MSG_DEBUG:
+        break;
+    }
+  });
+
+  std::vector<uint32_t> results;
+  if (fmt == ShaderFormat::kGlsl) {
+    Result r = CompileGlsl(type, data, &results);
+    if (!r.IsSuccess())
+      return {r, {}};
+  } else if (fmt == ShaderFormat::kSpirvAsm) {
+    if (!tools.Assemble(data, &results,
+                        spvtools::SpirvTools::kDefaultAssembleOption)) {
+      return {Result("Shader assembly failed: " + spv_errors), {}};
+    }
+  } else if (fmt == ShaderFormat::kSpirvHex) {
+    Result r = ParseHex(data, &results);
+    if (!r.IsSuccess())
+      return {Result("Unable to parse shader hex."), {}};
+  } else {
+    return {Result("Invalid shader format"), results};
+  }
+
+  spvtools::ValidatorOptions options;
+  if (!tools.Validate(results.data(), results.size(), options))
+    return {Result("Invalid shader: " + spv_errors), {}};
+
+  return {{}, results};
+}
+
+Result ShaderCompiler::ParseHex(const std::string& data,
+                                std::vector<uint32_t>* result) const {
+  size_t used = 0;
+  const char* str = data.c_str();
+  uint8_t converted = 0;
+  uint32_t tmp = 0;
+  while (used < data.length()) {
+    char* new_pos = nullptr;
+    long v = std::strtol(str, &new_pos, 16);
+
+    ++converted;
+
+    // TODO(dsinclair): Is this actually right?
+    tmp = tmp | (static_cast<uint32_t>(v) << (8 * (converted - 1)));
+    if (converted == 4) {
+      result->push_back(tmp);
+      tmp = 0;
+      converted = 0;
+    }
+
+    used += static_cast<size_t>(new_pos - str);
+    str = new_pos;
+  }
+  return {};
+}
+
+Result ShaderCompiler::CompileGlsl(ShaderType shader_type,
+                                   const std::string& data,
+                                   std::vector<uint32_t>* result) const {
+  shaderc::Compiler compiler;
+  shaderc::CompileOptions options;
+
+  shaderc_shader_kind kind;
+  if (shader_type == ShaderType::kCompute)
+    kind = shaderc_compute_shader;
+  else if (shader_type == ShaderType::kFragment)
+    kind = shaderc_fragment_shader;
+  else if (shader_type == ShaderType::kGeometry)
+    kind = shaderc_geometry_shader;
+  else if (shader_type == ShaderType::kVertex)
+    kind = shaderc_vertex_shader;
+  else if (shader_type == ShaderType::kTessellationControl)
+    kind = shaderc_tess_control_shader;
+  else if (shader_type == ShaderType::kTessellationEvaluation)
+    kind = shaderc_tess_evaluation_shader;
+  else
+    return Result("Unknown shader type");
+
+  shaderc::SpvCompilationResult module =
+      compiler.CompileGlslToSpv(data, kind, "-", options);
+
+  if (module.GetCompilationStatus() != shaderc_compilation_status_success)
+    return Result(module.GetErrorMessage());
+
+  std::copy(module.cbegin(), module.cend(), std::back_inserter(*result));
+  return {};
+}
+
+}  // namespace amber
diff --git a/src/shader_compiler.h b/src/shader_compiler.h
new file mode 100644
index 0000000..4fd32a6
--- /dev/null
+++ b/src/shader_compiler.h
@@ -0,0 +1,43 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_SHADER_COMPILER_H_
+#define SRC_SHADER_COMPILER_H_
+
+#include <tuple>
+#include <vector>
+
+#include "amber/result.h"
+#include "src/shader_data.h"
+
+namespace amber {
+
+class ShaderCompiler {
+ public:
+  ShaderCompiler();
+  ~ShaderCompiler();
+
+  std::pair<Result, std::vector<uint32_t>>
+  Compile(ShaderType type, ShaderFormat fmt, const std::string& data) const;
+
+ private:
+  Result ParseHex(const std::string& data, std::vector<uint32_t>* result) const;
+  Result CompileGlsl(ShaderType shader_type,
+                     const std::string& data,
+                     std::vector<uint32_t>* result) const;
+};
+
+}  // namespace amber
+
+#endif  // SRC_SHADER_COMPILER_H_
diff --git a/src/shader_compiler_test.cc b/src/shader_compiler_test.cc
new file mode 100644
index 0000000..5465331
--- /dev/null
+++ b/src/shader_compiler_test.cc
@@ -0,0 +1,160 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/shader_compiler.h"
+#include "gtest/gtest.h"
+#include "src/vkscript/section_parser.h"  // For the passthrough vertex shader
+
+namespace amber {
+namespace {
+
+const char kHexShader[] =
+    R"(0x03 0x02 0x23 0x07 0x00 0x00 0x01 0x00 0x07 0x00 0x08 0x00
+   0x15 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x11 0x00 0x02 0x00
+   0x01 0x00 0x00 0x00 0x0b 0x00 0x06 0x00 0x01 0x00 0x00 0x00
+   0x47 0x4c 0x53 0x4c 0x2e 0x73 0x74 0x64 0x2e 0x34 0x35 0x30
+   0x00 0x00 0x00 0x00 0x0e 0x00 0x03 0x00 0x00 0x00 0x00 0x00
+   0x01 0x00 0x00 0x00 0x0f 0x00 0x07 0x00 0x00 0x00 0x00 0x00
+   0x04 0x00 0x00 0x00 0x6d 0x61 0x69 0x6e 0x00 0x00 0x00 0x00
+   0x0d 0x00 0x00 0x00 0x11 0x00 0x00 0x00 0x03 0x00 0x03 0x00
+   0x02 0x00 0x00 0x00 0xae 0x01 0x00 0x00 0x05 0x00 0x04 0x00
+   0x04 0x00 0x00 0x00 0x6d 0x61 0x69 0x6e 0x00 0x00 0x00 0x00
+   0x05 0x00 0x06 0x00 0x0b 0x00 0x00 0x00 0x67 0x6c 0x5f 0x50
+   0x65 0x72 0x56 0x65 0x72 0x74 0x65 0x78 0x00 0x00 0x00 0x00
+   0x06 0x00 0x06 0x00 0x0b 0x00 0x00 0x00 0x00 0x00 0x00 0x00
+   0x67 0x6c 0x5f 0x50 0x6f 0x73 0x69 0x74 0x69 0x6f 0x6e 0x00
+   0x06 0x00 0x07 0x00 0x0b 0x00 0x00 0x00 0x01 0x00 0x00 0x00
+   0x67 0x6c 0x5f 0x50 0x6f 0x69 0x6e 0x74 0x53 0x69 0x7a 0x65
+   0x00 0x00 0x00 0x00 0x06 0x00 0x07 0x00 0x0b 0x00 0x00 0x00
+   0x02 0x00 0x00 0x00 0x67 0x6c 0x5f 0x43 0x6c 0x69 0x70 0x44
+   0x69 0x73 0x74 0x61 0x6e 0x63 0x65 0x00 0x05 0x00 0x03 0x00
+   0x0d 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x05 0x00 0x05 0x00
+   0x11 0x00 0x00 0x00 0x70 0x6f 0x73 0x69 0x74 0x69 0x6f 0x6e
+   0x00 0x00 0x00 0x00 0x48 0x00 0x05 0x00 0x0b 0x00 0x00 0x00
+   0x00 0x00 0x00 0x00 0x0b 0x00 0x00 0x00 0x00 0x00 0x00 0x00
+   0x48 0x00 0x05 0x00 0x0b 0x00 0x00 0x00 0x01 0x00 0x00 0x00
+   0x0b 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x48 0x00 0x05 0x00
+   0x0b 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x0b 0x00 0x00 0x00
+   0x03 0x00 0x00 0x00 0x47 0x00 0x03 0x00 0x0b 0x00 0x00 0x00
+   0x02 0x00 0x00 0x00 0x47 0x00 0x04 0x00 0x11 0x00 0x00 0x00
+   0x1e 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x13 0x00 0x02 0x00
+   0x02 0x00 0x00 0x00 0x21 0x00 0x03 0x00 0x03 0x00 0x00 0x00
+   0x02 0x00 0x00 0x00 0x16 0x00 0x03 0x00 0x06 0x00 0x00 0x00
+   0x20 0x00 0x00 0x00 0x17 0x00 0x04 0x00 0x07 0x00 0x00 0x00
+   0x06 0x00 0x00 0x00 0x04 0x00 0x00 0x00 0x15 0x00 0x04 0x00
+   0x08 0x00 0x00 0x00 0x20 0x00 0x00 0x00 0x00 0x00 0x00 0x00
+   0x2b 0x00 0x04 0x00 0x08 0x00 0x00 0x00 0x09 0x00 0x00 0x00
+   0x01 0x00 0x00 0x00 0x1c 0x00 0x04 0x00 0x0a 0x00 0x00 0x00
+   0x06 0x00 0x00 0x00 0x09 0x00 0x00 0x00 0x1e 0x00 0x05 0x00
+   0x0b 0x00 0x00 0x00 0x07 0x00 0x00 0x00 0x06 0x00 0x00 0x00
+   0x0a 0x00 0x00 0x00 0x20 0x00 0x04 0x00 0x0c 0x00 0x00 0x00
+   0x03 0x00 0x00 0x00 0x0b 0x00 0x00 0x00 0x3b 0x00 0x04 0x00
+   0x0c 0x00 0x00 0x00 0x0d 0x00 0x00 0x00 0x03 0x00 0x00 0x00
+   0x15 0x00 0x04 0x00 0x0e 0x00 0x00 0x00 0x20 0x00 0x00 0x00
+   0x01 0x00 0x00 0x00 0x2b 0x00 0x04 0x00 0x0e 0x00 0x00 0x00
+   0x0f 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x20 0x00 0x04 0x00
+   0x10 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x07 0x00 0x00 0x00
+   0x3b 0x00 0x04 0x00 0x10 0x00 0x00 0x00 0x11 0x00 0x00 0x00
+   0x01 0x00 0x00 0x00 0x20 0x00 0x04 0x00 0x13 0x00 0x00 0x00
+   0x03 0x00 0x00 0x00 0x07 0x00 0x00 0x00 0x36 0x00 0x05 0x00
+   0x02 0x00 0x00 0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00
+   0x03 0x00 0x00 0x00 0xf8 0x00 0x02 0x00 0x05 0x00 0x00 0x00
+   0x3d 0x00 0x04 0x00 0x07 0x00 0x00 0x00 0x12 0x00 0x00 0x00
+   0x11 0x00 0x00 0x00 0x41 0x00 0x05 0x00 0x13 0x00 0x00 0x00
+   0x14 0x00 0x00 0x00 0x0d 0x00 0x00 0x00 0x0f 0x00 0x00 0x00
+   0x3e 0x00 0x03 0x00 0x14 0x00 0x00 0x00 0x12 0x00 0x00 0x00
+   0xfd 0x00 0x01 0x00 0x38 0x00 0x01 0x00)";
+
+}  // namespace
+
+using ShaderCompilerTest = testing::Test;
+
+TEST_F(ShaderCompilerTest, CompilesGlsl) {
+  std::string contents = R"(
+#version 420
+layout(location = 0) in vec4 position;
+
+void main() {
+  gl_Position = position;
+})";
+
+  ShaderCompiler sc;
+  Result r;
+  std::vector<uint32_t> shader;
+  std::tie(r, shader) =
+      sc.Compile(ShaderType::kVertex, ShaderFormat::kGlsl, contents);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_FALSE(shader.empty());
+  EXPECT_EQ(0x07230203, shader[0]);  // Verify SPIR-V header present.
+}
+
+TEST_F(ShaderCompilerTest, CompilesSpirvAsm) {
+  ShaderCompiler sc;
+  Result r;
+  std::vector<uint32_t> shader;
+  std::tie(r, shader) = sc.Compile(ShaderType::kVertex, ShaderFormat::kSpirvAsm,
+                                   kPassThroughShader);
+  ASSERT_TRUE(r.IsSuccess());
+  EXPECT_FALSE(shader.empty());
+  EXPECT_EQ(0x07230203, shader[0]);  // Verify SPIR-V header present.
+}
+
+TEST_F(ShaderCompilerTest, CompilesSpirvHex) {
+  ShaderCompiler sc;
+  Result r;
+  std::vector<uint32_t> shader;
+  std::tie(r, shader) =
+      sc.Compile(ShaderType::kVertex, ShaderFormat::kSpirvHex, kHexShader);
+  ASSERT_TRUE(r.IsSuccess());
+  EXPECT_FALSE(shader.empty());
+  EXPECT_EQ(0x07230203, shader[0]);  // Verify SPIR-V header present.
+}
+
+TEST_F(ShaderCompilerTest, InvalidSpirvHex) {
+  std::string contents = kHexShader;
+  contents[3] = '0';
+
+  ShaderCompiler sc;
+  Result r;
+  std::vector<uint32_t> shader;
+  std::tie(r, shader) =
+      sc.Compile(ShaderType::kVertex, ShaderFormat::kSpirvHex, contents);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid shader: error: line 0: Invalid SPIR-V magic number.\n",
+            r.Error());
+}
+
+TEST_F(ShaderCompilerTest, InvalidHex) {
+  ShaderCompiler sc;
+  Result r;
+  std::vector<uint32_t> shader;
+  std::tie(r, shader) =
+      sc.Compile(ShaderType::kVertex, ShaderFormat::kSpirvHex, "aaaaaaaaa");
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid shader: error: line 0: Invalid SPIR-V magic number.\n",
+            r.Error());
+}
+
+TEST_F(ShaderCompilerTest, FailsOnInvalidShader) {
+  std::string contents = "Just Random\nText()\nThat doesn't work.";
+
+  ShaderCompiler sc;
+  Result r;
+  std::vector<uint32_t> shader;
+  std::tie(r, shader) =
+      sc.Compile(ShaderType::kVertex, ShaderFormat::kGlsl, contents);
+  ASSERT_FALSE(r.IsSuccess());
+}
+
+}  // namespace amber
diff --git a/src/shader_data.h b/src/shader_data.h
new file mode 100644
index 0000000..c1fb20e
--- /dev/null
+++ b/src/shader_data.h
@@ -0,0 +1,86 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_SHADER_DATA_H_
+#define SRC_SHADER_DATA_H_
+
+#include <cstdint>
+
+namespace amber {
+
+enum class ShaderFormat : uint8_t {
+  kDefault = 0,
+  kText,
+  kGlsl,
+  kSpirvAsm,
+  kSpirvHex,
+};
+
+enum class ShaderType : uint8_t {
+  kCompute = 0,
+  kGeometry,
+  kFragment,
+  kVertex,
+  kTessellationControl,
+  kTessellationEvaluation,
+};
+
+const char kPassThroughShader[] = R"(; SPIR-V
+; Version: 1.0
+; Generator: Khronos Glslang Reference Front End; 7
+; Bound: 21
+; Schema: 0
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Vertex %main "main" %_ %position
+               OpSource GLSL 430
+               OpName %main "main"
+               OpName %gl_PerVertex "gl_PerVertex"
+               OpMemberName %gl_PerVertex 0 "gl_Position"
+               OpMemberName %gl_PerVertex 1 "gl_PointSize"
+               OpMemberName %gl_PerVertex 2 "gl_ClipDistance"
+               OpName %_ ""
+               OpName %position "position"
+               OpMemberDecorate %gl_PerVertex 0 BuiltIn Position
+               OpMemberDecorate %gl_PerVertex 1 BuiltIn PointSize
+               OpMemberDecorate %gl_PerVertex 2 BuiltIn ClipDistance
+               OpDecorate %gl_PerVertex Block
+               OpDecorate %position Location 0
+       %void = OpTypeVoid
+          %3 = OpTypeFunction %void
+      %float = OpTypeFloat 32
+    %v4float = OpTypeVector %float 4
+       %uint = OpTypeInt 32 0
+     %uint_1 = OpConstant %uint 1
+%_arr_float_uint_1 = OpTypeArray %float %uint_1
+%gl_PerVertex = OpTypeStruct %v4float %float %_arr_float_uint_1
+%_ptr_Output_gl_PerVertex = OpTypePointer Output %gl_PerVertex
+          %_ = OpVariable %_ptr_Output_gl_PerVertex Output
+        %int = OpTypeInt 32 1
+      %int_0 = OpConstant %int 0
+%_ptr_Input_v4float = OpTypePointer Input %v4float
+   %position = OpVariable %_ptr_Input_v4float Input
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+       %main = OpFunction %void None %3
+          %5 = OpLabel
+         %18 = OpLoad %v4float %position
+         %20 = OpAccessChain %_ptr_Output_v4float %_ %int_0
+               OpStore %20 %18
+               OpReturn
+               OpFunctionEnd)";
+
+}  // namespace amber
+
+#endif  // SRC_SHADER_DATA_H_
diff --git a/src/tokenizer.cc b/src/tokenizer.cc
new file mode 100644
index 0000000..9f55ec8
--- /dev/null
+++ b/src/tokenizer.cc
@@ -0,0 +1,205 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tokenizer.h"
+
+#include <cstdlib>
+#include <limits>
+#include <sstream>
+
+#include "src/make_unique.h"
+
+namespace amber {
+
+Token::Token(TokenType type) : type_(type) {}
+
+Token::~Token() = default;
+
+Result Token::ConvertToDouble() {
+  if (IsDouble())
+    return {};
+
+  if (IsString() || IsEOL() || IsEOS())
+    return Result("Invalid conversion to double");
+
+  if (IsInteger()) {
+    if (is_negative_ || uint_value_ <= std::numeric_limits<int64_t>::max())
+      double_value_ = static_cast<double>(AsInt64());
+    else
+      return Result("uint64_t value too big to fit in double");
+
+    uint_value_ = 0;
+  } else if (IsHex()) {
+    double_value_ = static_cast<double>(AsHex());
+    string_value_ = "";
+  }
+  type_ = TokenType::kDouble;
+  return {};
+}
+
+Tokenizer::Tokenizer(const std::string& data) : data_(data) {}
+
+Tokenizer::~Tokenizer() = default;
+
+std::unique_ptr<Token> Tokenizer::NextToken() {
+  SkipWhitespace();
+  if (current_position_ >= data_.length())
+    return MakeUnique<Token>(TokenType::kEOS);
+
+  if (data_[current_position_] == '#') {
+    SkipComment();
+    SkipWhitespace();
+  }
+  if (current_position_ >= data_.length())
+    return MakeUnique<Token>(TokenType::kEOS);
+
+  if (data_[current_position_] == '\n') {
+    ++current_line_;
+    ++current_position_;
+    return MakeUnique<Token>(TokenType::kEOL);
+  }
+
+  // If the current position is a , ( or ) then handle it specially as we don't
+  // want to consume any other characters.
+  if (data_[current_position_] == ',' || data_[current_position_] == '(' ||
+      data_[current_position_] == ')') {
+    auto tok = MakeUnique<Token>(TokenType::kString);
+    std::string str(1, data_[current_position_]);
+    tok->SetStringValue(str);
+    ++current_position_;
+    return tok;
+  }
+
+  size_t end_pos = current_position_;
+  while (end_pos < data_.length()) {
+    if (data_[end_pos] == ' ' || data_[end_pos] == '\n' ||
+        data_[end_pos] == ')' || data_[end_pos] == ',' ||
+        data_[end_pos] == '(') {
+      break;
+    }
+    ++end_pos;
+  }
+
+  std::string tok_str =
+      data_.substr(current_position_, end_pos - current_position_);
+  current_position_ = end_pos;
+
+  // Starts with an alpha is a string.
+  if (!std::isdigit(tok_str[0]) &&
+      !(tok_str[0] == '-' && std::isdigit(tok_str[1])) &&
+      !(tok_str[0] == '.' && std::isdigit(tok_str[1]))) {
+    // If we've got a continuation, skip over the end of line and get the next
+    // token.
+    if (tok_str == "\\") {
+      if ((current_position_ < data_.length() &&
+           data_[current_position_] == '\n')) {
+        ++current_line_;
+        ++current_position_;
+        return NextToken();
+      } else if (current_position_ + 1 < data_.length() &&
+                 data_[current_position_] == '\r' &&
+                 data_[current_position_ + 1] == '\n') {
+        ++current_line_;
+        current_position_ += 2;
+        return NextToken();
+      }
+    }
+
+    auto tok = MakeUnique<Token>(TokenType::kString);
+    tok->SetStringValue(tok_str);
+    return tok;
+  }
+
+  // Handle hex strings
+  if (tok_str.size() > 2 && tok_str[0] == '0' && tok_str[1] == 'x') {
+    auto tok = MakeUnique<Token>(TokenType::kHex);
+    tok->SetStringValue(tok_str);
+    return tok;
+  }
+
+  bool is_double = false;
+  for (const char ch : tok_str) {
+    if (ch == '.') {
+      is_double = true;
+      break;
+    }
+  }
+
+  std::unique_ptr<Token> tok;
+
+  char* final_pos = nullptr;
+  if (is_double) {
+    tok = MakeUnique<Token>(TokenType::kDouble);
+
+    double val = strtod(tok_str.c_str(), &final_pos);
+    tok->SetDoubleValue(val);
+  } else {
+    tok = MakeUnique<Token>(TokenType::kInteger);
+
+    uint64_t val = strtoull(tok_str.c_str(), &final_pos, 10);
+    tok->SetUint64Value(static_cast<uint64_t>(val));
+  }
+  if (tok_str.size() > 1 && tok_str[0] == '-')
+    tok->SetNegative();
+
+  // If the number isn't the whole token then move back so we can then parse
+  // the string portion.
+  long diff = final_pos - tok_str.c_str();
+  if (diff > 0)
+    current_position_ -= (tok_str.length() - static_cast<size_t>(diff));
+
+  return tok;
+}
+
+std::string Tokenizer::ExtractToNext(const std::string& str) {
+  size_t pos = data_.find(str, current_position_);
+  std::string ret;
+  if (pos == std::string::npos) {
+    ret = data_.substr(current_position_);
+    current_position_ = data_.length();
+  } else {
+    ret = data_.substr(current_position_, pos - current_position_);
+    current_position_ = pos;
+  }
+
+  // Account for any new lines in the extracted text so our current line
+  // number stays correct.
+  for (const char c : ret) {
+    if (c == '\n')
+      ++current_line_;
+  }
+
+  return ret;
+}
+
+bool Tokenizer::IsWhitespace(char ch) {
+  return ch == '\0' || ch == '\t' || ch == '\r' || ch == 0x0c /* ff */ ||
+         ch == ' ';
+}
+
+void Tokenizer::SkipWhitespace() {
+  while (current_position_ < data_.size() &&
+         IsWhitespace(data_[current_position_])) {
+    ++current_position_;
+  }
+}
+
+void Tokenizer::SkipComment() {
+  while (current_position_ < data_.length() &&
+         data_[current_position_] != '\n') {
+    ++current_position_;
+  }
+}
+
+}  // namespace amber
diff --git a/src/tokenizer.h b/src/tokenizer.h
new file mode 100644
index 0000000..435f1db
--- /dev/null
+++ b/src/tokenizer.h
@@ -0,0 +1,111 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TOKENIZER_H_
+#define SRC_TOKENIZER_H_
+
+#include <memory>
+#include <string>
+
+#include "amber/result.h"
+
+namespace amber {
+
+enum class TokenType : uint8_t {
+  kEOS = 0,
+  kEOL,
+  kString,
+  kInteger,
+  kDouble,
+  kHex,
+};
+
+class Token {
+ public:
+  Token(TokenType type);
+  ~Token();
+
+  bool IsHex() const { return type_ == TokenType::kHex; }
+  bool IsInteger() const { return type_ == TokenType::kInteger; }
+  bool IsDouble() const { return type_ == TokenType::kDouble; }
+  bool IsString() const { return type_ == TokenType::kString; }
+  bool IsEOS() const { return type_ == TokenType::kEOS; }
+  bool IsEOL() const { return type_ == TokenType::kEOL; }
+
+  bool IsComma() const {
+    return type_ == TokenType::kString && string_value_ == ",";
+  }
+  bool IsOpenBracket() const {
+    return type_ == TokenType::kString && string_value_ == "(";
+  }
+  bool IsCloseBracket() const {
+    return type_ == TokenType::kString && string_value_ == ")";
+  }
+
+  void SetNegative() { is_negative_ = true; }
+  void SetStringValue(const std::string& val) { string_value_ = val; }
+  void SetUint64Value(uint64_t val) { uint_value_ = val; }
+  void SetDoubleValue(double val) { double_value_ = val; }
+
+  const std::string& AsString() const { return string_value_; }
+
+  uint8_t AsUint8() const { return static_cast<uint8_t>(uint_value_); }
+  uint16_t AsUint16() const { return static_cast<uint16_t>(uint_value_); }
+  uint32_t AsUint32() const { return static_cast<uint32_t>(uint_value_); }
+  uint64_t AsUint64() const { return static_cast<uint64_t>(uint_value_); }
+
+  int8_t AsInt8() const { return static_cast<int8_t>(uint_value_); }
+  int16_t AsInt16() const { return static_cast<int16_t>(uint_value_); }
+  int32_t AsInt32() const { return static_cast<int32_t>(uint_value_); }
+  int64_t AsInt64() const { return static_cast<int64_t>(uint_value_); }
+
+  Result ConvertToDouble();
+
+  float AsFloat() const { return static_cast<float>(double_value_); }
+  double AsDouble() const { return double_value_; }
+
+  uint64_t AsHex() const {
+    return strtoull(string_value_.c_str(), nullptr, 16);
+  }
+
+ private:
+  TokenType type_;
+  std::string string_value_;
+  uint64_t uint_value_ = 0;
+  double double_value_ = 0.0;
+  bool is_negative_ = false;
+};
+
+class Tokenizer {
+ public:
+  Tokenizer(const std::string& data);
+  ~Tokenizer();
+
+  std::unique_ptr<Token> NextToken();
+  std::string ExtractToNext(const std::string& str);
+  size_t GetCurrentLine() const { return current_line_; }
+
+ private:
+  bool IsWhitespace(char ch);
+  void SkipWhitespace();
+  void SkipComment();
+
+  std::string data_;
+  size_t current_position_ = 0;
+  size_t current_line_ = 1;
+};
+
+}  // namespace amber
+
+#endif  // SRC_TOKENIZER_H_
diff --git a/src/tokenizer_test.cc b/src/tokenizer_test.cc
new file mode 100644
index 0000000..fa74da7
--- /dev/null
+++ b/src/tokenizer_test.cc
@@ -0,0 +1,554 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tokenizer.h"
+#include "gtest/gtest.h"
+
+namespace amber {
+
+using TokenizerTest = testing::Test;
+
+TEST_F(TokenizerTest, ProcessEmpty) {
+  Tokenizer t("");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOS());
+}
+
+TEST_F(TokenizerTest, ProcessString) {
+  Tokenizer t("TestString");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsString());
+  EXPECT_EQ("TestString", next->AsString());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOS());
+}
+
+TEST_F(TokenizerTest, ProcessInt) {
+  Tokenizer t("123");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsInteger());
+  EXPECT_EQ(123U, next->AsUint32());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOS());
+}
+
+TEST_F(TokenizerTest, ProcessNegative) {
+  Tokenizer t("-123");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsInteger());
+  EXPECT_EQ(-123, next->AsInt32());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOS());
+}
+
+TEST_F(TokenizerTest, ProcessDouble) {
+  Tokenizer t("123.456");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsDouble());
+  EXPECT_EQ(123.456f, next->AsFloat());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOS());
+}
+
+TEST_F(TokenizerTest, ProcessNegativeDouble) {
+  Tokenizer t("-123.456");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsDouble());
+  EXPECT_EQ(-123.456f, next->AsFloat());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOS());
+}
+
+TEST_F(TokenizerTest, ProcessDoubleStartWithDot) {
+  Tokenizer t(".123456");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsDouble());
+  EXPECT_EQ(.123456f, next->AsFloat());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOS());
+}
+
+TEST_F(TokenizerTest, ProcessStringWithNumberInName) {
+  Tokenizer t("BufferAccess32");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsString());
+  EXPECT_EQ("BufferAccess32", next->AsString());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOS());
+}
+
+TEST_F(TokenizerTest, ProcessMultiStatement) {
+  Tokenizer t("TestValue 123.456");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsString());
+  EXPECT_EQ("TestValue", next->AsString());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsDouble());
+  EXPECT_EQ(123.456f, next->AsFloat());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOS());
+}
+
+TEST_F(TokenizerTest, ProcessMultiLineStatement) {
+  Tokenizer t("TestValue 123.456\nAnotherValue\n\nThirdValue 456");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsString());
+  EXPECT_EQ("TestValue", next->AsString());
+  EXPECT_EQ(1U, t.GetCurrentLine());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsDouble());
+  EXPECT_EQ(123.456f, next->AsFloat());
+  EXPECT_EQ(1U, t.GetCurrentLine());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOL());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsString());
+  EXPECT_EQ("AnotherValue", next->AsString());
+  EXPECT_EQ(2U, t.GetCurrentLine());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOL());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOL());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsString());
+  EXPECT_EQ("ThirdValue", next->AsString());
+  EXPECT_EQ(4U, t.GetCurrentLine());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsInteger());
+  EXPECT_EQ(456U, next->AsUint16());
+  EXPECT_EQ(4U, t.GetCurrentLine());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOS());
+}
+
+TEST_F(TokenizerTest, ProcessComments) {
+  Tokenizer t(R"(# Initial comment string
+TestValue 123.456
+    AnotherValue   # Space before, comment after
+
+ThirdValue 456)");
+  auto next = t.NextToken();
+  // The comment injects a blank line into the output
+  // so we can handle full line comment and end of line comment the same.
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOL());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsString());
+  EXPECT_EQ("TestValue", next->AsString());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsDouble());
+  EXPECT_EQ(123.456f, next->AsFloat());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOL());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsString());
+  EXPECT_EQ("AnotherValue", next->AsString());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOL());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOL());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsString());
+  EXPECT_EQ("ThirdValue", next->AsString());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsInteger());
+  EXPECT_EQ(456U, next->AsUint16());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOS());
+}
+
+TEST_F(TokenizerTest, HexValue) {
+  Tokenizer t("0xff00f0ff");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsHex());
+  EXPECT_EQ(0xff00f0ff, next->AsHex());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOS());
+}
+
+TEST_F(TokenizerTest, HexValueAfterWhiteSpace) {
+  Tokenizer t("     \t  \t   0xff00f0ff");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsHex());
+  EXPECT_EQ(0xff00f0ff, next->AsHex());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOS());
+}
+
+TEST_F(TokenizerTest, StringStartingWithNum) {
+  Tokenizer t("1/ABC");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsInteger());
+  EXPECT_EQ(1U, next->AsUint32());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsString());
+  EXPECT_EQ("/ABC", next->AsString());
+}
+
+TEST_F(TokenizerTest, BracketsAndCommas) {
+  Tokenizer t("(1.0, 2, abc)");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsOpenBracket());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsDouble());
+  EXPECT_FLOAT_EQ(1.0, next->AsFloat());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsComma());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsInteger());
+  EXPECT_EQ(2U, next->AsUint32());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsComma());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsString());
+  EXPECT_EQ("abc", next->AsString());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsCloseBracket());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOS());
+}
+
+TEST_F(TokenizerTest, TokenToDoubleFromDouble) {
+  Tokenizer t("-1.234");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsDouble());
+
+  Result r = next->ConvertToDouble();
+  ASSERT_TRUE(r.IsSuccess());
+  EXPECT_FLOAT_EQ(-1.234, next->AsFloat());
+}
+
+TEST_F(TokenizerTest, TokenToDoubleFromInt) {
+  Tokenizer t("-1");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsInteger());
+
+  Result r = next->ConvertToDouble();
+  ASSERT_TRUE(r.IsSuccess());
+  EXPECT_FLOAT_EQ(-1.0, next->AsFloat());
+}
+
+TEST_F(TokenizerTest, DashToken) {
+  Tokenizer t("-");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsString());
+  EXPECT_EQ("-", next->AsString());
+}
+
+TEST_F(TokenizerTest, ParseUint64Max) {
+  Tokenizer t(std::to_string(std::numeric_limits<uint64_t>::max()));
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsInteger());
+  EXPECT_EQ(std::numeric_limits<uint64_t>::max(), next->AsUint64());
+}
+
+TEST_F(TokenizerTest, ParseInt64Min) {
+  Tokenizer t(std::to_string(std::numeric_limits<int64_t>::min()));
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsInteger());
+  EXPECT_EQ(std::numeric_limits<int64_t>::min(), next->AsInt64());
+}
+
+TEST_F(TokenizerTest, TokenToDoubleFromUint64Max) {
+  Tokenizer t(std::to_string(std::numeric_limits<uint64_t>::max()));
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsInteger());
+
+  Result r = next->ConvertToDouble();
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("uint64_t value too big to fit in double", r.Error());
+}
+
+TEST_F(TokenizerTest, TokenToDoubleFromInt64Min) {
+  Tokenizer t(std::to_string(std::numeric_limits<int64_t>::min()));
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsInteger());
+
+  Result r = next->ConvertToDouble();
+  ASSERT_TRUE(r.IsSuccess());
+  EXPECT_FLOAT_EQ(std::numeric_limits<int64_t>::min(), next->AsDouble());
+}
+
+TEST_F(TokenizerTest, TokenToDoubleFromInt64Max) {
+  Tokenizer t(std::to_string(std::numeric_limits<int64_t>::max()));
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsInteger());
+
+  Result r = next->ConvertToDouble();
+  ASSERT_TRUE(r.IsSuccess());
+  EXPECT_FLOAT_EQ(std::numeric_limits<int64_t>::max(), next->AsDouble());
+}
+
+TEST_F(TokenizerTest, TokenToDoubleFromString) {
+  Tokenizer t("INVALID");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsString());
+
+  Result r = next->ConvertToDouble();
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid conversion to double", r.Error());
+}
+
+TEST_F(TokenizerTest, TokenToDoubleFromHex) {
+  Tokenizer t("0xff00f0ff");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsHex());
+
+  Result r = next->ConvertToDouble();
+  ASSERT_TRUE(r.IsSuccess());
+  EXPECT_FLOAT_EQ(0xff00f0ff, next->AsFloat());
+}
+
+TEST_F(TokenizerTest, TokenToDoubleFromEOS) {
+  Tokenizer t("");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsEOS());
+
+  Result r = next->ConvertToDouble();
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid conversion to double", r.Error());
+}
+
+TEST_F(TokenizerTest, TokenToDoubleFromEOL) {
+  Tokenizer t("-1\n-2");
+  auto next = t.NextToken();
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsEOL());
+
+  Result r = next->ConvertToDouble();
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid conversion to double", r.Error());
+}
+
+TEST_F(TokenizerTest, Continuations) {
+  Tokenizer t("1 \\\n2");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsInteger());
+  EXPECT_EQ(1, next->AsInt32());
+  EXPECT_EQ(1, t.GetCurrentLine());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsInteger());
+  EXPECT_EQ(2, next->AsInt32());
+  EXPECT_EQ(2, t.GetCurrentLine());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOS());
+}
+
+TEST_F(TokenizerTest, ContinuationAtEndOfString) {
+  Tokenizer t("1 \\");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsInteger());
+  EXPECT_EQ(1, next->AsInt32());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsString());
+  EXPECT_EQ("\\", next->AsString());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOS());
+}
+
+TEST_F(TokenizerTest, ContinuationTokenAtOfLine) {
+  Tokenizer t("1 \\2");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsInteger());
+  EXPECT_EQ(1, next->AsInt32());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsString());
+  EXPECT_EQ("\\2", next->AsString());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOS());
+}
+
+TEST_F(TokenizerTest, ContinuationTokenInMiddleOfLine) {
+  Tokenizer t("1 \\ 2");
+  auto next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsInteger());
+  EXPECT_EQ(1, next->AsInt32());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsString());
+  EXPECT_EQ("\\", next->AsString());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  ASSERT_TRUE(next->IsInteger());
+  EXPECT_EQ(2U, next->AsInt32());
+
+  next = t.NextToken();
+  ASSERT_TRUE(next != nullptr);
+  EXPECT_TRUE(next->IsEOS());
+}
+
+TEST_F(TokenizerTest, ExtractToNext) {
+  Tokenizer t("this\nis\na\ntest\nEND");
+
+  auto next = t.NextToken();
+  EXPECT_TRUE(next->IsString());
+  EXPECT_EQ("this", next->AsString());
+
+  std::string s = t.ExtractToNext("END");
+  ASSERT_EQ("\nis\na\ntest\n", s);
+
+  next = t.NextToken();
+  EXPECT_TRUE(next->IsString());
+  EXPECT_EQ("END", next->AsString());
+  EXPECT_EQ(5U, t.GetCurrentLine());
+
+  next = t.NextToken();
+  EXPECT_TRUE(next->IsEOS());
+}
+
+TEST_F(TokenizerTest, ExtractToNextMissingNext) {
+  Tokenizer t("this\nis\na\ntest\n");
+
+  auto next = t.NextToken();
+  EXPECT_TRUE(next->IsString());
+  EXPECT_EQ("this", next->AsString());
+
+  std::string s = t.ExtractToNext("END");
+  ASSERT_EQ("\nis\na\ntest\n", s);
+
+  next = t.NextToken();
+  EXPECT_TRUE(next->IsEOS());
+  EXPECT_EQ(5U, t.GetCurrentLine());
+}
+
+TEST_F(TokenizerTest, ExtractToNextCurrentIsNext) {
+  Tokenizer t("END");
+  std::string s = t.ExtractToNext("END");
+  ASSERT_EQ("", s);
+
+  auto next = t.NextToken();
+  EXPECT_TRUE(next->IsString());
+  EXPECT_EQ("END", next->AsString());
+
+  next = t.NextToken();
+  EXPECT_TRUE(next->IsEOS());
+}
+
+}  // namespace amber
diff --git a/src/value.cc b/src/value.cc
new file mode 100644
index 0000000..3bc5e5f
--- /dev/null
+++ b/src/value.cc
@@ -0,0 +1,25 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/value.h"
+
+namespace amber {
+
+Value::Value() = default;
+
+Value::Value(const Value&) = default;
+
+Value::~Value() = default;
+
+}  // namespace amber
diff --git a/src/value.h b/src/value.h
new file mode 100644
index 0000000..06de7d6
--- /dev/null
+++ b/src/value.h
@@ -0,0 +1,62 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VALUE_H_
+#define SRC_VALUE_H_
+
+#include <cstdint>
+
+namespace amber {
+
+class Value {
+ public:
+  Value();
+  Value(const Value&);
+  ~Value();
+
+  void SetIntValue(uint64_t val) {
+    type_ = Type::kInteger;
+    uint_value_ = val;
+  }
+  bool IsInteger() const { return type_ == Type::kInteger; }
+
+  void SetDoubleValue(double val) {
+    type_ = Type::kFloat;
+    double_value_ = val;
+  }
+  bool IsFloat() const { return type_ == Type::kFloat; }
+
+  uint8_t AsUint8() const { return static_cast<uint8_t>(uint_value_); }
+  uint16_t AsUint16() const { return static_cast<uint16_t>(uint_value_); }
+  uint32_t AsUint32() const { return static_cast<uint32_t>(uint_value_); }
+  uint64_t AsUint64() const { return static_cast<uint64_t>(uint_value_); }
+
+  int8_t AsInt8() const { return static_cast<int8_t>(uint_value_); }
+  int16_t AsInt16() const { return static_cast<int16_t>(uint_value_); }
+  int32_t AsInt32() const { return static_cast<int32_t>(uint_value_); }
+  int64_t AsInt64() const { return static_cast<int64_t>(uint_value_); }
+
+  float AsFloat() const { return static_cast<float>(double_value_); }
+  double AsDouble() const { return double_value_; }
+
+ private:
+  enum class Type { kFloat, kInteger };
+  Type type_ = Type::kFloat;
+  uint64_t uint_value_ = 0;
+  double double_value_ = 0.0;
+};
+
+}  // namespace amber
+
+#endif  // SRC_VALUE_H_
diff --git a/src/vkscript/command_parser.cc b/src/vkscript/command_parser.cc
new file mode 100644
index 0000000..af10525
--- /dev/null
+++ b/src/vkscript/command_parser.cc
@@ -0,0 +1,1856 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vkscript/command_parser.h"
+
+#include <algorithm>
+#include <cassert>
+
+#include "src/command_data.h"
+#include "src/make_unique.h"
+#include "src/tokenizer.h"
+#include "src/vkscript/datum_type_parser.h"
+
+namespace amber {
+namespace vkscript {
+namespace {
+
+ShaderType ShaderNameToType(const std::string& name) {
+  if (name == "fragment")
+    return ShaderType::kFragment;
+  if (name == "compute")
+    return ShaderType::kCompute;
+  if (name == "geometry")
+    return ShaderType::kGeometry;
+  if (name == "tessellation evaluation")
+    return ShaderType::kTessellationEvaluation;
+  if (name == "tessellation control")
+    return ShaderType::kTessellationControl;
+
+  return ShaderType::kVertex;
+}
+
+}  // namespace
+
+CommandParser::CommandParser() = default;
+
+CommandParser::~CommandParser() = default;
+
+Result CommandParser::ParseBoolean(const std::string& str, bool* result) {
+  assert(result);
+
+  std::string tmp;
+  tmp.resize(str.size());
+  std::transform(str.begin(), str.end(), tmp.begin(),
+                 [](unsigned char c) { return std::tolower(c); });
+
+  if (tmp == "true") {
+    *result = true;
+    return {};
+  }
+  if (tmp == "false") {
+    *result = false;
+    return {};
+  }
+  return Result("Invalid value passed as a boolean string");
+}
+
+Result CommandParser::Parse(const std::string& data) {
+  tokenizer_ = MakeUnique<Tokenizer>(data);
+
+  for (auto token = tokenizer_->NextToken(); !token->IsEOS();
+       token = tokenizer_->NextToken()) {
+    if (token->IsEOL())
+      continue;
+
+    if (!token->IsString()) {
+      return Result(
+          "Command not recognized. Received something other then a string.");
+    }
+
+    std::string cmd_name = token->AsString();
+    Result r;
+    if (cmd_name == "draw") {
+      token = tokenizer_->NextToken();
+      if (!token->IsString())
+        return Result("Invalid draw command in test");
+
+      cmd_name = token->AsString();
+      if (cmd_name == "rect")
+        r = ProcessDrawRect();
+      else if (cmd_name == "arrays")
+        r = ProcessDrawArrays();
+      else
+        return Result("Unknown draw command: " + cmd_name);
+
+    } else if (cmd_name == "clear") {
+      r = ProcessClear();
+    } else if (cmd_name == "ssbo") {
+      r = ProcessSSBO();
+    } else if (cmd_name == "uniform") {
+      r = ProcessUniform();
+    } else if (cmd_name == "patch") {
+      r = ProcessPatch();
+    } else if (cmd_name == "probe") {
+      r = ProcessProbe(false);
+    } else if (cmd_name == "tolerance") {
+      r = ProcessTolerance();
+    } else if (cmd_name == "relative") {
+      token = tokenizer_->NextToken();
+      if (!token->IsString() || token->AsString() != "probe")
+        return Result("relative must be used with probe");
+
+      r = ProcessProbe(true);
+    } else if (cmd_name == "compute") {
+      r = ProcessCompute();
+    } else if (cmd_name == "vertex" || cmd_name == "fragment" ||
+               cmd_name == "geometry" || cmd_name == "tessellation") {
+      std::string shader_name = cmd_name;
+      if (cmd_name == "tessellation") {
+        token = tokenizer_->NextToken();
+        if (!token->IsString() || (token->AsString() != "control" &&
+                                   token->AsString() != "evaluation")) {
+          return Result(
+              "Tessellation entrypoint must have <evaluation|control> in name");
+        }
+        shader_name += " " + token->AsString();
+      }
+
+      token = tokenizer_->NextToken();
+      if (!token->IsString() || token->AsString() != "entrypoint")
+        return Result("Unknown command: " + shader_name);
+
+      r = ProcessEntryPoint(shader_name);
+
+      // Pipeline Commands
+    } else if (cmd_name == "primitiveRestartEnable") {
+      r = ProcessPrimitiveRestartEnable();
+    } else if (cmd_name == "depthClampEnable") {
+      r = ProcessDepthClampEnable();
+    } else if (cmd_name == "rasterizerDiscardEnable") {
+      r = ProcessRasterizerDiscardEnable();
+    } else if (cmd_name == "depthBiasEnable") {
+      r = ProcessDepthBiasEnable();
+    } else if (cmd_name == "logicOpEnable") {
+      r = ProcessLogicOpEnable();
+    } else if (cmd_name == "blendEnable") {
+      r = ProcessBlendEnable();
+    } else if (cmd_name == "depthTestEnable") {
+      r = ProcessDepthTestEnable();
+    } else if (cmd_name == "depthWriteEnable") {
+      r = ProcessDepthWriteEnable();
+    } else if (cmd_name == "depthBoundsTestEnable") {
+      r = ProcessDepthBoundsTestEnable();
+    } else if (cmd_name == "stencilTestEnable") {
+      r = ProcessStencilTestEnable();
+    } else if (cmd_name == "topology") {
+      r = ProcessTopology();
+    } else if (cmd_name == "polygonMode") {
+      r = ProcessPolygonMode();
+    } else if (cmd_name == "logicOp") {
+      r = ProcessLogicOp();
+    } else if (cmd_name == "frontFace") {
+      r = ProcessFrontFace();
+    } else if (cmd_name == "cullMode") {
+      r = ProcessCullMode();
+    } else if (cmd_name == "depthBiasConstantFactor") {
+      r = ProcessDepthBiasConstantFactor();
+    } else if (cmd_name == "depthBiasClamp") {
+      r = ProcessDepthBiasClamp();
+    } else if (cmd_name == "depthBiasSlopeFactor") {
+      r = ProcessDepthBiasSlopeFactor();
+    } else if (cmd_name == "lineWidth") {
+      r = ProcessLineWidth();
+    } else if (cmd_name == "minDepthBounds") {
+      r = ProcessMinDepthBounds();
+    } else if (cmd_name == "maxDepthBounds") {
+      r = ProcessMaxDepthBounds();
+    } else if (cmd_name == "srcColorBlendFactor") {
+      r = ProcessSrcColorBlendFactor();
+    } else if (cmd_name == "dstColorBlendFactor") {
+      r = ProcessDstColorBlendFactor();
+    } else if (cmd_name == "srcAlphaBlendFactor") {
+      r = ProcessSrcAlphaBlendFactor();
+    } else if (cmd_name == "dstAlphaBlendFactor") {
+      r = ProcessDstAlphaBlendFactor();
+    } else if (cmd_name == "colorBlendOp") {
+      r = ProcessColorBlendOp();
+    } else if (cmd_name == "alphaBlendOp") {
+      r = ProcessAlphaBlendOp();
+    } else if (cmd_name == "depthCompareOp") {
+      r = ProcessDepthCompareOp();
+    } else if (cmd_name == "front.compareOp") {
+      r = ProcessFrontCompareOp();
+    } else if (cmd_name == "back.compareOp") {
+      r = ProcessBackCompareOp();
+    } else if (cmd_name == "front.failOp") {
+      r = ProcessFrontFailOp();
+    } else if (cmd_name == "front.passOp") {
+      r = ProcessFrontPassOp();
+    } else if (cmd_name == "front.depthFailOp") {
+      r = ProcessFrontDepthFailOp();
+    } else if (cmd_name == "back.failOp") {
+      r = ProcessBackFailOp();
+    } else if (cmd_name == "back.passOp") {
+      r = ProcessBackPassOp();
+    } else if (cmd_name == "back.depthFailOp") {
+      r = ProcessBackDepthFailOp();
+    } else if (cmd_name == "front.compareMask") {
+      r = ProcessFrontCompareMask();
+    } else if (cmd_name == "front.writeMask") {
+      r = ProcessFrontWriteMask();
+    } else if (cmd_name == "back.compareMask") {
+      r = ProcessBackCompareMask();
+    } else if (cmd_name == "back.writeMask") {
+      r = ProcessBackWriteMask();
+    } else if (cmd_name == "front.reference") {
+      r = ProcessFrontReference();
+    } else if (cmd_name == "back.reference") {
+      r = ProcessBackReference();
+    } else if (cmd_name == "colorWriteMask") {
+      r = ProcessColorWriteMask();
+    } else {
+      return Result("Unknown command: " + cmd_name);
+    }
+
+    if (!r.IsSuccess())
+      return r;
+  }
+
+  return {};
+}
+
+Result CommandParser::ProcessDrawRect() {
+  auto cmd = MakeUnique<DrawRectCommand>(pipeline_data_);
+
+  auto token = tokenizer_->NextToken();
+  while (token->IsString()) {
+    std::string str = token->AsString();
+    if (str != "ortho" && str != "patch")
+      return Result("Unknown parameter to draw rect: " + str);
+
+    if (str == "ortho") {
+      cmd->EnableOrtho();
+    } else {
+      cmd->EnablePatch();
+    }
+    token = tokenizer_->NextToken();
+  }
+
+  Result r = token->ConvertToDouble();
+  if (!r.IsSuccess())
+    return r;
+  cmd->SetX(token->AsFloat());
+
+  token = tokenizer_->NextToken();
+  r = token->ConvertToDouble();
+  if (!r.IsSuccess())
+    return r;
+  cmd->SetY(token->AsFloat());
+
+  token = tokenizer_->NextToken();
+  r = token->ConvertToDouble();
+  if (!r.IsSuccess())
+    return r;
+  cmd->SetWidth(token->AsFloat());
+
+  token = tokenizer_->NextToken();
+  r = token->ConvertToDouble();
+  if (!r.IsSuccess())
+    return r;
+  cmd->SetHeight(token->AsFloat());
+
+  token = tokenizer_->NextToken();
+  if (!token->IsEOS() && !token->IsEOL())
+    return Result("Extra parameter to draw rect command");
+
+  commands_.push_back(std::move(cmd));
+  return {};
+}
+
+Result CommandParser::ProcessDrawArrays() {
+  auto cmd = MakeUnique<DrawArraysCommand>(pipeline_data_);
+
+  auto token = tokenizer_->NextToken();
+  while (token->IsString()) {
+    std::string str = token->AsString();
+    if (str != "indexed" && str != "instanced") {
+      Topology topo = NameToTopology(token->AsString());
+      if (topo != Topology::kUnknown) {
+        cmd->SetTopology(topo);
+
+        // Advance token here so we're consistent with the non-topology case.
+        token = tokenizer_->NextToken();
+        break;
+      }
+      return Result("Unknown parameter to draw arrays: " + str);
+    }
+
+    if (str == "indexed") {
+      cmd->EnableIndexed();
+    } else {
+      cmd->EnableInstanced();
+    }
+    token = tokenizer_->NextToken();
+  }
+
+  if (cmd->GetTopology() == Topology::kUnknown)
+    return Result("Missing draw arrays topology");
+
+  if (!token->IsInteger())
+    return Result("Missing integer first vertex value for draw arrays");
+  cmd->SetFirstVertexIndex(token->AsUint32());
+
+  token = tokenizer_->NextToken();
+  if (!token->IsInteger())
+    return Result("Missing integer vertex count value for draw arrays");
+  cmd->SetVertexCount(token->AsUint32());
+
+  token = tokenizer_->NextToken();
+  if (cmd->IsInstanced()) {
+    if (!token->IsEOL() && !token->IsEOS()) {
+      if (!token->IsInteger())
+        return Result("Invalid instance count for draw arrays");
+
+      cmd->SetInstanceCount(token->AsUint32());
+    }
+    token = tokenizer_->NextToken();
+  }
+
+  if (!token->IsEOL() && !token->IsEOS())
+    return Result("Extra parameter to draw arrays command");
+
+  commands_.push_back(std::move(cmd));
+  return {};
+}
+
+Result CommandParser::ProcessCompute() {
+  auto cmd = MakeUnique<ComputeCommand>(pipeline_data_);
+
+  auto token = tokenizer_->NextToken();
+
+  // Compute can start a compute line or an entryp oint line ...
+  if (token->IsString() && token->AsString() == "entrypoint")
+    return ProcessEntryPoint("compute");
+
+  if (!token->IsInteger())
+    return Result("Missing integer value for compute X entry");
+  cmd->SetX(token->AsUint32());
+
+  token = tokenizer_->NextToken();
+  if (!token->IsInteger())
+    return Result("Missing integer value for compute Y entry");
+  cmd->SetY(token->AsUint32());
+
+  token = tokenizer_->NextToken();
+  if (!token->IsInteger())
+    return Result("Missing integer value for compute Z entry");
+  cmd->SetZ(token->AsUint32());
+
+  token = tokenizer_->NextToken();
+  if (!token->IsEOS() && !token->IsEOL())
+    return Result("Extra parameter to compute command");
+
+  commands_.push_back(std::move(cmd));
+  return {};
+}
+
+Result CommandParser::ProcessClear() {
+  std::unique_ptr<Command> cmd;
+
+  auto token = tokenizer_->NextToken();
+  std::string cmd_suffix = "";
+  if (token->IsString()) {
+    std::string str = token->AsString();
+    cmd_suffix = str + " ";
+    if (str == "depth") {
+      cmd = MakeUnique<ClearDepthCommand>();
+
+      token = tokenizer_->NextToken();
+      Result r = token->ConvertToDouble();
+      if (!r.IsSuccess())
+        return r;
+
+      cmd->AsClearDepth()->SetValue(token->AsFloat());
+    } else if (str == "stencil") {
+      cmd = MakeUnique<ClearStencilCommand>();
+
+      token = tokenizer_->NextToken();
+      if (token->IsEOL() || token->IsEOS())
+        return Result("Missing stencil value for clear stencil command");
+      if (!token->IsInteger())
+        return Result("Invalid stencil value for clear stencil command");
+
+      cmd->AsClearStencil()->SetValue(token->AsUint32());
+    } else if (str == "color") {
+      cmd = MakeUnique<ClearColorCommand>();
+
+      token = tokenizer_->NextToken();
+      Result r = token->ConvertToDouble();
+      if (!r.IsSuccess())
+        return r;
+      cmd->AsClearColor()->SetR(token->AsFloat());
+
+      token = tokenizer_->NextToken();
+      r = token->ConvertToDouble();
+      if (!r.IsSuccess())
+        return r;
+      cmd->AsClearColor()->SetG(token->AsFloat());
+
+      token = tokenizer_->NextToken();
+      r = token->ConvertToDouble();
+      if (!r.IsSuccess())
+        return r;
+      cmd->AsClearColor()->SetB(token->AsFloat());
+
+      token = tokenizer_->NextToken();
+      r = token->ConvertToDouble();
+      if (!r.IsSuccess())
+        return r;
+      cmd->AsClearColor()->SetA(token->AsFloat());
+    } else {
+      return Result("Extra parameter to clear command");
+    }
+
+    token = tokenizer_->NextToken();
+  } else {
+    cmd = MakeUnique<ClearCommand>();
+  }
+  if (!token->IsEOS() && !token->IsEOL())
+    return Result("Extra parameter to clear " + cmd_suffix + "command");
+
+  commands_.push_back(std::move(cmd));
+  return {};
+}
+
+Result CommandParser::ParseValues(const std::string& name,
+                                  const DatumType& type,
+                                  std::vector<Value>* values) {
+  assert(values);
+
+  auto token = tokenizer_->NextToken();
+  while (!token->IsEOL() && !token->IsEOS()) {
+    Value v;
+
+    if ((type.IsFloat() || type.IsDouble())) {
+      if (!token->IsInteger() && !token->IsDouble()) {
+        return Result(std::string("Invalid value provided to ") + name +
+                      "  command");
+      }
+
+      Result r = token->ConvertToDouble();
+      if (!r.IsSuccess())
+        return r;
+
+      v.SetDoubleValue(token->AsDouble());
+    } else {
+      if (!token->IsInteger()) {
+        return Result(std::string("Invalid value provided to ") + name +
+                      " command");
+      }
+
+      v.SetIntValue(token->AsUint64());
+    }
+
+    values->push_back(v);
+    token = tokenizer_->NextToken();
+  }
+
+  size_t seen = values->size();
+  // This could overflow, but I don't really expect us to get command files
+  // that big ....
+  size_t num_per_row = type.ColumnCount() * type.RowCount();
+  if (seen == 0 || (seen % num_per_row) != 0) {
+    return Result(std::string("Incorrect number of values provided to ") +
+                  name + " command");
+  }
+
+  return {};
+}
+
+Result CommandParser::ProcessSSBO() {
+  auto cmd = MakeUnique<BufferCommand>(BufferCommand::BufferType::kSSBO);
+
+  auto token = tokenizer_->NextToken();
+  if (token->IsEOL() || token->IsEOS())
+    return Result("Missing binding and size values for ssbo command");
+  if (!token->IsInteger())
+    return Result("Invalid binding value for ssbo command");
+
+  uint32_t val = token->AsUint32();
+
+  token = tokenizer_->NextToken();
+  if (token->IsString() && token->AsString() != "subdata") {
+    auto& str = token->AsString();
+    if (str.size() >= 2 && str[0] == ':') {
+      cmd->SetDescriptorSet(val);
+
+      auto substr = str.substr(1, str.size());
+      uint64_t binding_val = strtoul(substr.c_str(), nullptr, 10);
+      if (binding_val > std::numeric_limits<uint32_t>::max())
+        return Result("binding value too large in ssbo command");
+
+      cmd->SetBinding(static_cast<uint32_t>(binding_val));
+    } else {
+      return Result("Invalid value for ssbo command");
+    }
+
+    token = tokenizer_->NextToken();
+  } else {
+    cmd->SetBinding(val);
+  }
+
+  if (token->IsString() && token->AsString() == "subdata") {
+    cmd->SetIsSubdata();
+
+    token = tokenizer_->NextToken();
+    if (!token->IsString())
+      return Result("Invalid type for ssbo command");
+
+    DatumTypeParser tp;
+    Result r = tp.Parse(token->AsString());
+    if (!r.IsSuccess())
+      return r;
+
+    cmd->SetDatumType(tp.GetType());
+
+    token = tokenizer_->NextToken();
+    if (!token->IsInteger())
+      return Result("Invalid offset for ssbo command");
+
+    cmd->SetOffset(token->AsUint32());
+
+    std::vector<Value> values;
+    r = ParseValues("ssbo", cmd->GetDatumType(), &values);
+    if (!r.IsSuccess())
+      return r;
+
+    cmd->SetValues(std::move(values));
+
+  } else {
+    if (token->IsEOL() || token->IsEOS())
+      return Result("Missing size value for ssbo command");
+    if (!token->IsInteger())
+      return Result("Invalid size value for ssbo command");
+
+    cmd->SetSize(token->AsUint32());
+
+    token = tokenizer_->NextToken();
+    if (!token->IsEOS() && !token->IsEOL())
+      return Result("Extra parameter for ssbo command");
+  }
+
+  commands_.push_back(std::move(cmd));
+  return {};
+}
+
+Result CommandParser::ProcessUniform() {
+  auto token = tokenizer_->NextToken();
+  if (token->IsEOL() || token->IsEOS())
+    return Result("Missing binding and size values for uniform command");
+  if (!token->IsString())
+    return Result("Invalid type value for uniform command");
+
+  std::unique_ptr<BufferCommand> cmd;
+  if (token->AsString() == "ubo") {
+    cmd = MakeUnique<BufferCommand>(BufferCommand::BufferType::kUniform);
+
+    token = tokenizer_->NextToken();
+    if (!token->IsInteger())
+      return Result("Invalid binding value for uniform ubo command");
+
+    uint32_t val = token->AsUint32();
+
+    token = tokenizer_->NextToken();
+    if (!token->IsString())
+      return Result("Invalid type value for uniform ubo command");
+
+    auto& str = token->AsString();
+    if (str.size() >= 2 && str[0] == ':') {
+      cmd->SetDescriptorSet(val);
+
+      auto substr = str.substr(1, str.size());
+      uint64_t binding_val = strtoul(substr.c_str(), nullptr, 10);
+      if (binding_val > std::numeric_limits<uint32_t>::max())
+        return Result("binding value too large in uniform ubo command");
+
+      cmd->SetBinding(static_cast<uint32_t>(binding_val));
+
+      token = tokenizer_->NextToken();
+      if (!token->IsString())
+        return Result("Invalid type value for uniform ubo command");
+    } else {
+      cmd->SetBinding(val);
+    }
+
+  } else {
+    cmd = MakeUnique<BufferCommand>(BufferCommand::BufferType::kPushConstant);
+  }
+
+  DatumTypeParser tp;
+  Result r = tp.Parse(token->AsString());
+  if (!r.IsSuccess())
+    return r;
+
+  cmd->SetDatumType(tp.GetType());
+
+  token = tokenizer_->NextToken();
+  if (!token->IsInteger())
+    return Result("Invalid offset value for uniform command");
+
+  cmd->SetOffset(token->AsUint32());
+
+  std::vector<Value> values;
+  r = ParseValues("uniform", cmd->GetDatumType(), &values);
+  if (!r.IsSuccess())
+    return r;
+
+  cmd->SetValues(std::move(values));
+
+  commands_.push_back(std::move(cmd));
+  return {};
+}
+
+Result CommandParser::ProcessTolerance() {
+  auto cmd = MakeUnique<ToleranceCommand>();
+
+  auto token = tokenizer_->NextToken();
+  size_t found_tokens = 0;
+  while (!token->IsEOL() && !token->IsEOS() && found_tokens < 4) {
+    if (token->IsString() && token->AsString() == ",") {
+      token = tokenizer_->NextToken();
+      continue;
+    }
+
+    if (token->IsInteger() || token->IsDouble()) {
+      Result r = token->ConvertToDouble();
+      if (!r.IsSuccess())
+        return r;
+      double value = token->AsDouble();
+
+      token = tokenizer_->NextToken();
+      if (token->IsString() && token->AsString() != ",") {
+        if (token->AsString() != "%")
+          return Result("Invalid value for tolerance command");
+
+        cmd->AddPercentTolerance(value);
+        token = tokenizer_->NextToken();
+      } else {
+        cmd->AddValueTolerance(value);
+      }
+    } else {
+      return Result("Invalid value for tolerance command");
+    }
+
+    ++found_tokens;
+  }
+  if (found_tokens == 0)
+    return Result("Missing value for tolerance command");
+  if (found_tokens != 1 && found_tokens != 4)
+    return Result("Invalid number of tolerance parameters provided");
+
+  if (!token->IsEOS() && !token->IsEOL())
+    return Result("Extra parameter for tolerance command");
+
+  commands_.push_back(std::move(cmd));
+  return {};
+}
+
+Result CommandParser::ProcessPatch() {
+  auto cmd = MakeUnique<PatchParameterVerticesCommand>();
+
+  auto token = tokenizer_->NextToken();
+  if (!token->IsString() || token->AsString() != "parameter")
+    return Result("Missing parameter flag to patch command");
+
+  token = tokenizer_->NextToken();
+  if (!token->IsString() || token->AsString() != "vertices")
+    return Result("Missing vertices flag to patch command");
+
+  token = tokenizer_->NextToken();
+  if (!token->IsInteger())
+    return Result("Invalid count parameter for patch parameter vertices");
+  cmd->SetControlPointCount(token->AsUint32());
+
+  token = tokenizer_->NextToken();
+  if (!token->IsEOS() && !token->IsEOL())
+    return Result("Extra parameter for patch parameter vertices command");
+
+  commands_.push_back(std::move(cmd));
+  return {};
+}
+
+Result CommandParser::ProcessEntryPoint(const std::string& name) {
+  auto cmd = MakeUnique<EntryPointCommand>();
+
+  auto token = tokenizer_->NextToken();
+  if (token->IsEOL() || token->IsEOS())
+    return Result("Missing entrypoint name");
+
+  if (!token->IsString())
+    return Result("Entrypoint name must be a string");
+
+  cmd->SetShaderType(ShaderNameToType(name));
+  cmd->SetEntryPointName(token->AsString());
+
+  token = tokenizer_->NextToken();
+  if (!token->IsEOS() && !token->IsEOL())
+    return Result("Extra parameter for entrypoint command");
+
+  commands_.push_back(std::move(cmd));
+
+  return {};
+}
+
+Result CommandParser::ProcessProbe(bool relative) {
+  auto token = tokenizer_->NextToken();
+  if (!token->IsString())
+    return Result("Invalid token in probe command");
+
+  // The SSBO syntax is different from probe or probe all so handle specially.
+  if (token->AsString() == "ssbo")
+    return ProcessProbeSSBO();
+
+  auto cmd = MakeUnique<ProbeCommand>();
+  if (relative)
+    cmd->SetRelative();
+
+  bool is_rect = false;
+  if (token->AsString() == "rect") {
+    is_rect = true;
+
+    token = tokenizer_->NextToken();
+    if (!token->IsString())
+      return Result("Invalid token in probe command");
+  } else if (token->AsString() == "all") {
+    cmd->SetWholeWindow();
+
+    token = tokenizer_->NextToken();
+    if (!token->IsString())
+      return Result("Invalid token in probe command");
+  }
+
+  std::string format = token->AsString();
+  if (format != "rgba" && format != "rgb")
+    return Result("Invalid format specified to probe command");
+
+  if (format == "rgba")
+    cmd->SetIsRGBA();
+
+  token = tokenizer_->NextToken();
+  if (!cmd->IsWholeWindow()) {
+    bool got_rect_open_bracket = false;
+    if (token->IsOpenBracket()) {
+      got_rect_open_bracket = true;
+      token = tokenizer_->NextToken();
+    }
+
+    Result r = token->ConvertToDouble();
+    if (!r.IsSuccess())
+      return r;
+    cmd->SetX(token->AsFloat());
+
+    token = tokenizer_->NextToken();
+    if (token->IsComma())
+      token = tokenizer_->NextToken();
+
+    r = token->ConvertToDouble();
+    if (!r.IsSuccess())
+      return r;
+    cmd->SetY(token->AsFloat());
+
+    if (is_rect) {
+      token = tokenizer_->NextToken();
+      if (token->IsComma())
+        token = tokenizer_->NextToken();
+
+      r = token->ConvertToDouble();
+      if (!r.IsSuccess())
+        return r;
+      cmd->SetWidth(token->AsFloat());
+
+      token = tokenizer_->NextToken();
+      if (token->IsComma())
+        token = tokenizer_->NextToken();
+
+      r = token->ConvertToDouble();
+      if (!r.IsSuccess())
+        return r;
+      cmd->SetHeight(token->AsFloat());
+    }
+
+    token = tokenizer_->NextToken();
+    if (token->IsCloseBracket()) {
+      // Close bracket without an open
+      if (!got_rect_open_bracket)
+        return Result("Missing open bracket for probe command");
+
+      token = tokenizer_->NextToken();
+    } else if (got_rect_open_bracket) {
+      // An open bracket without a close bracket.
+      return Result("Missing close bracket for probe command");
+    }
+  }
+
+  bool got_color_open_bracket = false;
+  if (token->IsOpenBracket()) {
+    got_color_open_bracket = true;
+    token = tokenizer_->NextToken();
+  }
+
+  Result r = token->ConvertToDouble();
+  if (!r.IsSuccess())
+    return r;
+  cmd->SetR(token->AsFloat());
+
+  token = tokenizer_->NextToken();
+  if (token->IsComma())
+    token = tokenizer_->NextToken();
+
+  r = token->ConvertToDouble();
+  if (!r.IsSuccess())
+    return r;
+  cmd->SetG(token->AsFloat());
+
+  token = tokenizer_->NextToken();
+  if (token->IsComma())
+    token = tokenizer_->NextToken();
+
+  r = token->ConvertToDouble();
+  if (!r.IsSuccess())
+    return r;
+  cmd->SetB(token->AsFloat());
+
+  if (format == "rgba") {
+    token = tokenizer_->NextToken();
+    if (token->IsComma())
+      token = tokenizer_->NextToken();
+
+    r = token->ConvertToDouble();
+    if (!r.IsSuccess())
+      return r;
+    cmd->SetA(token->AsFloat());
+  }
+
+  token = tokenizer_->NextToken();
+  if (token->IsCloseBracket()) {
+    if (!got_color_open_bracket) {
+      // Close without an open.
+      return Result("Missing open bracket for probe command");
+    }
+    token = tokenizer_->NextToken();
+  } else if (got_color_open_bracket) {
+    // Open bracket without a close.
+    return Result("Missing close bracket for probe command");
+  }
+
+  if (!token->IsEOS() && !token->IsEOL())
+    return Result("Extra parameter to probe command");
+
+  commands_.push_back(std::move(cmd));
+  return {};
+}
+
+Result CommandParser::ProcessTopology() {
+  auto token = tokenizer_->NextToken();
+  if (token->IsEOS() || token->IsEOL())
+    return Result("Missing value for topology command");
+  if (!token->IsString())
+    return Result("Invalid value for topology command");
+
+  Topology topology = Topology::kPatchList;
+  std::string topo = token->AsString();
+
+  if (topo == "VK_PRIMITIVE_TOPOLOGY_PATCH_LIST")
+    topology = Topology::kPatchList;
+  else if (topo == "VK_PRIMITIVE_TOPOLOGY_POINT_LIST")
+    topology = Topology::kPointList;
+  else if (topo == "VK_PRIMITIVE_TOPOLOGY_LINE_LIST")
+    topology = Topology::kLineList;
+  else if (topo == "VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY")
+    topology = Topology::kLineListWithAdjacency;
+  else if (topo == "VK_PRIMITIVE_TOPOLOGY_LINE_STRIP")
+    topology = Topology::kLineStrip;
+  else if (topo == "VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY")
+    topology = Topology::kLineStripWithAdjacency;
+  else if (topo == "VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN")
+    topology = Topology::kTriangleFan;
+  else if (topo == "VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST")
+    topology = Topology::kTriangleList;
+  else if (topo == "VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY")
+    topology = Topology::kTriangleListWithAdjacency;
+  else if (topo == "VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP")
+    topology = Topology::kTriangleStrip;
+  else if (topo == "VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY")
+    topology = Topology::kTriangleStripWithAdjacency;
+  else
+    return Result("Unknown value for topology command");
+
+  token = tokenizer_->NextToken();
+  if (!token->IsEOS() && !token->IsEOL())
+    return Result("Extra parameter for topology command");
+
+  pipeline_data_.SetTopology(topology);
+  return {};
+}
+
+Result CommandParser::ProcessPolygonMode() {
+  auto token = tokenizer_->NextToken();
+  if (token->IsEOS() || token->IsEOL())
+    return Result("Missing value for polygonMode command");
+  if (!token->IsString())
+    return Result("Invalid value for polygonMode command");
+
+  PolygonMode mode = PolygonMode::kFill;
+  std::string m = token->AsString();
+  if (m == "VK_POLYGON_MODE_FILL")
+    mode = PolygonMode::kFill;
+  else if (m == "VK_POLYGON_MODE_LINE")
+    mode = PolygonMode::kLine;
+  else if (m == "VK_POLYGON_MODE_POINT")
+    mode = PolygonMode::kPoint;
+  else
+    return Result("Unknown value for polygonMode command");
+
+  token = tokenizer_->NextToken();
+  if (!token->IsEOS() && !token->IsEOL())
+    return Result("Extra parameter for polygonMode command");
+
+  pipeline_data_.SetPolygonMode(mode);
+  return {};
+}
+
+Result CommandParser::ProcessLogicOp() {
+  auto token = tokenizer_->NextToken();
+  if (token->IsEOS() || token->IsEOL())
+    return Result("Missing value for logicOp command");
+  if (!token->IsString())
+    return Result("Invalid value for logicOp command");
+
+  LogicOp op = LogicOp::kClear;
+  std::string name = token->AsString();
+  if (name == "VK_LOGIC_OP_CLEAR")
+    op = LogicOp::kClear;
+  else if (name == "VK_LOGIC_OP_AND")
+    op = LogicOp::kAnd;
+  else if (name == "VK_LOGIC_OP_AND_REVERSE")
+    op = LogicOp::kAndReverse;
+  else if (name == "VK_LOGIC_OP_COPY")
+    op = LogicOp::kCopy;
+  else if (name == "VK_LOGIC_OP_AND_INVERTED")
+    op = LogicOp::kAndInverted;
+  else if (name == "VK_LOGIC_OP_NO_OP")
+    op = LogicOp::kNoOp;
+  else if (name == "VK_LOGIC_OP_XOR")
+    op = LogicOp::kXor;
+  else if (name == "VK_LOGIC_OP_OR")
+    op = LogicOp::kOr;
+  else if (name == "VK_LOGIC_OP_NOR")
+    op = LogicOp::kNor;
+  else if (name == "VK_LOGIC_OP_EQUIVALENT")
+    op = LogicOp::kEquivalent;
+  else if (name == "VK_LOGIC_OP_INVERT")
+    op = LogicOp::kInvert;
+  else if (name == "VK_LOGIC_OP_OR_REVERSE")
+    op = LogicOp::kOrReverse;
+  else if (name == "VK_LOGIC_OP_COPY_INVERTED")
+    op = LogicOp::kCopyInverted;
+  else if (name == "VK_LOGIC_OP_OR_INVERTED")
+    op = LogicOp::kOrInverted;
+  else if (name == "VK_LOGIC_OP_NAND")
+    op = LogicOp::kNand;
+  else if (name == "VK_LOGIC_OP_SET")
+    op = LogicOp::kSet;
+  else
+    return Result("Unknown value for logicOp command");
+
+  token = tokenizer_->NextToken();
+  if (!token->IsEOS() && !token->IsEOL())
+    return Result("Extra parameter for logicOp command");
+
+  pipeline_data_.SetLogicOp(op);
+  return {};
+}
+
+Result CommandParser::ProcessCullMode() {
+  auto token = tokenizer_->NextToken();
+  if (token->IsEOS() || token->IsEOL())
+    return Result("Missing value for cullMode command");
+  if (!token->IsString())
+    return Result("Invalid value for cullMode command");
+
+  CullMode mode = CullMode::kNone;
+  while (!token->IsEOS() && !token->IsEOL()) {
+    std::string name = token->AsString();
+
+    if (name == "|") {
+      // We treat everything as an |.
+    } else if (name == "VK_CULL_MODE_FRONT_BIT") {
+      if (mode == CullMode::kNone)
+        mode = CullMode::kFront;
+      else if (mode == CullMode::kBack)
+        mode = CullMode::kFrontAndBack;
+    } else if (name == "VK_CULL_MODE_BACK_BIT") {
+      if (mode == CullMode::kNone)
+        mode = CullMode::kBack;
+      else if (mode == CullMode::kFront)
+        mode = CullMode::kFrontAndBack;
+    } else if (name == "VK_CULL_MODE_FRONT_AND_BACK") {
+      mode = CullMode::kFrontAndBack;
+    } else if (name == "VK_CULL_MODE_NONE") {
+      // Do nothing ...
+    } else {
+      return Result("Unknown value for cullMode command");
+    }
+
+    token = tokenizer_->NextToken();
+  }
+
+  pipeline_data_.SetCullMode(mode);
+  return {};
+}
+
+Result CommandParser::ProcessFrontFace() {
+  auto token = tokenizer_->NextToken();
+  if (token->IsEOS() || token->IsEOL())
+    return Result("Missing value for frontFace command");
+  if (!token->IsString())
+    return Result("Invalid value for frontFace command");
+
+  FrontFace face = FrontFace::kCounterClockwise;
+  std::string f = token->AsString();
+  if (f == "VK_FRONT_FACE_COUNTER_CLOCKWISE")
+    face = FrontFace::kCounterClockwise;
+  else if (f == "VK_FRONT_FACE_CLOCKWISE")
+    face = FrontFace::kClockwise;
+  else
+    return Result("Unknown value for frontFace command");
+
+  token = tokenizer_->NextToken();
+  if (!token->IsEOS() && !token->IsEOL())
+    return Result("Extra parameter for frontFace command");
+
+  pipeline_data_.SetFrontFace(face);
+  return {};
+}
+
+Result CommandParser::ProcessBooleanPipelineData(const std::string& name,
+                                                 bool* value) {
+  auto token = tokenizer_->NextToken();
+  if (token->IsEOS() || token->IsEOL())
+    return Result("Missing value for " + name + " command");
+  if (!token->IsString())
+    return Result("Invalid value for " + name + " command");
+
+  Result r = ParseBoolean(token->AsString(), value);
+  if (!r.IsSuccess())
+    return r;
+
+  token = tokenizer_->NextToken();
+  if (!token->IsEOS() && !token->IsEOL())
+    return Result("Extra parameter for " + name + " command");
+
+  return {};
+}
+
+Result CommandParser::ProcessPrimitiveRestartEnable() {
+  bool value = false;
+  Result r = ProcessBooleanPipelineData("primitiveRestartEnable", &value);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetEnablePrimitiveRestart(value);
+  return {};
+}
+
+Result CommandParser::ProcessDepthClampEnable() {
+  bool value = false;
+  Result r = ProcessBooleanPipelineData("depthClampEnable", &value);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetEnableDepthClamp(value);
+  return {};
+}
+
+Result CommandParser::ProcessRasterizerDiscardEnable() {
+  bool value = false;
+  Result r = ProcessBooleanPipelineData("rasterizerDiscardEnable", &value);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetEnableRasterizerDiscard(value);
+  return {};
+}
+
+Result CommandParser::ProcessDepthBiasEnable() {
+  bool value = false;
+  Result r = ProcessBooleanPipelineData("depthBiasEnable", &value);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetEnableDepthBias(value);
+  return {};
+}
+
+Result CommandParser::ProcessLogicOpEnable() {
+  bool value = false;
+  Result r = ProcessBooleanPipelineData("logicOpEnable", &value);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetEnableLogicOp(value);
+  return {};
+}
+
+Result CommandParser::ProcessBlendEnable() {
+  bool value = false;
+  Result r = ProcessBooleanPipelineData("blendEnable", &value);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetEnableBlend(value);
+  return {};
+}
+
+Result CommandParser::ProcessDepthTestEnable() {
+  bool value = false;
+  Result r = ProcessBooleanPipelineData("depthTestEnable", &value);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetEnableDepthTest(value);
+  return {};
+}
+
+Result CommandParser::ProcessDepthWriteEnable() {
+  bool value = false;
+  Result r = ProcessBooleanPipelineData("depthWriteEnable", &value);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetEnableDepthWrite(value);
+  return {};
+}
+
+Result CommandParser::ProcessDepthBoundsTestEnable() {
+  bool value = false;
+  Result r = ProcessBooleanPipelineData("depthBoundsTestEnable", &value);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetEnableDepthBoundsTest(value);
+  return {};
+}
+
+Result CommandParser::ProcessStencilTestEnable() {
+  bool value = false;
+  Result r = ProcessBooleanPipelineData("stencilTestEnable", &value);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetEnableStencilTest(value);
+  return {};
+}
+
+Result CommandParser::ProcessFloatPipelineData(const std::string& name,
+                                               float* value) {
+  assert(value);
+
+  auto token = tokenizer_->NextToken();
+  if (token->IsEOS() || token->IsEOL())
+    return Result("Missing value for " + name + " command");
+
+  Result r = token->ConvertToDouble();
+  if (!r.IsSuccess())
+    return r;
+
+  *value = token->AsFloat();
+
+  token = tokenizer_->NextToken();
+  if (!token->IsEOS() && !token->IsEOL())
+    return Result("Extra parameter for " + name + " command");
+
+  return {};
+}
+
+Result CommandParser::ProcessDepthBiasConstantFactor() {
+  float value = 0.0;
+  Result r = ProcessFloatPipelineData("depthBiasConstantFactor", &value);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetDepthBiasConstantFactor(value);
+  return {};
+}
+
+Result CommandParser::ProcessDepthBiasClamp() {
+  float value = 0.0;
+  Result r = ProcessFloatPipelineData("depthBiasClamp", &value);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetDepthBiasClamp(value);
+  return {};
+}
+
+Result CommandParser::ProcessDepthBiasSlopeFactor() {
+  float value = 0.0;
+  Result r = ProcessFloatPipelineData("depthBiasSlopeFactor", &value);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetDepthBiasSlopeFactor(value);
+  return {};
+}
+
+Result CommandParser::ProcessLineWidth() {
+  float value = 0.0;
+  Result r = ProcessFloatPipelineData("lineWidth", &value);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetLineWidth(value);
+  return {};
+}
+
+Result CommandParser::ProcessMinDepthBounds() {
+  float value = 0.0;
+  Result r = ProcessFloatPipelineData("minDepthBounds", &value);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetMinDepthBounds(value);
+  return {};
+}
+
+Result CommandParser::ProcessMaxDepthBounds() {
+  float value = 0.0;
+  Result r = ProcessFloatPipelineData("maxDepthBounds", &value);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetMaxDepthBounds(value);
+  return {};
+}
+
+Result CommandParser::ParseBlendFactorName(const std::string& name,
+                                           BlendFactor* factor) {
+  assert(factor);
+
+  if (name == "VK_BLEND_FACTOR_ZERO")
+    *factor = BlendFactor::kZero;
+  else if (name == "VK_BLEND_FACTOR_ONE")
+    *factor = BlendFactor::kOne;
+  else if (name == "VK_BLEND_FACTOR_SRC_COLOR")
+    *factor = BlendFactor::kSrcColor;
+  else if (name == "VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR")
+    *factor = BlendFactor::kOneMinusSrcColor;
+  else if (name == "VK_BLEND_FACTOR_DST_COLOR")
+    *factor = BlendFactor::kDstColor;
+  else if (name == "VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR")
+    *factor = BlendFactor::kOneMinusDstColor;
+  else if (name == "VK_BLEND_FACTOR_SRC_ALPHA")
+    *factor = BlendFactor::kSrcAlpha;
+  else if (name == "VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA")
+    *factor = BlendFactor::kOneMinusSrcAlpha;
+  else if (name == "VK_BLEND_FACTOR_DST_ALPHA")
+    *factor = BlendFactor::kDstAlpha;
+  else if (name == "VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA")
+    *factor = BlendFactor::kOneMinusDstAlpha;
+  else if (name == "VK_BLEND_FACTOR_CONSTANT_COLOR")
+    *factor = BlendFactor::kConstantColor;
+  else if (name == "VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_COLOR")
+    *factor = BlendFactor::kOneMinusConstantColor;
+  else if (name == "VK_BLEND_FACTOR_CONSTANT_ALPHA")
+    *factor = BlendFactor::kConstantAlpha;
+  else if (name == "VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_ALPHA")
+    *factor = BlendFactor::kOneMinusConstantAlpha;
+  else if (name == "VK_BLEND_FACTOR_SRC_ALPHA_SATURATE")
+    *factor = BlendFactor::kSrcAlphaSaturate;
+  else if (name == "VK_BLEND_FACTOR_SRC1_COLOR")
+    *factor = BlendFactor::kSrc1Color;
+  else if (name == "VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR")
+    *factor = BlendFactor::kOneMinusSrc1Color;
+  else if (name == "VK_BLEND_FACTOR_SRC1_ALPHA")
+    *factor = BlendFactor::kSrc1Alpha;
+  else if (name == "VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA")
+    *factor = BlendFactor::kOneMinusSrc1Alpha;
+  else
+    return Result("Unknown BlendFactor provided: " + name);
+
+  return {};
+}
+
+Result CommandParser::ParseBlendFactor(const std::string& name,
+                                       BlendFactor* factor) {
+  auto token = tokenizer_->NextToken();
+  if (token->IsEOL() || token->IsEOS())
+    return Result(std::string("Missing parameter for ") + name + " command");
+  if (!token->IsString())
+    return Result(std::string("Invalid parameter for ") + name + " command");
+
+  Result r = ParseBlendFactorName(token->AsString(), factor);
+  if (!r.IsSuccess())
+    return r;
+
+  token = tokenizer_->NextToken();
+  if (!token->IsEOS() && !token->IsEOL())
+    return Result(std::string("Extra parameter for ") + name + " command");
+
+  return {};
+}
+
+Result CommandParser::ProcessSrcAlphaBlendFactor() {
+  BlendFactor factor = BlendFactor::kZero;
+  Result r = ParseBlendFactor("srcAlphaBlendFactor", &factor);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetSrcAlphaBlendFactor(factor);
+  return {};
+}
+
+Result CommandParser::ProcessDstAlphaBlendFactor() {
+  BlendFactor factor = BlendFactor::kZero;
+  Result r = ParseBlendFactor("dstAlphaBlendFactor", &factor);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetDstAlphaBlendFactor(factor);
+  return {};
+}
+
+Result CommandParser::ProcessSrcColorBlendFactor() {
+  BlendFactor factor = BlendFactor::kZero;
+  Result r = ParseBlendFactor("srcColorBlendFactor", &factor);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetSrcColorBlendFactor(factor);
+  return {};
+}
+
+Result CommandParser::ProcessDstColorBlendFactor() {
+  BlendFactor factor = BlendFactor::kZero;
+  Result r = ParseBlendFactor("dstColorBlendFactor", &factor);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetDstColorBlendFactor(factor);
+  return {};
+}
+
+Result CommandParser::ParseBlendOpName(const std::string& name, BlendOp* op) {
+  assert(op);
+
+  if (name == "VK_BLEND_OP_ADD")
+    *op = BlendOp::kAdd;
+  else if (name == "VK_BLEND_OP_ADD")
+    *op = BlendOp::kAdd;
+  else if (name == "VK_BLEND_OP_SUBTRACT")
+    *op = BlendOp::kSubtract;
+  else if (name == "VK_BLEND_OP_REVERSE_SUBTRACT")
+    *op = BlendOp::kReverseSubtract;
+  else if (name == "VK_BLEND_OP_MIN")
+    *op = BlendOp::kMin;
+  else if (name == "VK_BLEND_OP_MAX")
+    *op = BlendOp::kMax;
+  else if (name == "VK_BLEND_OP_ZERO_EXT")
+    *op = BlendOp::kZero;
+  else if (name == "VK_BLEND_OP_SRC_EXT")
+    *op = BlendOp::kSrc;
+  else if (name == "VK_BLEND_OP_DST_EXT")
+    *op = BlendOp::kDst;
+  else if (name == "VK_BLEND_OP_SRC_OVER_EXT")
+    *op = BlendOp::kSrcOver;
+  else if (name == "VK_BLEND_OP_DST_OVER_EXT")
+    *op = BlendOp::kDstOver;
+  else if (name == "VK_BLEND_OP_SRC_IN_EXT")
+    *op = BlendOp::kSrcIn;
+  else if (name == "VK_BLEND_OP_DST_IN_EXT")
+    *op = BlendOp::kDstIn;
+  else if (name == "VK_BLEND_OP_SRC_OUT_EXT")
+    *op = BlendOp::kSrcOut;
+  else if (name == "VK_BLEND_OP_DST_OUT_EXT")
+    *op = BlendOp::kDstOut;
+  else if (name == "VK_BLEND_OP_SRC_ATOP_EXT")
+    *op = BlendOp::kSrcAtop;
+  else if (name == "VK_BLEND_OP_DST_ATOP_EXT")
+    *op = BlendOp::kDstAtop;
+  else if (name == "VK_BLEND_OP_XOR_EXT")
+    *op = BlendOp::kXor;
+  else if (name == "VK_BLEND_OP_MULTIPLY_EXT")
+    *op = BlendOp::kMultiply;
+  else if (name == "VK_BLEND_OP_SCREEN_EXT")
+    *op = BlendOp::kScreen;
+  else if (name == "VK_BLEND_OP_OVERLAY_EXT")
+    *op = BlendOp::kOverlay;
+  else if (name == "VK_BLEND_OP_DARKEN_EXT")
+    *op = BlendOp::kDarken;
+  else if (name == "VK_BLEND_OP_LIGHTEN_EXT")
+    *op = BlendOp::kLighten;
+  else if (name == "VK_BLEND_OP_COLORDODGE_EXT")
+    *op = BlendOp::kColorDodge;
+  else if (name == "VK_BLEND_OP_COLORBURN_EXT")
+    *op = BlendOp::kColorBurn;
+  else if (name == "VK_BLEND_OP_HARDLIGHT_EXT")
+    *op = BlendOp::kHardLight;
+  else if (name == "VK_BLEND_OP_SOFTLIGHT_EXT")
+    *op = BlendOp::kSoftLight;
+  else if (name == "VK_BLEND_OP_DIFFERENCE_EXT")
+    *op = BlendOp::kDifference;
+  else if (name == "VK_BLEND_OP_EXCLUSION_EXT")
+    *op = BlendOp::kExclusion;
+  else if (name == "VK_BLEND_OP_INVERT_EXT")
+    *op = BlendOp::kInvert;
+  else if (name == "VK_BLEND_OP_INVERT_RGB_EXT")
+    *op = BlendOp::kInvertRGB;
+  else if (name == "VK_BLEND_OP_LINEARDODGE_EXT")
+    *op = BlendOp::kLinearDodge;
+  else if (name == "VK_BLEND_OP_LINEARBURN_EXT")
+    *op = BlendOp::kLinearBurn;
+  else if (name == "VK_BLEND_OP_VIVIDLIGHT_EXT")
+    *op = BlendOp::kVividLight;
+  else if (name == "VK_BLEND_OP_LINEARLIGHT_EXT")
+    *op = BlendOp::kLinearLight;
+  else if (name == "VK_BLEND_OP_PINLIGHT_EXT")
+    *op = BlendOp::kPinLight;
+  else if (name == "VK_BLEND_OP_HARDMIX_EXT")
+    *op = BlendOp::kHardMix;
+  else if (name == "VK_BLEND_OP_HSL_HUE_EXT")
+    *op = BlendOp::kHslHue;
+  else if (name == "VK_BLEND_OP_HSL_SATURATION_EXT")
+    *op = BlendOp::kHslSaturation;
+  else if (name == "VK_BLEND_OP_HSL_COLOR_EXT")
+    *op = BlendOp::kHslColor;
+  else if (name == "VK_BLEND_OP_HSL_LUMINOSITY_EXT")
+    *op = BlendOp::kHslLuminosity;
+  else if (name == "VK_BLEND_OP_PLUS_EXT")
+    *op = BlendOp::kPlus;
+  else if (name == "VK_BLEND_OP_PLUS_CLAMPED_EXT")
+    *op = BlendOp::kPlusClamped;
+  else if (name == "VK_BLEND_OP_PLUS_CLAMPED_ALPHA_EXT")
+    *op = BlendOp::kPlusClampedAlpha;
+  else if (name == "VK_BLEND_OP_PLUS_DARKER_EXT")
+    *op = BlendOp::kPlusDarker;
+  else if (name == "VK_BLEND_OP_MINUS_EXT")
+    *op = BlendOp::kMinus;
+  else if (name == "VK_BLEND_OP_MINUS_CLAMPED_EXT")
+    *op = BlendOp::kMinusClamped;
+  else if (name == "VK_BLEND_OP_CONTRAST_EXT")
+    *op = BlendOp::kContrast;
+  else if (name == "VK_BLEND_OP_INVERT_OVG_EXT")
+    *op = BlendOp::kInvertOvg;
+  else if (name == "VK_BLEND_OP_RED_EXT")
+    *op = BlendOp::kRed;
+  else if (name == "VK_BLEND_OP_GREEN_EXT")
+    *op = BlendOp::kGreen;
+  else if (name == "VK_BLEND_OP_BLUE_EXT")
+    *op = BlendOp::kBlue;
+  else
+    return Result("Unknown BlendOp provided: " + name);
+
+  return {};
+}
+
+Result CommandParser::ParseBlendOp(const std::string& name, BlendOp* op) {
+  auto token = tokenizer_->NextToken();
+  if (token->IsEOL() || token->IsEOS())
+    return Result(std::string("Missing parameter for ") + name + " command");
+  if (!token->IsString())
+    return Result(std::string("Invalid parameter for ") + name + " command");
+
+  Result r = ParseBlendOpName(token->AsString(), op);
+  if (!r.IsSuccess())
+    return r;
+
+  token = tokenizer_->NextToken();
+  if (!token->IsEOS() && !token->IsEOL())
+    return Result(std::string("Extra parameter for ") + name + " command");
+
+  return {};
+}
+
+Result CommandParser::ProcessColorBlendOp() {
+  BlendOp op = BlendOp::kAdd;
+  Result r = ParseBlendOp("colorBlendOp", &op);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetColorBlendOp(op);
+  return {};
+}
+
+Result CommandParser::ProcessAlphaBlendOp() {
+  BlendOp op = BlendOp::kAdd;
+  Result r = ParseBlendOp("alphaBlendOp", &op);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetAlphaBlendOp(op);
+  return {};
+}
+
+Result CommandParser::ParseCompareOp(const std::string& name, CompareOp* op) {
+  auto token = tokenizer_->NextToken();
+  if (token->IsEOL() || token->IsEOS())
+    return Result(std::string("Missing parameter for ") + name + " command");
+  if (!token->IsString())
+    return Result(std::string("Invalid parameter for ") + name + " command");
+
+  Result r = ParseCompareOpName(token->AsString(), op);
+  if (!r.IsSuccess())
+    return r;
+
+  token = tokenizer_->NextToken();
+  if (!token->IsEOS() && !token->IsEOL())
+    return Result(std::string("Extra parameter for ") + name + " command");
+
+  return {};
+}
+
+Result CommandParser::ParseCompareOpName(const std::string& name,
+                                         CompareOp* op) {
+  assert(op);
+
+  if (name == "VK_COMPARE_OP_NEVER")
+    *op = CompareOp::kNever;
+  else if (name == "VK_COMPARE_OP_LESS")
+    *op = CompareOp::kLess;
+  else if (name == "VK_COMPARE_OP_EQUAL")
+    *op = CompareOp::kEqual;
+  else if (name == "VK_COMPARE_OP_LESS_OR_EQUAL")
+    *op = CompareOp::kLessOrEqual;
+  else if (name == "VK_COMPARE_OP_GREATER")
+    *op = CompareOp::kGreater;
+  else if (name == "VK_COMPARE_OP_NOT_EQUAL")
+    *op = CompareOp::kNotEqual;
+  else if (name == "VK_COMPARE_OP_GREATER_OR_EQUAL")
+    *op = CompareOp::kGreaterOrEqual;
+  else if (name == "VK_COMPARE_OP_ALWAYS")
+    *op = CompareOp::kAlways;
+  else
+    return Result("Unknown CompareOp provided: " + name);
+
+  return {};
+}
+
+Result CommandParser::ProcessDepthCompareOp() {
+  CompareOp op = CompareOp::kNever;
+  Result r = ParseCompareOp("depthCompareOp", &op);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetDepthCompareOp(op);
+  return {};
+}
+
+Result CommandParser::ProcessFrontCompareOp() {
+  CompareOp op = CompareOp::kNever;
+  Result r = ParseCompareOp("front.compareOp", &op);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetFrontCompareOp(op);
+  return {};
+}
+
+Result CommandParser::ProcessBackCompareOp() {
+  CompareOp op = CompareOp::kNever;
+  Result r = ParseCompareOp("back.compareOp", &op);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetBackCompareOp(op);
+  return {};
+}
+
+Result CommandParser::ParseStencilOp(const std::string& name, StencilOp* op) {
+  auto token = tokenizer_->NextToken();
+  if (token->IsEOL() || token->IsEOS())
+    return Result(std::string("Missing parameter for ") + name + " command");
+  if (!token->IsString())
+    return Result(std::string("Invalid parameter for ") + name + " command");
+
+  Result r = ParseStencilOpName(token->AsString(), op);
+  if (!r.IsSuccess())
+    return r;
+
+  token = tokenizer_->NextToken();
+  if (!token->IsEOS() && !token->IsEOL())
+    return Result(std::string("Extra parameter for ") + name + " command");
+
+  return {};
+}
+
+Result CommandParser::ParseStencilOpName(const std::string& name,
+                                         StencilOp* op) {
+  assert(op);
+
+  if (name == "VK_STENCIL_OP_KEEP")
+    *op = StencilOp::kKeep;
+  else if (name == "VK_STENCIL_OP_ZERO")
+    *op = StencilOp::kZero;
+  else if (name == "VK_STENCIL_OP_REPLACE")
+    *op = StencilOp::kReplace;
+  else if (name == "VK_STENCIL_OP_INCREMENT_AND_CLAMP")
+    *op = StencilOp::kIncrementAndClamp;
+  else if (name == "VK_STENCIL_OP_DECREMENT_AND_CLAMP")
+    *op = StencilOp::kDecrementAndClamp;
+  else if (name == "VK_STENCIL_OP_INVERT")
+    *op = StencilOp::kInvert;
+  else if (name == "VK_STENCIL_OP_INCREMENT_AND_WRAP")
+    *op = StencilOp::kIncrementAndWrap;
+  else if (name == "VK_STENCIL_OP_DECREMENT_AND_WRAP")
+    *op = StencilOp::kDecrementAndWrap;
+  else
+    return Result("Unknown StencilOp provided: " + name);
+
+  return {};
+}
+
+Result CommandParser::ProcessFrontFailOp() {
+  StencilOp op = StencilOp::kKeep;
+  Result r = ParseStencilOp("front.failOp", &op);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetFrontFailOp(op);
+  return {};
+}
+
+Result CommandParser::ProcessFrontPassOp() {
+  StencilOp op = StencilOp::kKeep;
+  Result r = ParseStencilOp("front.passOp", &op);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetFrontPassOp(op);
+  return {};
+}
+
+Result CommandParser::ProcessFrontDepthFailOp() {
+  StencilOp op = StencilOp::kKeep;
+  Result r = ParseStencilOp("front.depthFailOp", &op);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetFrontDepthFailOp(op);
+  return {};
+}
+
+Result CommandParser::ProcessBackFailOp() {
+  StencilOp op = StencilOp::kKeep;
+  Result r = ParseStencilOp("back.failOp", &op);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetBackFailOp(op);
+  return {};
+}
+
+Result CommandParser::ProcessBackPassOp() {
+  StencilOp op = StencilOp::kKeep;
+  Result r = ParseStencilOp("back.passOp", &op);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetBackPassOp(op);
+  return {};
+}
+
+Result CommandParser::ProcessBackDepthFailOp() {
+  StencilOp op = StencilOp::kKeep;
+  Result r = ParseStencilOp("back.depthFailOp", &op);
+  if (!r.IsSuccess())
+    return r;
+
+  pipeline_data_.SetBackDepthFailOp(op);
+  return {};
+}
+
+Result CommandParser::ProcessFrontCompareMask() {
+  return Result("front.compareMask not implemented");
+}
+
+Result CommandParser::ProcessFrontWriteMask() {
+  return Result("front.writeMask not implemented");
+}
+
+Result CommandParser::ProcessBackCompareMask() {
+  return Result("back.compareMask not implemented");
+}
+
+Result CommandParser::ProcessBackWriteMask() {
+  return Result("back.writeMask not implemented");
+}
+
+Result CommandParser::ProcessFrontReference() {
+  auto token = tokenizer_->NextToken();
+  if (token->IsEOL() || token->IsEOS())
+    return Result("Missing parameter for front.reference command");
+  if (!token->IsInteger())
+    return Result("Invalid parameter for front.reference command");
+
+  pipeline_data_.SetFrontReference(token->AsUint32());
+
+  token = tokenizer_->NextToken();
+  if (!token->IsEOS() && !token->IsEOL())
+    return Result("Extra parameter for front.reference command");
+
+  return {};
+}
+
+Result CommandParser::ProcessBackReference() {
+  auto token = tokenizer_->NextToken();
+  if (token->IsEOL() || token->IsEOS())
+    return Result("Missing parameter for back.reference command");
+  if (!token->IsInteger())
+    return Result("Invalid parameter for back.reference command");
+
+  pipeline_data_.SetBackReference(token->AsUint32());
+
+  token = tokenizer_->NextToken();
+  if (!token->IsEOS() && !token->IsEOL())
+    return Result("Extra parameter for back.reference command");
+
+  return {};
+}
+
+Result CommandParser::ProcessColorWriteMask() {
+  auto token = tokenizer_->NextToken();
+  if (token->IsEOS() || token->IsEOL())
+    return Result("Missing parameter for colorWriteMask command");
+  if (!token->IsString())
+    return Result("Invalid parameter for colorWriteMask command");
+
+  uint8_t mask = 0;
+  while (!token->IsEOS() && !token->IsEOL()) {
+    std::string name = token->AsString();
+
+    if (name == "|") {
+      // We treat everything as an |.
+    } else if (name == "VK_COLOR_COMPONENT_R_BIT") {
+      mask |= kColorMaskR;
+    } else if (name == "VK_COLOR_COMPONENT_G_BIT") {
+      mask |= kColorMaskG;
+    } else if (name == "VK_COLOR_COMPONENT_B_BIT") {
+      mask |= kColorMaskB;
+    } else if (name == "VK_COLOR_COMPONENT_A_BIT") {
+      mask |= kColorMaskA;
+    } else {
+      return Result("Unknown parameter for colorWriteMask command");
+    }
+
+    token = tokenizer_->NextToken();
+  }
+
+  pipeline_data_.SetColorWriteMask(mask);
+  return {};
+}
+
+Result CommandParser::ParseComparator(const std::string& name,
+                                      ProbeSSBOCommand::Comparator* op) {
+  if (name == "==")
+    *op = ProbeSSBOCommand::Comparator::kEqual;
+  else if (name == "!=")
+    *op = ProbeSSBOCommand::Comparator::kNotEqual;
+  else if (name == "~=")
+    *op = ProbeSSBOCommand::Comparator::kFuzzyEqual;
+  else if (name == "<")
+    *op = ProbeSSBOCommand::Comparator::kLess;
+  else if (name == "<=")
+    *op = ProbeSSBOCommand::Comparator::kLessOrEqual;
+  else if (name == ">")
+    *op = ProbeSSBOCommand::Comparator::kGreater;
+  else if (name == ">=")
+    *op = ProbeSSBOCommand::Comparator::kGreaterOrEqual;
+  else
+    return Result("Invalid comparator");
+  return {};
+}
+
+Result CommandParser::ProcessProbeSSBO() {
+  auto cmd = MakeUnique<ProbeSSBOCommand>();
+
+  auto token = tokenizer_->NextToken();
+  if (token->IsEOL() || token->IsEOS())
+    return Result("Missing values for probe ssbo command");
+  if (!token->IsString())
+    return Result("Invalid type for probe ssbo command");
+
+  DatumTypeParser tp;
+  Result r = tp.Parse(token->AsString());
+  if (!r.IsSuccess())
+    return r;
+
+  cmd->SetDatumType(tp.GetType());
+
+  token = tokenizer_->NextToken();
+  if (!token->IsInteger())
+    return Result("Invalid binding value for probe ssbo command");
+
+  uint32_t val = token->AsUint32();
+
+  token = tokenizer_->NextToken();
+  if (token->IsString()) {
+    auto& str = token->AsString();
+    if (str.size() >= 2 && str[0] == ':') {
+      cmd->SetDescriptorSet(val);
+
+      auto substr = str.substr(1, str.size());
+      uint64_t binding_val = strtoul(substr.c_str(), nullptr, 10);
+      if (binding_val > std::numeric_limits<uint32_t>::max())
+        return Result("binding value too large in probe ssbo command");
+
+      cmd->SetBinding(static_cast<uint32_t>(binding_val));
+    } else {
+      return Result("Invalid value for probe ssbo command");
+    }
+
+    token = tokenizer_->NextToken();
+  } else {
+    cmd->SetBinding(val);
+  }
+
+  if (!token->IsInteger())
+    return Result("Invalid offset for probe ssbo command");
+
+  cmd->SetOffset(token->AsUint32());
+
+  token = tokenizer_->NextToken();
+  if (!token->IsString())
+    return Result("Invalid comparator for probe ssbo command");
+
+  ProbeSSBOCommand::Comparator comp;
+  r = ParseComparator(token->AsString(), &comp);
+  if (!r.IsSuccess())
+    return r;
+
+  cmd->SetComparator(comp);
+
+  std::vector<Value> values;
+  r = ParseValues("probe ssbo", cmd->GetDatumType(), &values);
+  if (!r.IsSuccess())
+    return r;
+
+  cmd->SetValues(std::move(values));
+
+  commands_.push_back(std::move(cmd));
+  return {};
+}
+
+}  // namespace vkscript
+}  // namespace amber
diff --git a/src/vkscript/command_parser.h b/src/vkscript/command_parser.h
new file mode 100644
index 0000000..2ded530
--- /dev/null
+++ b/src/vkscript/command_parser.h
@@ -0,0 +1,159 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VKSCRIPT_COMMAND_PARSER_H_
+#define SRC_VKSCRIPT_COMMAND_PARSER_H_
+
+#include <memory>
+#include <vector>
+
+#include "amber/result.h"
+#include "src/command.h"
+#include "src/datum_type.h"
+#include "src/pipeline_data.h"
+
+namespace amber {
+
+class Tokenizer;
+class Token;
+
+namespace vkscript {
+
+class CommandParser {
+ public:
+  CommandParser();
+  ~CommandParser();
+
+  Result Parse(const std::string& data);
+
+  void AddCommand(std::unique_ptr<Command> command) {
+    commands_.push_back(std::move(command));
+  }
+
+  const std::vector<std::unique_ptr<Command>>& Commands() const {
+    return commands_;
+  }
+
+  std::vector<std::unique_ptr<Command>>&& TakeCommands() {
+    return std::move(commands_);
+  }
+
+  const PipelineData* PipelineDataForTesting() const { return &pipeline_data_; }
+
+  Result ParseBooleanForTesting(const std::string& str, bool* result) {
+    return ParseBoolean(str, result);
+  }
+
+  Result ParseBlendFactorNameForTesting(const std::string& name,
+                                        BlendFactor* factor) {
+    return ParseBlendFactorName(name, factor);
+  }
+  Result ParseBlendOpNameForTesting(const std::string& name, BlendOp* op) {
+    return ParseBlendOpName(name, op);
+  }
+  Result ParseCompareOpNameForTesting(const std::string& name, CompareOp* op) {
+    return ParseCompareOpName(name, op);
+  }
+  Result ParseStencilOpNameForTesting(const std::string& name, StencilOp* op) {
+    return ParseStencilOpName(name, op);
+  }
+  Result ParseComparatorForTesting(const std::string& name,
+                                   ProbeSSBOCommand::Comparator* op) {
+    return ParseComparator(name, op);
+  }
+
+ private:
+  Result TokenToFloat(Token* token, float* val) const;
+  Result TokenToDouble(Token* token, double* val) const;
+  Result ParseBoolean(const std::string& str, bool* result);
+  Result ParseValues(const std::string& name,
+                     const DatumType& type,
+                     std::vector<Value>* values);
+
+  Result ProcessDrawRect();
+  Result ProcessDrawArrays();
+  Result ProcessCompute();
+  Result ProcessClear();
+  Result ProcessPatch();
+  Result ProcessSSBO();
+  Result ProcessUniform();
+  Result ProcessTolerance();
+  Result ProcessEntryPoint(const std::string& name);
+  Result ProcessProbe(bool relative);
+  Result ProcessProbeSSBO();
+  Result ProcessTopology();
+  Result ProcessPolygonMode();
+  Result ProcessLogicOp();
+  Result ProcessCullMode();
+  Result ProcessFrontFace();
+  Result ProcessFloatPipelineData(const std::string& name, float* value);
+  Result ProcessDepthBiasConstantFactor();
+  Result ProcessDepthBiasClamp();
+  Result ProcessDepthBiasSlopeFactor();
+  Result ProcessLineWidth();
+  Result ProcessMinDepthBounds();
+  Result ProcessMaxDepthBounds();
+  Result ProcessBooleanPipelineData(const std::string& name, bool* value);
+  Result ProcessPrimitiveRestartEnable();
+  Result ProcessDepthClampEnable();
+  Result ProcessRasterizerDiscardEnable();
+  Result ProcessDepthBiasEnable();
+  Result ProcessLogicOpEnable();
+  Result ProcessBlendEnable();
+  Result ProcessDepthTestEnable();
+  Result ProcessDepthWriteEnable();
+  Result ProcessDepthBoundsTestEnable();
+  Result ProcessStencilTestEnable();
+  Result ParseBlendFactor(const std::string& name, BlendFactor* factor);
+  Result ParseBlendFactorName(const std::string& name, BlendFactor* factor);
+  Result ProcessSrcAlphaBlendFactor();
+  Result ProcessDstAlphaBlendFactor();
+  Result ProcessSrcColorBlendFactor();
+  Result ProcessDstColorBlendFactor();
+  Result ParseBlendOp(const std::string& name, BlendOp* op);
+  Result ParseBlendOpName(const std::string& name, BlendOp* op);
+  Result ProcessColorBlendOp();
+  Result ProcessAlphaBlendOp();
+  Result ParseCompareOp(const std::string& name, CompareOp* op);
+  Result ParseCompareOpName(const std::string& name, CompareOp* op);
+  Result ProcessDepthCompareOp();
+  Result ProcessFrontCompareOp();
+  Result ProcessBackCompareOp();
+  Result ParseStencilOp(const std::string& name, StencilOp* op);
+  Result ParseStencilOpName(const std::string& name, StencilOp* op);
+  Result ProcessFrontFailOp();
+  Result ProcessFrontPassOp();
+  Result ProcessFrontDepthFailOp();
+  Result ProcessBackFailOp();
+  Result ProcessBackPassOp();
+  Result ProcessBackDepthFailOp();
+  Result ProcessFrontCompareMask();
+  Result ProcessFrontWriteMask();
+  Result ProcessBackCompareMask();
+  Result ProcessBackWriteMask();
+  Result ProcessFrontReference();
+  Result ProcessBackReference();
+  Result ProcessColorWriteMask();
+  Result ParseComparator(const std::string& name,
+                         ProbeSSBOCommand::Comparator* op);
+
+  PipelineData pipeline_data_;
+  std::unique_ptr<Tokenizer> tokenizer_;
+  std::vector<std::unique_ptr<Command>> commands_;
+};
+
+}  // namespace vkscript
+}  // namespace amber
+
+#endif  // SRC_VKSCRIPT_COMMAND_PARSER_H_
diff --git a/src/vkscript/command_parser_test.cc b/src/vkscript/command_parser_test.cc
new file mode 100644
index 0000000..214350c
--- /dev/null
+++ b/src/vkscript/command_parser_test.cc
@@ -0,0 +1,3488 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vkscript/command_parser.h"
+#include "gtest/gtest.h"
+#include "src/vkscript/section_parser.h"
+
+namespace amber {
+namespace vkscript {
+
+using CommandParserTest = testing::Test;
+
+TEST_F(CommandParserTest, MultipleCommands) {
+  std::string data = R"(# this is the test data
+draw rect 1.2 2.3 200 400.2
+# another comment
+clear color 255 128 1 100 # set clear color
+clear
+# done)";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(3U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsDrawRect());
+
+  auto* draw_cmd = cmds[0]->AsDrawRect();
+  EXPECT_FALSE(draw_cmd->IsOrtho());
+  EXPECT_FALSE(draw_cmd->IsPatch());
+  EXPECT_FLOAT_EQ(1.2, draw_cmd->GetX());
+  EXPECT_FLOAT_EQ(2.3, draw_cmd->GetY());
+  EXPECT_FLOAT_EQ(200, draw_cmd->GetWidth());
+  EXPECT_FLOAT_EQ(400.2, draw_cmd->GetHeight());
+
+  ASSERT_TRUE(cmds[1]->IsClearColor());
+
+  auto* clear_cmd = cmds[1]->AsClearColor();
+  EXPECT_EQ(255, clear_cmd->GetR());
+  EXPECT_EQ(128, clear_cmd->GetG());
+  EXPECT_EQ(1, clear_cmd->GetB());
+  EXPECT_EQ(100, clear_cmd->GetA());
+
+  ASSERT_TRUE(cmds[2]->IsClear());
+}
+
+TEST_F(CommandParserTest, DISABLED_DrawArraysNonInstancedFollowedByCommand) {}
+
+TEST_F(CommandParserTest, DISABLED_DrawArraysInstancedFollowedByCommand) {}
+
+TEST_F(CommandParserTest, DISABLED_UnknownCommand) {}
+
+TEST_F(CommandParserTest, DrawRect) {
+  std::string data = "draw rect 1.2 2.3 200 400.2";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsDrawRect());
+
+  auto* cmd = cmds[0]->AsDrawRect();
+  EXPECT_FALSE(cmd->IsOrtho());
+  EXPECT_FALSE(cmd->IsPatch());
+  EXPECT_FLOAT_EQ(1.2, cmd->GetX());
+  EXPECT_FLOAT_EQ(2.3, cmd->GetY());
+  EXPECT_FLOAT_EQ(200, cmd->GetWidth());
+  EXPECT_FLOAT_EQ(400.2, cmd->GetHeight());
+}
+
+TEST_F(CommandParserTest, DrawRectWithOrth) {
+  std::string data = "draw rect ortho 1.2 2.3 200 400.2";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsDrawRect());
+
+  auto* cmd = cmds[0]->AsDrawRect();
+  EXPECT_TRUE(cmd->IsOrtho());
+  EXPECT_FALSE(cmd->IsPatch());
+  EXPECT_FLOAT_EQ(1.2, cmd->GetX());
+  EXPECT_FLOAT_EQ(2.3, cmd->GetY());
+  EXPECT_FLOAT_EQ(200, cmd->GetWidth());
+  EXPECT_FLOAT_EQ(400.2, cmd->GetHeight());
+}
+
+TEST_F(CommandParserTest, DrawRectWithPatch) {
+  std::string data = "draw rect patch 1.2 2.3 200 400.2";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsDrawRect());
+
+  auto* cmd = cmds[0]->AsDrawRect();
+  EXPECT_FALSE(cmd->IsOrtho());
+  EXPECT_TRUE(cmd->IsPatch());
+  EXPECT_FLOAT_EQ(1.2, cmd->GetX());
+  EXPECT_FLOAT_EQ(2.3, cmd->GetY());
+  EXPECT_FLOAT_EQ(200, cmd->GetWidth());
+  EXPECT_FLOAT_EQ(400.2, cmd->GetHeight());
+}
+
+TEST_F(CommandParserTest, DrawRectWithOrthAndPatch) {
+  std::string data = "draw rect ortho patch 1.2 2.3 200 400.2";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsDrawRect());
+
+  auto* cmd = cmds[0]->AsDrawRect();
+  EXPECT_TRUE(cmd->IsOrtho());
+  EXPECT_TRUE(cmd->IsPatch());
+  EXPECT_FLOAT_EQ(1.2, cmd->GetX());
+  EXPECT_FLOAT_EQ(2.3, cmd->GetY());
+  EXPECT_FLOAT_EQ(200, cmd->GetWidth());
+  EXPECT_FLOAT_EQ(400.2, cmd->GetHeight());
+}
+
+TEST_F(CommandParserTest, DrawRectTooShort) {
+  std::string data = "draw rect 1.2 2.3 400.2";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid conversion to double", r.Error());
+}
+
+TEST_F(CommandParserTest, DrawRectExtraParameters) {
+  std::string data = "draw rect ortho patch 1.2 2.3 200 400.2 EXTRA";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Extra parameter to draw rect command", r.Error());
+}
+
+TEST_F(CommandParserTest, DrawArrays) {
+  std::string data = "draw arrays GL_LINES 2 4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsDrawArrays());
+
+  auto* cmd = cmds[0]->AsDrawArrays();
+  EXPECT_FALSE(cmd->IsIndexed());
+  EXPECT_FALSE(cmd->IsInstanced());
+  EXPECT_EQ(static_cast<uint32_t>(0U), cmd->GetInstanceCount());
+  EXPECT_EQ(Topology::kLineList, cmd->GetTopology());
+  EXPECT_EQ(2U, cmd->GetFirstVertexIndex());
+  EXPECT_EQ(4U, cmd->GetVertexCount());
+}
+
+TEST_F(CommandParserTest, DrawArraysIndexed) {
+  std::string data = "draw arrays indexed TRIANGLE_FAN 2 4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsDrawArrays());
+
+  auto* cmd = cmds[0]->AsDrawArrays();
+  EXPECT_TRUE(cmd->IsIndexed());
+  EXPECT_FALSE(cmd->IsInstanced());
+  EXPECT_EQ(static_cast<uint32_t>(0U), cmd->GetInstanceCount());
+  EXPECT_EQ(Topology::kTriangleFan, cmd->GetTopology());
+  EXPECT_EQ(2U, cmd->GetFirstVertexIndex());
+  EXPECT_EQ(4U, cmd->GetVertexCount());
+}
+
+TEST_F(CommandParserTest, DrawArraysExtraParams) {
+  std::string data = "draw arrays indexed TRIANGLE_FAN 2 4 EXTRA_PARAM";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Extra parameter to draw arrays command", r.Error());
+}
+
+TEST_F(CommandParserTest, DrawArraysInstanced) {
+  std::string data = "draw arrays instanced LINE_LIST_WITH_ADJACENCY 2 9";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsDrawArrays());
+
+  auto* cmd = cmds[0]->AsDrawArrays();
+  EXPECT_FALSE(cmd->IsIndexed());
+  EXPECT_TRUE(cmd->IsInstanced());
+  EXPECT_EQ(static_cast<uint32_t>(0U), cmd->GetInstanceCount());
+  EXPECT_EQ(Topology::kLineListWithAdjacency, cmd->GetTopology());
+  EXPECT_EQ(2U, cmd->GetFirstVertexIndex());
+  EXPECT_EQ(9U, cmd->GetVertexCount());
+}
+
+TEST_F(CommandParserTest, DrawArraysInstancedExtraParams) {
+  std::string data =
+      "draw arrays instanced LINE_LIST_WITH_ADJACENCY 2 9 4 EXTRA_COMMAND";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Extra parameter to draw arrays command", r.Error());
+}
+
+TEST_F(CommandParserTest, DrawArraysIndexedAndInstanced) {
+  std::string data =
+      "draw arrays indexed instanced LINE_LIST_WITH_ADJACENCY 3 9";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsDrawArrays());
+
+  auto* cmd = cmds[0]->AsDrawArrays();
+  EXPECT_TRUE(cmd->IsIndexed());
+  EXPECT_TRUE(cmd->IsInstanced());
+  EXPECT_EQ(static_cast<uint32_t>(0U), cmd->GetInstanceCount());
+  EXPECT_EQ(Topology::kLineListWithAdjacency, cmd->GetTopology());
+  EXPECT_EQ(3U, cmd->GetFirstVertexIndex());
+  EXPECT_EQ(9U, cmd->GetVertexCount());
+}
+
+TEST_F(CommandParserTest, DrawArraysInstancedWithCount) {
+  std::string data = "draw arrays instanced LINE_LIST_WITH_ADJACENCY 3 9 12";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsDrawArrays());
+
+  auto* cmd = cmds[0]->AsDrawArrays();
+  EXPECT_FALSE(cmd->IsIndexed());
+  EXPECT_TRUE(cmd->IsInstanced());
+  EXPECT_EQ(12U, cmd->GetInstanceCount());
+  EXPECT_EQ(Topology::kLineListWithAdjacency, cmd->GetTopology());
+  EXPECT_EQ(3U, cmd->GetFirstVertexIndex());
+  EXPECT_EQ(9U, cmd->GetVertexCount());
+}
+
+TEST_F(CommandParserTest, DrawArraysBadTopology) {
+  std::string data = "draw arrays UNKNOWN_TOPO 1 4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Unknown parameter to draw arrays: UNKNOWN_TOPO", r.Error());
+}
+
+TEST_F(CommandParserTest, DrawArraysTooShort) {
+  std::string data = "draw arrays PATCH_LIST 1";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Missing integer vertex count value for draw arrays", r.Error());
+}
+
+TEST_F(CommandParserTest, DrawArraysInstanceCountWithoutInstanced) {
+  std::string data = "draw arrays PATCH_LIST 1 2 3";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Extra parameter to draw arrays command", r.Error());
+}
+
+TEST_F(CommandParserTest, DrawArraysMissingTopology) {
+  std::string data = "draw arrays 1 2";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Missing draw arrays topology", r.Error());
+}
+
+TEST_F(CommandParserTest, Compute) {
+  std::string data = "compute 1 2 3";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsCompute());
+
+  auto* cmd = cmds[0]->AsCompute();
+  EXPECT_EQ(1U, cmd->GetX());
+  EXPECT_EQ(2U, cmd->GetY());
+  EXPECT_EQ(3U, cmd->GetZ());
+}
+
+TEST_F(CommandParserTest, ComputeTooShort) {
+  std::string data = "compute 1 2";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Missing integer value for compute Z entry", r.Error());
+}
+
+TEST_F(CommandParserTest, ComputeInvalidX) {
+  std::string data = "compute 1.2 2 3";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Missing integer value for compute X entry", r.Error());
+}
+
+TEST_F(CommandParserTest, ComputeInvalidY) {
+  std::string data = "compute 1 a 3";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Missing integer value for compute Y entry", r.Error());
+}
+
+TEST_F(CommandParserTest, ComputeInvalidZ) {
+  std::string data = "compute 1 2 1.5";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Missing integer value for compute Z entry", r.Error());
+}
+
+TEST_F(CommandParserTest, ComputeExtraCommands) {
+  std::string data = "compute 1 2 3 EXTRA";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Extra parameter to compute command", r.Error());
+}
+
+TEST_F(CommandParserTest, Clear) {
+  std::string data = "clear";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsClear());
+}
+
+TEST_F(CommandParserTest, ClearExtraParams) {
+  std::string data = "clear EXTRA";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Extra parameter to clear command", r.Error());
+}
+
+TEST_F(CommandParserTest, ClearDepth) {
+  std::string data = "clear depth 0.8";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsClearDepth());
+
+  auto* cmd = cmds[0]->AsClearDepth();
+  EXPECT_FLOAT_EQ(0.8, cmd->GetValue());
+}
+
+TEST_F(CommandParserTest, ClearDepthMissingValue) {
+  std::string data = "clear depth";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid conversion to double", r.Error());
+}
+
+TEST_F(CommandParserTest, ClearDepthExtraParameters) {
+  std::string data = "clear depth 0.2 EXTRA";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Extra parameter to clear depth command", r.Error());
+}
+
+TEST_F(CommandParserTest, ClearStencil) {
+  std::string data = "clear stencil 8";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsClearStencil());
+
+  auto* cmd = cmds[0]->AsClearStencil();
+  EXPECT_FLOAT_EQ(8, cmd->GetValue());
+}
+
+TEST_F(CommandParserTest, ClearStencilMissingValue) {
+  std::string data = "clear stencil";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Missing stencil value for clear stencil command", r.Error());
+}
+
+TEST_F(CommandParserTest, ClearStencilExtraParameters) {
+  std::string data = "clear stencil 2 EXTRA";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Extra parameter to clear stencil command", r.Error());
+}
+
+TEST_F(CommandParserTest, ClearStencilNotInteger) {
+  std::string data = "clear stencil 2.3";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid stencil value for clear stencil command", r.Error());
+}
+
+TEST_F(CommandParserTest, ClearColor) {
+  std::string data = "clear color 0.8 0.4 0.2 1.3";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsClearColor());
+
+  auto* cmd = cmds[0]->AsClearColor();
+  EXPECT_FLOAT_EQ(0.8, cmd->GetR());
+  EXPECT_FLOAT_EQ(0.4, cmd->GetG());
+  EXPECT_FLOAT_EQ(0.2, cmd->GetB());
+  EXPECT_FLOAT_EQ(1.3, cmd->GetA());
+}
+
+TEST_F(CommandParserTest, ClearColorMissingParams) {
+  std::string data = "clear color 0.8 0.4 0.2";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid conversion to double", r.Error());
+}
+
+TEST_F(CommandParserTest, ClearColorExtraParams) {
+  std::string data = "clear color 0.8 0.4 0.2 1.3 EXTRA";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Extra parameter to clear color command", r.Error());
+}
+
+TEST_F(CommandParserTest, ClearColorBadR) {
+  std::string data = "clear color a 0.4 0.2 0.4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid conversion to double", r.Error());
+}
+
+TEST_F(CommandParserTest, ClearColorBadG) {
+  std::string data = "clear color 0.2 a 0.2 0.4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid conversion to double", r.Error());
+}
+
+TEST_F(CommandParserTest, ClearColorBadB) {
+  std::string data = "clear color 0.2 0.4 a 0.2";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid conversion to double", r.Error());
+}
+
+TEST_F(CommandParserTest, ClearColorBadA) {
+  std::string data = "clear color 0.2 0.4 0.2 a";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid conversion to double", r.Error());
+}
+
+TEST_F(CommandParserTest, PatchParameterVertices) {
+  std::string data = "patch parameter vertices 9";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsPatchParameterVertices());
+
+  auto* cmd = cmds[0]->AsPatchParameterVertices();
+  EXPECT_FLOAT_EQ(9U, cmd->GetControlPointCount());
+}
+
+TEST_F(CommandParserTest, PatchParameterVerticesMissingParameter) {
+  std::string data = "patch vertices 5";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Missing parameter flag to patch command", r.Error());
+}
+
+TEST_F(CommandParserTest, PatchParameterVerticesMissingVertices) {
+  std::string data = "patch parameter 5";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Missing vertices flag to patch command", r.Error());
+}
+
+TEST_F(CommandParserTest, PatchParameterVerticesMissingParam) {
+  std::string data = "patch parameter vertices";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid count parameter for patch parameter vertices", r.Error());
+}
+
+TEST_F(CommandParserTest, PatchParameterVerticesInvalidParam) {
+  std::string data = "patch parameter vertices invalid";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid count parameter for patch parameter vertices", r.Error());
+}
+
+TEST_F(CommandParserTest, PatchParameterVerticesExtraParam) {
+  std::string data = "patch parameter vertices 3 EXTRA";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Extra parameter for patch parameter vertices command", r.Error());
+}
+
+struct EntryInfo {
+  const char* name;
+  ShaderType type;
+};
+static const EntryInfo kEntryPoints[] = {
+    {"vertex", ShaderType::kVertex},
+    {"fragment", ShaderType::kFragment},
+    {"geometry", ShaderType::kGeometry},
+    {"compute", ShaderType::kCompute},
+    {"tessellation evaluation", ShaderType::kTessellationEvaluation},
+    {"tessellation control", ShaderType::kTessellationControl},
+};
+
+TEST_F(CommandParserTest, EntryPoint) {
+  for (const auto& ep : kEntryPoints) {
+    std::string data = std::string(ep.name) + " entrypoint main";
+
+    CommandParser cp;
+    Result r = cp.Parse(data);
+    ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+    auto& cmds = cp.Commands();
+    ASSERT_EQ(1U, cmds.size());
+    ASSERT_TRUE(cmds[0]->IsEntryPoint());
+
+    auto* cmd = cmds[0]->AsEntryPoint();
+    EXPECT_EQ(ep.type, cmd->GetShaderType());
+    EXPECT_EQ("main", cmd->GetEntryPointName());
+  }
+}
+
+TEST_F(CommandParserTest, EntryPointNameMissing) {
+  for (const auto& ep : kEntryPoints) {
+    std::string data = std::string(ep.name) + " entrypoint";
+
+    CommandParser cp;
+    Result r = cp.Parse(data);
+    ASSERT_FALSE(r.IsSuccess());
+    EXPECT_EQ("Missing entrypoint name", r.Error());
+  }
+}
+
+TEST_F(CommandParserTest, EntryPointEntryPointMissing) {
+  for (const auto& ep : kEntryPoints) {
+    // Skip compute because compute is also a command ....
+    if (std::string(ep.name) == "compute")
+      continue;
+
+    std::string data = std::string(ep.name) + " main";
+
+    CommandParser cp;
+    Result r = cp.Parse(data);
+    ASSERT_FALSE(r.IsSuccess());
+    EXPECT_EQ("Unknown command: " + std::string(ep.name), r.Error());
+  }
+}
+
+TEST_F(CommandParserTest, EntryPointExtraParam) {
+  for (const auto& ep : kEntryPoints) {
+    std::string data = std::string(ep.name) + " entrypoint main EXTRA";
+
+    CommandParser cp;
+    Result r = cp.Parse(data);
+    ASSERT_FALSE(r.IsSuccess());
+    EXPECT_EQ("Extra parameter for entrypoint command", r.Error());
+  }
+}
+
+TEST_F(CommandParserTest, EntryPointInvalidValue) {
+  for (const auto& ep : kEntryPoints) {
+    std::string data = std::string(ep.name) + " entrypoint 123";
+
+    CommandParser cp;
+    Result r = cp.Parse(data);
+    ASSERT_FALSE(r.IsSuccess());
+    EXPECT_EQ("Entrypoint name must be a string", r.Error());
+  }
+}
+
+TEST_F(CommandParserTest, TessellationEntryPointRequiresASuffix) {
+  std::string data = "tessellation entrypoint main";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Tessellation entrypoint must have <evaluation|control> in name",
+            r.Error());
+}
+
+TEST_F(CommandParserTest, TessellationEntryPointRequiresAKnownSuffix) {
+  std::string data = "tessellation unknown entrypoint main";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Tessellation entrypoint must have <evaluation|control> in name",
+            r.Error());
+}
+
+TEST_F(CommandParserTest, InvalidEntryPoint) {
+  std::string data = "unknown entrypoint main";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Unknown command: unknown", r.Error());
+}
+
+using CommandParserProbeTest = testing::TestWithParam<bool>;
+
+TEST_P(CommandParserProbeTest, ProbeRgb) {
+  bool is_relative = GetParam();
+
+  std::string data = (is_relative ? std::string("relative ") : std::string()) +
+                     "probe rgb 25 30 0.2 0.4 0.6";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << data << std::endl << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsProbe());
+
+  auto* cmd = cmds[0]->AsProbe();
+  EXPECT_EQ(is_relative, cmd->IsRelative());
+  EXPECT_FALSE(cmd->IsWholeWindow());
+  EXPECT_FALSE(cmd->IsRGBA());
+
+  EXPECT_FLOAT_EQ(25U, cmd->GetX());
+  EXPECT_FLOAT_EQ(30U, cmd->GetY());
+  EXPECT_FLOAT_EQ(1U, cmd->GetWidth());
+  EXPECT_FLOAT_EQ(1U, cmd->GetHeight());
+
+  EXPECT_FLOAT_EQ(0.2, cmd->GetR());
+  EXPECT_FLOAT_EQ(0.4, cmd->GetG());
+  EXPECT_FLOAT_EQ(0.6, cmd->GetB());
+}
+
+TEST_P(CommandParserProbeTest, ProbeRgba) {
+  bool is_relative = GetParam();
+
+  std::string data = (is_relative ? std::string("relative ") : std::string()) +
+                     "probe rgba 25 30 1 255 9 4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << data << std::endl << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsProbe());
+
+  auto* cmd = cmds[0]->AsProbe();
+  EXPECT_EQ(is_relative, cmd->IsRelative());
+  EXPECT_FALSE(cmd->IsWholeWindow());
+  EXPECT_TRUE(cmd->IsRGBA());
+
+  EXPECT_FLOAT_EQ(25U, cmd->GetX());
+  EXPECT_FLOAT_EQ(30U, cmd->GetY());
+  EXPECT_FLOAT_EQ(1U, cmd->GetWidth());
+  EXPECT_FLOAT_EQ(1U, cmd->GetHeight());
+
+  EXPECT_FLOAT_EQ(1, cmd->GetR());
+  EXPECT_FLOAT_EQ(255, cmd->GetG());
+  EXPECT_FLOAT_EQ(9, cmd->GetB());
+  EXPECT_FLOAT_EQ(4, cmd->GetA());
+}
+
+TEST_P(CommandParserProbeTest, ProbeRect) {
+  bool is_relative = GetParam();
+
+  std::string data = (is_relative ? std::string("relative ") : std::string()) +
+                     "probe rect rgba 25 30 200 400 1 255 9 4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << data << std::endl << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsProbe());
+
+  auto* cmd = cmds[0]->AsProbe();
+  EXPECT_EQ(is_relative, cmd->IsRelative());
+  EXPECT_FALSE(cmd->IsWholeWindow());
+  EXPECT_TRUE(cmd->IsRGBA());
+
+  EXPECT_FLOAT_EQ(25U, cmd->GetX());
+  EXPECT_FLOAT_EQ(30U, cmd->GetY());
+  EXPECT_FLOAT_EQ(200U, cmd->GetWidth());
+  EXPECT_FLOAT_EQ(400U, cmd->GetHeight());
+
+  EXPECT_FLOAT_EQ(1, cmd->GetR());
+  EXPECT_FLOAT_EQ(255, cmd->GetG());
+  EXPECT_FLOAT_EQ(9, cmd->GetB());
+  EXPECT_FLOAT_EQ(4, cmd->GetA());
+}
+
+INSTANTIATE_TEST_CASE_P(ProbeTests,
+                        CommandParserProbeTest,
+                        testing::Values(false, true), );
+
+TEST_F(CommandParserTest, ProbeAllRGB) {
+  std::string data = "probe all rgb 0.2 0.3 0.4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsProbe());
+
+  auto* cmd = cmds[0]->AsProbe();
+  EXPECT_FALSE(cmd->IsRelative());
+  EXPECT_TRUE(cmd->IsWholeWindow());
+  EXPECT_FALSE(cmd->IsRGBA());
+
+  EXPECT_FLOAT_EQ(0.2, cmd->GetR());
+  EXPECT_FLOAT_EQ(0.3, cmd->GetG());
+  EXPECT_FLOAT_EQ(0.4, cmd->GetB());
+}
+
+TEST_F(CommandParserTest, ProbeAllRGBA) {
+  std::string data = "probe all rgba 0.2 0.3 0.4 0.5";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsProbe());
+
+  auto* cmd = cmds[0]->AsProbe();
+  EXPECT_FALSE(cmd->IsRelative());
+  EXPECT_TRUE(cmd->IsWholeWindow());
+  EXPECT_TRUE(cmd->IsRGBA());
+
+  EXPECT_FLOAT_EQ(0.2, cmd->GetR());
+  EXPECT_FLOAT_EQ(0.3, cmd->GetG());
+  EXPECT_FLOAT_EQ(0.4, cmd->GetB());
+  EXPECT_FLOAT_EQ(0.5, cmd->GetA());
+}
+
+TEST_F(CommandParserTest, ProbeCommandRectBrackets) {
+  std::string data = "relative probe rect rgb (0.5, 0.6, 0.3, 0.4) 1 2 3";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsProbe());
+
+  auto* cmd = cmds[0]->AsProbe();
+  EXPECT_TRUE(cmd->IsRelative());
+  EXPECT_FALSE(cmd->IsWholeWindow());
+  EXPECT_FALSE(cmd->IsRGBA());
+
+  EXPECT_FLOAT_EQ(0.5, cmd->GetX());
+  EXPECT_FLOAT_EQ(0.6, cmd->GetY());
+  EXPECT_FLOAT_EQ(0.3, cmd->GetWidth());
+  EXPECT_FLOAT_EQ(0.4, cmd->GetHeight());
+
+  EXPECT_FLOAT_EQ(1.0, cmd->GetR());
+  EXPECT_FLOAT_EQ(2.0, cmd->GetG());
+  EXPECT_FLOAT_EQ(3.0, cmd->GetB());
+}
+
+TEST_F(CommandParserTest, ProbeCommandColorBrackets) {
+  std::string data = "relative probe rect rgb 0.5 0.6 0.3 0.4 (1, 2, 3)";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsProbe());
+
+  auto* cmd = cmds[0]->AsProbe();
+  EXPECT_TRUE(cmd->IsRelative());
+  EXPECT_FALSE(cmd->IsWholeWindow());
+  EXPECT_FALSE(cmd->IsRGBA());
+
+  EXPECT_FLOAT_EQ(0.5, cmd->GetX());
+  EXPECT_FLOAT_EQ(0.6, cmd->GetY());
+  EXPECT_FLOAT_EQ(0.3, cmd->GetWidth());
+  EXPECT_FLOAT_EQ(0.4, cmd->GetHeight());
+
+  EXPECT_FLOAT_EQ(1.0, cmd->GetR());
+  EXPECT_FLOAT_EQ(2.0, cmd->GetG());
+  EXPECT_FLOAT_EQ(3.0, cmd->GetB());
+}
+
+TEST_F(CommandParserTest, ProbeCommandColorOptionalCommas) {
+  std::string data = "relative probe rect rgb 0.5, 0.6, 0.3 0.4 1 2 3";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsProbe());
+
+  auto* cmd = cmds[0]->AsProbe();
+  EXPECT_TRUE(cmd->IsRelative());
+  EXPECT_FALSE(cmd->IsWholeWindow());
+  EXPECT_FALSE(cmd->IsRGBA());
+
+  EXPECT_FLOAT_EQ(0.5, cmd->GetX());
+  EXPECT_FLOAT_EQ(0.6, cmd->GetY());
+  EXPECT_FLOAT_EQ(0.3, cmd->GetWidth());
+  EXPECT_FLOAT_EQ(0.4, cmd->GetHeight());
+
+  EXPECT_FLOAT_EQ(1.0, cmd->GetR());
+  EXPECT_FLOAT_EQ(2.0, cmd->GetG());
+  EXPECT_FLOAT_EQ(3.0, cmd->GetB());
+}
+
+TEST_F(CommandParserTest, ProbeErrors) {
+  struct {
+    const char* str;
+    const char* err;
+  } probes[] = {
+      {"probe rgba ab 30 0.2 0.3 0.4 0.5", "Invalid conversion to double"},
+      {"relative probe rgba ab 30 0.2 0.3 0.4 0.5",
+       "Invalid conversion to double"},
+      {"probe rect rgba ab 30 2 3 0.2 0.3 0.4 0.5",
+       "Invalid conversion to double"},
+      {"relative probe rect rgba ab 30 2 3 0.2 0.3 0.4 0.5",
+       "Invalid conversion to double"},
+
+      {"probe rgba 30 ab 0.2 0.3 0.4 0.5", "Invalid conversion to double"},
+      {"relative probe rgba 30 ab 0.2 0.3 0.4 0.5",
+       "Invalid conversion to double"},
+      {"probe rect rgba 30 ab 2 3 0.2 0.3 0.4 0.5",
+       "Invalid conversion to double"},
+      {"relative probe rect rgba 30 ab 2 3 0.2 0.3 0.4 0.5",
+       "Invalid conversion to double"},
+
+      {"probe rect rgba 30 40 ab 3 0.2 0.3 0.4 0.5",
+       "Invalid conversion to double"},
+      {"relative probe rect rgba 30 40 ab 3 0.2 0.3 0.4 0.5",
+       "Invalid conversion to double"},
+
+      {"probe rect rgba 30 40 3 ab 0.2 0.3 0.4 0.5",
+       "Invalid conversion to double"},
+      {"relative probe rect rgba 30 40 3 ab 0.2 0.3 0.4 0.5",
+       "Invalid conversion to double"},
+
+      {"probe rgba 10 30 ab 0.3 0.4 0.5", "Invalid conversion to double"},
+      {"relative probe rgba 10 30 ab 0.3 0.4 0.5",
+       "Invalid conversion to double"},
+      {"probe rect rgba 10 30 2 3 ab 0.3 0.4 0.5",
+       "Invalid conversion to double"},
+      {"relative probe rect rgba 10 30 2 3 ab 0.3 0.4 0.5",
+       "Invalid conversion to double"},
+
+      {"probe rgba 10 30 0.2 ab 0.4 0.5", "Invalid conversion to double"},
+      {"relative probe rgba 10 30 0.2 ab 0.4 0.5",
+       "Invalid conversion to double"},
+      {"probe rect rgba 10 30 2 3 0.2 ab 0.4 0.5",
+       "Invalid conversion to double"},
+      {"relative probe rect rgba 10 30 2 3 0.2 ab 0.4 0.5",
+       "Invalid conversion to double"},
+
+      {"probe rgba 10 30 0.2 0.3 ab 0.5", "Invalid conversion to double"},
+      {"relative probe rgba 10 30 0.2 0.3 ab 0.5",
+       "Invalid conversion to double"},
+      {"probe rect rgba 10 30 2 3 0.2 0.3 ab 0.5",
+       "Invalid conversion to double"},
+      {"relative probe rect rgba 10 30 2 3 0.2 0.3 ab 0.5",
+       "Invalid conversion to double"},
+
+      {"probe rgba 10 30 0.2 0.3 0.4 ab", "Invalid conversion to double"},
+      {"relative probe rgba 10 30 0.2 0.3 0.4 ab",
+       "Invalid conversion to double"},
+      {"probe rect rgba 10 30 2 3 0.2 0.3 0.4 ab",
+       "Invalid conversion to double"},
+      {"relative probe rect rgba 10 30 2 3 0.2 0.3 0.4 ab",
+       "Invalid conversion to double"},
+
+      {"probe all rgb ab 2 3", "Invalid conversion to double"},
+      {"probe all rgb 2 ab 4", "Invalid conversion to double"},
+      {"probe all rgb 2 3 ab", "Invalid conversion to double"},
+
+      {"probe all rgba ab 2 3 4", "Invalid conversion to double"},
+      {"probe all rgba 2 ab 4 5", "Invalid conversion to double"},
+      {"probe all rgba 2 3 ab 5", "Invalid conversion to double"},
+      {"probe all rgba 2 3 4 ab", "Invalid conversion to double"},
+
+      {"probe rgb 10 30 0.2 0.3 0.4 extra", "Extra parameter to probe command"},
+      {"probe rgba 10 30 0.2 0.3 0.4 0.4 extra",
+       "Extra parameter to probe command"},
+      {"relative probe rgb 10 30 0.2 0.3 0.4 extra",
+       "Extra parameter to probe command"},
+      {"relative probe rgba 10 30 0.2 0.3 0.4 0.4 extra",
+       "Extra parameter to probe command"},
+      {"probe rect rgb 10 30 40 50 0.2 0.3 0.4 extra",
+       "Extra parameter to probe command"},
+      {"probe rect rgba 10 30 40 50 0.2 0.3 0.4 0.4 extra",
+       "Extra parameter to probe command"},
+      {"relative probe rect rgb 10 30 40 50 0.2 0.3 0.4 extra",
+       "Extra parameter to probe command"},
+      {"relative probe rect rgba 10 30 40 50 0.2 0.3 0.4 0.4 extra",
+       "Extra parameter to probe command"},
+      {"probe all rgb 2 3 4 EXTRA", "Extra parameter to probe command"},
+      {"probe all rgba 2 3 4 5 EXTRA", "Extra parameter to probe command"},
+
+      {"relative probe rect rgb 0.5 0.6 0.3 0.4 1 2 3)",
+       "Missing open bracket for probe command"},
+      {"relative probe rect rgb (0.5 0.6 0.3 0.4 1 2 3",
+       "Missing close bracket for probe command"},
+      {"relative probe rect rgb 0.5 0.6 0.3 0.4) 1 2 3",
+       "Missing open bracket for probe command"},
+      {"relative probe rect rgb 0.5 0.6 0.3 0.4 (1, 2, 3",
+       "Missing close bracket for probe command"},
+      {"relative probe rect rgb (0.5, 0.6, 0.3, 0.4, 1, 2, 3)",
+       "Missing close bracket for probe command"},
+  };
+
+  for (const auto& probe : probes) {
+    CommandParser cp;
+    Result r = cp.Parse(probe.str);
+    EXPECT_FALSE(r.IsSuccess()) << probe.str;
+    EXPECT_EQ(probe.err, r.Error()) << probe.str;
+  }
+}
+
+TEST_F(CommandParserTest, RelativeWithoutProbe) {
+  std::string data = "relative unknown";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("relative must be used with probe", r.Error());
+}
+
+TEST_F(CommandParserTest, ProbeWithInvalidRGBA) {
+  std::string data = "probe 1";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid token in probe command", r.Error());
+}
+
+TEST_F(CommandParserTest, ProbeWithRectAndInvalidRGB) {
+  std::string data = "probe rect 1";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid token in probe command", r.Error());
+}
+
+TEST_F(CommandParserTest, ProbeWithRectMissingFormat) {
+  std::string data = "probe rect unknown";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid format specified to probe command", r.Error());
+}
+
+TEST_F(CommandParserTest, ProbeAllMissingFormat) {
+  std::string data = "probe all unknown";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid format specified to probe command", r.Error());
+}
+
+TEST_F(CommandParserTest, ProbeAlWithInvalidRGB) {
+  std::string data = "probe all unknown";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid format specified to probe command", r.Error());
+}
+
+struct TopologyTestData {
+  const char* name;
+  Topology value;
+};
+using CommandDataPipelineTopologyParser =
+    testing::TestWithParam<TopologyTestData>;
+
+TEST_P(CommandDataPipelineTopologyParser, Topology) {
+  const auto& test_data = GetParam();
+
+  std::string data = "topology " + std::string(test_data.name);
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(test_data.value, cp.PipelineDataForTesting()->GetTopology());
+}
+
+INSTANTIATE_TEST_CASE_P(
+    TopologyTests,
+    CommandDataPipelineTopologyParser,
+    testing::Values(
+        TopologyTestData{"VK_PRIMITIVE_TOPOLOGY_PATCH_LIST",
+                         Topology::kPatchList},
+        TopologyTestData{"VK_PRIMITIVE_TOPOLOGY_POINT_LIST",
+                         Topology::kPointList},
+        TopologyTestData{"VK_PRIMITIVE_TOPOLOGY_LINE_LIST",
+                         Topology::kLineList},
+        TopologyTestData{"VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY",
+                         Topology::kLineListWithAdjacency},
+        TopologyTestData{"VK_PRIMITIVE_TOPOLOGY_LINE_STRIP",
+                         Topology::kLineStrip},
+        TopologyTestData{"VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY",
+                         Topology::kLineStripWithAdjacency},
+        TopologyTestData{"VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN",
+                         Topology::kTriangleFan},
+        TopologyTestData{"VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST",
+                         Topology::kTriangleList},
+        TopologyTestData{"VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY",
+                         Topology::kTriangleListWithAdjacency},
+        TopologyTestData{"VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP",
+                         Topology::kTriangleStrip},
+        TopologyTestData{"VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY",
+                         Topology::kTriangleStripWithAdjacency}), );
+
+struct PipelineDataInvalidTest {
+  const char* name;
+  const char* arg;
+};
+using CommandDataPipelineDataInvalidParser =
+    testing::TestWithParam<PipelineDataInvalidTest>;
+
+TEST_P(CommandDataPipelineDataInvalidParser, InvalidPipelineParamValue) {
+  const auto& test_data = GetParam();
+
+  std::string data = std::string(test_data.name) + " 123";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Invalid value for ") + test_data.name + " command",
+            r.Error());
+}
+
+TEST_P(CommandDataPipelineDataInvalidParser, MissingTopologyValue) {
+  const auto& test_data = GetParam();
+
+  std::string data = test_data.name;
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Missing value for ") + test_data.name + " command",
+            r.Error());
+}
+
+TEST_P(CommandDataPipelineDataInvalidParser, UnknownPipelineParamValue) {
+  const auto& test_data = GetParam();
+
+  std::string data = std::string(test_data.name) + " UNKNOWN";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Unknown value for ") + test_data.name + " command",
+            r.Error());
+}
+
+TEST_P(CommandDataPipelineDataInvalidParser, ExtraPipelineParamValue) {
+  const auto& test_data = GetParam();
+
+  // CullMode consumes all parameters, so skip this test.
+  if (std::string(test_data.name) == "cullMode")
+    return;
+
+  std::string data =
+      std::string(test_data.name) + " " + test_data.arg + " EXTRA";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Extra parameter for ") + test_data.name + " command",
+            r.Error());
+}
+
+INSTANTIATE_TEST_CASE_P(
+    PipelineDataInvalidTests,
+    CommandDataPipelineDataInvalidParser,
+    testing::Values(
+        PipelineDataInvalidTest{"topology", "VK_PRIMITIVE_TOPOLOGY_POINT_LIST"},
+        PipelineDataInvalidTest{"polygonMode", "VK_POLYGON_MODE_POINT"},
+        PipelineDataInvalidTest{"cullMode", "VK_CULL_MODE_BACK_BIT"},
+        PipelineDataInvalidTest{"frontFace", "VK_FRONT_FACE_COUNTER_CLOCKWISE"},
+        PipelineDataInvalidTest{"logicOp", "VK_LOGIC_OP_NO_OP"}), );
+
+TEST_F(CommandParserTest, BooleanTrue) {
+  struct {
+    const char* name;
+  } data[] = {{"TRUE"}, {"true"}, {"TRuE"}};
+
+  for (const auto& d : data) {
+    CommandParser cp;
+
+    bool value = false;
+    Result r = cp.ParseBooleanForTesting(d.name, &value);
+    EXPECT_TRUE(r.IsSuccess()) << r.Error();
+    EXPECT_TRUE(value);
+  }
+}
+
+TEST_F(CommandParserTest, BooleanFalse) {
+  struct {
+    const char* name;
+  } data[] = {{"FALSE"}, {"false"}, {"FAlsE"}};
+
+  for (const auto& d : data) {
+    CommandParser cp;
+
+    bool value = true;
+    Result r = cp.ParseBooleanForTesting(d.name, &value);
+    EXPECT_TRUE(r.IsSuccess()) << d.name << " " << r.Error();
+    EXPECT_FALSE(value);
+  }
+}
+
+TEST_F(CommandParserTest, BooleanInvalid) {
+  struct {
+    const char* name;
+  } data[] = {{""}, {"Invalid"}};
+
+  for (const auto& d : data) {
+    CommandParser cp;
+
+    bool value = true;
+    Result r = cp.ParseBooleanForTesting(d.name, &value);
+    ASSERT_FALSE(r.IsSuccess()) << d.name;
+    EXPECT_EQ("Invalid value passed as a boolean string", r.Error());
+  }
+}
+
+TEST_F(CommandParserTest, PrimitiveRestartEnable) {
+  std::string data = "primitiveRestartEnable true";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_TRUE(cp.PipelineDataForTesting()->GetEnablePrimitiveRestart());
+}
+
+TEST_F(CommandParserTest, PrimitiveRestartDisable) {
+  std::string data = "primitiveRestartEnable false";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_FALSE(cp.PipelineDataForTesting()->GetEnablePrimitiveRestart());
+}
+
+TEST_F(CommandParserTest, DepthClampEnable) {
+  std::string data = "depthClampEnable true";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_TRUE(cp.PipelineDataForTesting()->GetEnableDepthClamp());
+}
+
+TEST_F(CommandParserTest, DepthClampDisable) {
+  std::string data = "depthClampEnable false";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_FALSE(cp.PipelineDataForTesting()->GetEnableDepthClamp());
+}
+
+TEST_F(CommandParserTest, RasterizerDiscardEnable) {
+  std::string data = "rasterizerDiscardEnable true";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_TRUE(cp.PipelineDataForTesting()->GetEnableRasterizerDiscard());
+}
+
+TEST_F(CommandParserTest, RasterizerDiscardDisable) {
+  std::string data = "rasterizerDiscardEnable false";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_FALSE(cp.PipelineDataForTesting()->GetEnableRasterizerDiscard());
+}
+
+TEST_F(CommandParserTest, DepthBiasEnable) {
+  std::string data = "depthBiasEnable true";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_TRUE(cp.PipelineDataForTesting()->GetEnableDepthBias());
+}
+
+TEST_F(CommandParserTest, DepthBiasDisable) {
+  std::string data = "depthBiasEnable false";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_FALSE(cp.PipelineDataForTesting()->GetEnableDepthBias());
+}
+
+TEST_F(CommandParserTest, LogicOpEnable) {
+  std::string data = "logicOpEnable true";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_TRUE(cp.PipelineDataForTesting()->GetEnableLogicOp());
+}
+
+TEST_F(CommandParserTest, LogicOpDisable) {
+  std::string data = "logicOpEnable false";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_FALSE(cp.PipelineDataForTesting()->GetEnableLogicOp());
+}
+
+TEST_F(CommandParserTest, BlendEnable) {
+  std::string data = "blendEnable true";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_TRUE(cp.PipelineDataForTesting()->GetEnableBlend());
+}
+
+TEST_F(CommandParserTest, BlendDisable) {
+  std::string data = "blendEnable false";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_FALSE(cp.PipelineDataForTesting()->GetEnableBlend());
+}
+
+TEST_F(CommandParserTest, DepthTestEnable) {
+  std::string data = "depthTestEnable true";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_TRUE(cp.PipelineDataForTesting()->GetEnableDepthTest());
+}
+
+TEST_F(CommandParserTest, DepthTestDisable) {
+  std::string data = "depthTestEnable false";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_FALSE(cp.PipelineDataForTesting()->GetEnableDepthTest());
+}
+
+TEST_F(CommandParserTest, DepthWriteEnable) {
+  std::string data = "depthWriteEnable true";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_TRUE(cp.PipelineDataForTesting()->GetEnableDepthWrite());
+}
+
+TEST_F(CommandParserTest, DepthWriteDisable) {
+  std::string data = "depthWriteEnable false";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_FALSE(cp.PipelineDataForTesting()->GetEnableDepthWrite());
+}
+
+TEST_F(CommandParserTest, DepthBoundsTestEnable) {
+  std::string data = "depthBoundsTestEnable true";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_TRUE(cp.PipelineDataForTesting()->GetEnableDepthBoundsTest());
+}
+
+TEST_F(CommandParserTest, DepthBoundsTestDisable) {
+  std::string data = "depthBoundsTestEnable false";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_FALSE(cp.PipelineDataForTesting()->GetEnableDepthBoundsTest());
+}
+
+TEST_F(CommandParserTest, StencilTestEnable) {
+  std::string data = "stencilTestEnable true";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_TRUE(cp.PipelineDataForTesting()->GetEnableStencilTest());
+}
+
+TEST_F(CommandParserTest, StencilTestDisable) {
+  std::string data = "stencilTestEnable false";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_FALSE(cp.PipelineDataForTesting()->GetEnableStencilTest());
+}
+
+struct BooleanTest {
+  const char* name;
+};
+using CommandParserBooleanTests = testing::TestWithParam<BooleanTest>;
+
+TEST_P(CommandParserBooleanTests, MissingParam) {
+  const auto& test_data = GetParam();
+
+  std::string data = std::string(test_data.name);
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Missing value for ") + test_data.name + " command",
+            r.Error());
+}
+
+TEST_P(CommandParserBooleanTests, IllegalParam) {
+  const auto& test_data = GetParam();
+
+  std::string data = std::string(test_data.name) + " 123";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Invalid value for ") + test_data.name + " command",
+            r.Error());
+}
+
+TEST_P(CommandParserBooleanTests, ExtraParam) {
+  const auto& test_data = GetParam();
+
+  std::string data = std::string(test_data.name) + " true EXTRA";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Extra parameter for ") + test_data.name + " command",
+            r.Error());
+}
+
+INSTANTIATE_TEST_CASE_P(BooleanTests,
+                        CommandParserBooleanTests,
+                        testing::Values(BooleanTest{"primitiveRestartEnable"},
+                                        BooleanTest{"depthClampEnable"},
+                                        BooleanTest{"rasterizerDiscardEnable"},
+                                        BooleanTest{"depthBiasEnable"},
+                                        BooleanTest{"logicOpEnable"},
+                                        BooleanTest{"blendEnable"},
+                                        BooleanTest{"depthTestEnable"},
+                                        BooleanTest{"depthWriteEnable"},
+                                        BooleanTest{"depthBoundsTestEnable"},
+                                        BooleanTest{"stencilTestEnable"}), );
+
+struct PolygonModeTestData {
+  const char* name;
+  PolygonMode value;
+};
+using CommandDataPipelinePolygonModeParser =
+    testing::TestWithParam<PolygonModeTestData>;
+
+TEST_P(CommandDataPipelinePolygonModeParser, PolygonMode) {
+  const auto& test_data = GetParam();
+
+  std::string data = "polygonMode " + std::string(test_data.name);
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(test_data.value, cp.PipelineDataForTesting()->GetPolygonMode());
+}
+
+INSTANTIATE_TEST_CASE_P(
+    PolygonModeTests,
+    CommandDataPipelinePolygonModeParser,
+    testing::Values(
+        PolygonModeTestData{"VK_POLYGON_MODE_FILL", PolygonMode::kFill},
+        PolygonModeTestData{"VK_POLYGON_MODE_LINE", PolygonMode::kLine},
+        PolygonModeTestData{"VK_POLYGON_MODE_POINT", PolygonMode::kPoint}), );
+
+struct CullModeTestData {
+  const char* name;
+  CullMode value;
+};
+using CommandDataPipelineCullModeParser =
+    testing::TestWithParam<CullModeTestData>;
+
+TEST_P(CommandDataPipelineCullModeParser, CullMode) {
+  const auto& test_data = GetParam();
+
+  std::string data = "cullMode " + std::string(test_data.name);
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(test_data.value, cp.PipelineDataForTesting()->GetCullMode());
+}
+
+INSTANTIATE_TEST_CASE_P(
+    CullModeTests,
+    CommandDataPipelineCullModeParser,
+    testing::Values(
+        CullModeTestData{"VK_CULL_MODE_NONE", CullMode::kNone},
+        CullModeTestData{"VK_CULL_MODE_FRONT_BIT", CullMode::kFront},
+        CullModeTestData{"VK_CULL_MODE_BACK_BIT", CullMode::kBack},
+        CullModeTestData{"VK_CULL_MODE_BACK_BIT | VK_CULL_MODE_FRONT_BIT",
+                         CullMode::kFrontAndBack},
+        CullModeTestData{"VK_CULL_MODE_FRONT_BIT | VK_CULL_MODE_BACK_BIT",
+                         CullMode::kFrontAndBack},
+        CullModeTestData{"VK_CULL_MODE_FRONT_AND_BACK",
+                         CullMode::kFrontAndBack}), );
+
+struct FrontFaceTestData {
+  const char* name;
+  FrontFace value;
+};
+using CommandDataPipelineFrontFaceParser =
+    testing::TestWithParam<FrontFaceTestData>;
+
+TEST_P(CommandDataPipelineFrontFaceParser, FrontFace) {
+  const auto& test_data = GetParam();
+
+  std::string data = "frontFace " + std::string(test_data.name);
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(test_data.value, cp.PipelineDataForTesting()->GetFrontFace());
+}
+
+INSTANTIATE_TEST_CASE_P(
+    FrontFaceTests,
+    CommandDataPipelineFrontFaceParser,
+    testing::Values(FrontFaceTestData{"VK_FRONT_FACE_COUNTER_CLOCKWISE",
+                                      FrontFace::kCounterClockwise},
+                    FrontFaceTestData{"VK_FRONT_FACE_CLOCKWISE",
+                                      FrontFace::kClockwise}), );
+
+struct LogicOpTestData {
+  const char* name;
+  LogicOp value;
+};
+using CommandDataPipelineLogicOpParser =
+    testing::TestWithParam<LogicOpTestData>;
+
+TEST_P(CommandDataPipelineLogicOpParser, LogicOp) {
+  const auto& test_data = GetParam();
+
+  std::string data = "logicOp " + std::string(test_data.name);
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(test_data.value, cp.PipelineDataForTesting()->GetLogicOp());
+}
+
+INSTANTIATE_TEST_CASE_P(
+    LogicOpTests,
+    CommandDataPipelineLogicOpParser,
+    testing::Values(
+        LogicOpTestData{"VK_LOGIC_OP_CLEAR", LogicOp::kClear},
+        LogicOpTestData{"VK_LOGIC_OP_AND", LogicOp::kAnd},
+        LogicOpTestData{"VK_LOGIC_OP_AND_REVERSE", LogicOp::kAndReverse},
+        LogicOpTestData{"VK_LOGIC_OP_COPY", LogicOp::kCopy},
+        LogicOpTestData{"VK_LOGIC_OP_AND_INVERTED", LogicOp::kAndInverted},
+        LogicOpTestData{"VK_LOGIC_OP_NO_OP", LogicOp::kNoOp},
+        LogicOpTestData{"VK_LOGIC_OP_XOR", LogicOp::kXor},
+        LogicOpTestData{"VK_LOGIC_OP_OR", LogicOp::kOr},
+        LogicOpTestData{"VK_LOGIC_OP_NOR", LogicOp::kNor},
+        LogicOpTestData{"VK_LOGIC_OP_EQUIVALENT", LogicOp::kEquivalent},
+        LogicOpTestData{"VK_LOGIC_OP_INVERT", LogicOp::kInvert},
+        LogicOpTestData{"VK_LOGIC_OP_OR_REVERSE", LogicOp::kOrReverse},
+        LogicOpTestData{"VK_LOGIC_OP_COPY_INVERTED", LogicOp::kCopyInverted},
+        LogicOpTestData{"VK_LOGIC_OP_OR_INVERTED", LogicOp::kOrInverted},
+        LogicOpTestData{"VK_LOGIC_OP_NAND", LogicOp::kNand},
+        LogicOpTestData{"VK_LOGIC_OP_SET", LogicOp::kSet}), );
+
+TEST_F(CommandParserTest, DepthBiasConstantFactor) {
+  std::string data = "depthBiasConstantFactor 3.4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_FLOAT_EQ(3.4,
+                  cp.PipelineDataForTesting()->GetDepthBiasConstantFactor());
+}
+
+TEST_F(CommandParserTest, DepthBiasClamp) {
+  std::string data = "depthBiasClamp 3.4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_FLOAT_EQ(3.4, cp.PipelineDataForTesting()->GetDepthBiasClamp());
+}
+
+TEST_F(CommandParserTest, DepthBiasSlopeFactor) {
+  std::string data = "depthBiasSlopeFactor 3.4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_FLOAT_EQ(3.4, cp.PipelineDataForTesting()->GetDepthBiasSlopeFactor());
+}
+
+TEST_F(CommandParserTest, LineWidth) {
+  std::string data = "lineWidth 3.4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_FLOAT_EQ(3.4, cp.PipelineDataForTesting()->GetLineWidth());
+}
+
+TEST_F(CommandParserTest, MinDepthBounds) {
+  std::string data = "minDepthBounds 3.4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_FLOAT_EQ(3.4, cp.PipelineDataForTesting()->GetMinDepthBounds());
+}
+
+TEST_F(CommandParserTest, MaxDepthBounds) {
+  std::string data = "maxDepthBounds 3.4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_FLOAT_EQ(3.4, cp.PipelineDataForTesting()->GetMaxDepthBounds());
+}
+
+struct FloatTest {
+  const char* name;
+};
+using CommandParserFloatTests = testing::TestWithParam<FloatTest>;
+
+TEST_P(CommandParserFloatTests, MissingParam) {
+  const auto& test_data = GetParam();
+
+  std::string data = std::string(test_data.name);
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Missing value for ") + test_data.name + " command",
+            r.Error());
+}
+
+TEST_P(CommandParserFloatTests, IllegalParam) {
+  const auto& test_data = GetParam();
+
+  std::string data = std::string(test_data.name) + " INVALID";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid conversion to double", r.Error());
+}
+
+TEST_P(CommandParserFloatTests, ExtraParam) {
+  const auto& test_data = GetParam();
+
+  std::string data = std::string(test_data.name) + " 3.2 EXTRA";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Extra parameter for ") + test_data.name + " command",
+            r.Error());
+}
+
+INSTANTIATE_TEST_CASE_P(FloatTests,
+                        CommandParserFloatTests,
+                        testing::Values(FloatTest{"depthBiasConstantFactor"},
+                                        FloatTest{"lineWidth"},
+                                        FloatTest{"depthBiasClamp"},
+                                        FloatTest{"depthBiasSlopeFactor"},
+                                        FloatTest{"minDepthBounds"},
+                                        FloatTest{"maxDepthBounds"}), );
+
+TEST_F(CommandParserTest, SrcColorBlendFactor) {
+  std::string data = "srcColorBlendFactor VK_BLEND_FACTOR_DST_COLOR";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(BlendFactor::kDstColor,
+            cp.PipelineDataForTesting()->GetSrcColorBlendFactor());
+}
+
+TEST_F(CommandParserTest, DstColorBlendFactor) {
+  std::string data = "dstColorBlendFactor VK_BLEND_FACTOR_DST_COLOR";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(BlendFactor::kDstColor,
+            cp.PipelineDataForTesting()->GetDstColorBlendFactor());
+}
+
+TEST_F(CommandParserTest, SrcAlphaBlendFactor) {
+  std::string data = "srcAlphaBlendFactor VK_BLEND_FACTOR_DST_COLOR";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(BlendFactor::kDstColor,
+            cp.PipelineDataForTesting()->GetSrcAlphaBlendFactor());
+}
+
+TEST_F(CommandParserTest, DstAlphaBlendFactor) {
+  std::string data = "dstAlphaBlendFactor VK_BLEND_FACTOR_DST_COLOR";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(BlendFactor::kDstColor,
+            cp.PipelineDataForTesting()->GetDstAlphaBlendFactor());
+}
+
+struct BlendFactorData {
+  const char* name;
+  BlendFactor type;
+};
+using CommandParserBlendFactorParsing = testing::TestWithParam<BlendFactorData>;
+
+TEST_P(CommandParserBlendFactorParsing, Parse) {
+  const auto& test_data = GetParam();
+
+  CommandParser cp;
+  BlendFactor factor = BlendFactor::kZero;
+  Result r = cp.ParseBlendFactorNameForTesting(test_data.name, &factor);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(test_data.type, factor);
+}
+
+INSTANTIATE_TEST_CASE_P(
+    BlendFactorParsingTests,
+    CommandParserBlendFactorParsing,
+    testing::Values(
+        BlendFactorData{"VK_BLEND_FACTOR_ZERO", BlendFactor::kZero},
+        BlendFactorData{"VK_BLEND_FACTOR_ONE", BlendFactor::kOne},
+        BlendFactorData{"VK_BLEND_FACTOR_SRC_COLOR", BlendFactor::kSrcColor},
+        BlendFactorData{"VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR",
+                        BlendFactor::kOneMinusSrcColor},
+        BlendFactorData{"VK_BLEND_FACTOR_DST_COLOR", BlendFactor::kDstColor},
+        BlendFactorData{"VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR",
+                        BlendFactor::kOneMinusDstColor},
+        BlendFactorData{"VK_BLEND_FACTOR_SRC_ALPHA", BlendFactor::kSrcAlpha},
+        BlendFactorData{"VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA",
+                        BlendFactor::kOneMinusSrcAlpha},
+        BlendFactorData{"VK_BLEND_FACTOR_DST_ALPHA", BlendFactor::kDstAlpha},
+        BlendFactorData{"VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA",
+                        BlendFactor::kOneMinusDstAlpha},
+        BlendFactorData{"VK_BLEND_FACTOR_CONSTANT_COLOR",
+                        BlendFactor::kConstantColor},
+        BlendFactorData{"VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_COLOR",
+                        BlendFactor::kOneMinusConstantColor},
+        BlendFactorData{"VK_BLEND_FACTOR_CONSTANT_ALPHA",
+                        BlendFactor::kConstantAlpha},
+        BlendFactorData{"VK_BLEND_FACTOR_ONE_MINUS_CONSTANT_ALPHA",
+                        BlendFactor::kOneMinusConstantAlpha},
+        BlendFactorData{"VK_BLEND_FACTOR_SRC_ALPHA_SATURATE",
+                        BlendFactor::kSrcAlphaSaturate},
+        BlendFactorData{"VK_BLEND_FACTOR_SRC1_COLOR", BlendFactor::kSrc1Color},
+        BlendFactorData{"VK_BLEND_FACTOR_ONE_MINUS_SRC1_COLOR",
+                        BlendFactor::kOneMinusSrc1Color},
+        BlendFactorData{"VK_BLEND_FACTOR_SRC1_ALPHA", BlendFactor::kSrc1Alpha},
+        BlendFactorData{"VK_BLEND_FACTOR_ONE_MINUS_SRC1_ALPHA",
+                        BlendFactor::kOneMinusSrc1Alpha}), );
+
+TEST_F(CommandParserTest, BlendFactorParsingInvalid) {
+  CommandParser cp;
+  BlendFactor factor = BlendFactor::kZero;
+  Result r = cp.ParseBlendFactorNameForTesting("INVALID", &factor);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Unknown BlendFactor provided: INVALID", r.Error());
+}
+
+struct BlendFactorTest {
+  const char* name;
+};
+using CommandParserBlendFactorTests = testing::TestWithParam<BlendFactorTest>;
+
+TEST_P(CommandParserBlendFactorTests, MissingParam) {
+  const auto& test_data = GetParam();
+
+  std::string data = std::string(test_data.name);
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Missing parameter for ") + test_data.name + " command",
+            r.Error());
+}
+
+TEST_P(CommandParserBlendFactorTests, IllegalParam) {
+  const auto& test_data = GetParam();
+
+  std::string data = std::string(test_data.name) + " 1.23";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Invalid parameter for ") + test_data.name + " command",
+            r.Error());
+}
+
+TEST_P(CommandParserBlendFactorTests, ExtraParam) {
+  const auto& test_data = GetParam();
+
+  std::string data = std::string(test_data.name) + " VK_BLEND_FACTOR_ONE EXTRA";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Extra parameter for ") + test_data.name + " command",
+            r.Error());
+}
+
+INSTANTIATE_TEST_CASE_P(BlendFactorTests,
+                        CommandParserBlendFactorTests,
+                        testing::Values(BlendFactorTest{"srcColorBlendFactor"},
+                                        BlendFactorTest{"dstColorBlendFactor"},
+                                        BlendFactorTest{"srcAlphaBlendFactor"},
+                                        BlendFactorTest{
+                                            "dstAlphaBlendFactor"}), );
+
+TEST_F(CommandParserTest, ColorBlendOp) {
+  std::string data = "colorBlendOp VK_BLEND_OP_XOR_EXT";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(BlendOp::kXor, cp.PipelineDataForTesting()->GetColorBlendOp());
+}
+
+TEST_F(CommandParserTest, AlphaBlendOp) {
+  std::string data = "alphaBlendOp VK_BLEND_OP_XOR_EXT";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(BlendOp::kXor, cp.PipelineDataForTesting()->GetAlphaBlendOp());
+}
+
+struct BlendOpData {
+  const char* name;
+  BlendOp type;
+};
+using CommandParserBlendOpParsing = testing::TestWithParam<BlendOpData>;
+
+TEST_P(CommandParserBlendOpParsing, Parse) {
+  const auto& test_data = GetParam();
+
+  CommandParser cp;
+  BlendOp op = BlendOp::kAdd;
+  Result r = cp.ParseBlendOpNameForTesting(test_data.name, &op);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(test_data.type, op);
+}
+
+INSTANTIATE_TEST_CASE_P(
+    BlendOpParsingTests1,
+    CommandParserBlendOpParsing,
+    testing::Values(
+        BlendOpData{"VK_BLEND_OP_ADD", BlendOp::kAdd},
+        BlendOpData{"VK_BLEND_OP_SUBTRACT", BlendOp::kSubtract},
+        BlendOpData{"VK_BLEND_OP_REVERSE_SUBTRACT", BlendOp::kReverseSubtract},
+        BlendOpData{"VK_BLEND_OP_MIN", BlendOp::kMin},
+        BlendOpData{"VK_BLEND_OP_MAX", BlendOp::kMax},
+        BlendOpData{"VK_BLEND_OP_ZERO_EXT", BlendOp::kZero},
+        BlendOpData{"VK_BLEND_OP_SRC_EXT", BlendOp::kSrc},
+        BlendOpData{"VK_BLEND_OP_DST_EXT", BlendOp::kDst},
+        BlendOpData{"VK_BLEND_OP_SRC_OVER_EXT", BlendOp::kSrcOver},
+        BlendOpData{"VK_BLEND_OP_DST_OVER_EXT", BlendOp::kDstOver},
+        BlendOpData{"VK_BLEND_OP_SRC_IN_EXT", BlendOp::kSrcIn},
+        BlendOpData{"VK_BLEND_OP_DST_IN_EXT", BlendOp::kDstIn},
+        BlendOpData{"VK_BLEND_OP_SRC_OUT_EXT", BlendOp::kSrcOut},
+        BlendOpData{"VK_BLEND_OP_DST_OUT_EXT", BlendOp::kDstOut},
+        BlendOpData{"VK_BLEND_OP_SRC_ATOP_EXT", BlendOp::kSrcAtop},
+        BlendOpData{"VK_BLEND_OP_DST_ATOP_EXT", BlendOp::kDstAtop},
+        BlendOpData{"VK_BLEND_OP_XOR_EXT", BlendOp::kXor},
+        BlendOpData{"VK_BLEND_OP_MULTIPLY_EXT", BlendOp::kMultiply},
+        BlendOpData{"VK_BLEND_OP_SCREEN_EXT", BlendOp::kScreen},
+        BlendOpData{"VK_BLEND_OP_OVERLAY_EXT", BlendOp::kOverlay},
+        BlendOpData{"VK_BLEND_OP_DARKEN_EXT", BlendOp::kDarken},
+        BlendOpData{"VK_BLEND_OP_LIGHTEN_EXT", BlendOp::kLighten},
+        BlendOpData{"VK_BLEND_OP_COLORDODGE_EXT", BlendOp::kColorDodge},
+        BlendOpData{"VK_BLEND_OP_COLORBURN_EXT", BlendOp::kColorBurn},
+        BlendOpData{"VK_BLEND_OP_HARDLIGHT_EXT", BlendOp::kHardLight},
+        BlendOpData{"VK_BLEND_OP_SOFTLIGHT_EXT", BlendOp::kSoftLight},
+        BlendOpData{"VK_BLEND_OP_DIFFERENCE_EXT", BlendOp::kDifference},
+        BlendOpData{"VK_BLEND_OP_EXCLUSION_EXT", BlendOp::kExclusion},
+        BlendOpData{"VK_BLEND_OP_INVERT_EXT", BlendOp::kInvert}), );
+
+INSTANTIATE_TEST_CASE_P(
+    BlendOpParsingTests2,
+    CommandParserBlendOpParsing,
+    testing::Values(
+        BlendOpData{"VK_BLEND_OP_INVERT_RGB_EXT", BlendOp::kInvertRGB},
+        BlendOpData{"VK_BLEND_OP_LINEARDODGE_EXT", BlendOp::kLinearDodge},
+        BlendOpData{"VK_BLEND_OP_LINEARBURN_EXT", BlendOp::kLinearBurn},
+        BlendOpData{"VK_BLEND_OP_VIVIDLIGHT_EXT", BlendOp::kVividLight},
+        BlendOpData{"VK_BLEND_OP_LINEARLIGHT_EXT", BlendOp::kLinearLight},
+        BlendOpData{"VK_BLEND_OP_PINLIGHT_EXT", BlendOp::kPinLight},
+        BlendOpData{"VK_BLEND_OP_HARDMIX_EXT", BlendOp::kHardMix},
+        BlendOpData{"VK_BLEND_OP_HSL_HUE_EXT", BlendOp::kHslHue},
+        BlendOpData{"VK_BLEND_OP_HSL_SATURATION_EXT", BlendOp::kHslSaturation},
+        BlendOpData{"VK_BLEND_OP_HSL_COLOR_EXT", BlendOp::kHslColor},
+        BlendOpData{"VK_BLEND_OP_HSL_LUMINOSITY_EXT", BlendOp::kHslLuminosity},
+        BlendOpData{"VK_BLEND_OP_PLUS_EXT", BlendOp::kPlus},
+        BlendOpData{"VK_BLEND_OP_PLUS_CLAMPED_EXT", BlendOp::kPlusClamped},
+        BlendOpData{"VK_BLEND_OP_PLUS_CLAMPED_ALPHA_EXT",
+                    BlendOp::kPlusClampedAlpha},
+        BlendOpData{"VK_BLEND_OP_PLUS_DARKER_EXT", BlendOp::kPlusDarker},
+        BlendOpData{"VK_BLEND_OP_MINUS_EXT", BlendOp::kMinus},
+        BlendOpData{"VK_BLEND_OP_MINUS_CLAMPED_EXT", BlendOp::kMinusClamped},
+        BlendOpData{"VK_BLEND_OP_CONTRAST_EXT", BlendOp::kContrast},
+        BlendOpData{"VK_BLEND_OP_INVERT_OVG_EXT", BlendOp::kInvertOvg},
+        BlendOpData{"VK_BLEND_OP_RED_EXT", BlendOp::kRed},
+        BlendOpData{"VK_BLEND_OP_GREEN_EXT", BlendOp::kGreen},
+        BlendOpData{"VK_BLEND_OP_BLUE_EXT", BlendOp::kBlue}), );
+
+TEST_F(CommandParserTest, BlendOpParsingInvalid) {
+  CommandParser cp;
+  BlendOp op = BlendOp::kAdd;
+  Result r = cp.ParseBlendOpNameForTesting("INVALID", &op);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Unknown BlendOp provided: INVALID", r.Error());
+}
+
+struct BlendOpTest {
+  const char* name;
+};
+using CommandParserBlendOpTests = testing::TestWithParam<BlendOpTest>;
+
+TEST_P(CommandParserBlendOpTests, MissingParam) {
+  const auto& test_data = GetParam();
+
+  std::string data = std::string(test_data.name);
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Missing parameter for ") + test_data.name + " command",
+            r.Error());
+}
+
+TEST_P(CommandParserBlendOpTests, IllegalParam) {
+  const auto& test_data = GetParam();
+
+  std::string data = std::string(test_data.name) + " 1.23";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Invalid parameter for ") + test_data.name + " command",
+            r.Error());
+}
+
+TEST_P(CommandParserBlendOpTests, ExtraParam) {
+  const auto& test_data = GetParam();
+
+  std::string data = std::string(test_data.name) + " VK_BLEND_OP_MAX EXTRA";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Extra parameter for ") + test_data.name + " command",
+            r.Error());
+}
+
+INSTANTIATE_TEST_CASE_P(BlendOpTests,
+                        CommandParserBlendOpTests,
+                        testing::Values(BlendOpTest{"colorBlendOp"},
+                                        BlendOpTest{"alphaBlendOp"}), );
+
+TEST_F(CommandParserTest, DepthCompareOp) {
+  std::string data = "depthCompareOp VK_COMPARE_OP_EQUAL";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(CompareOp::kEqual,
+            cp.PipelineDataForTesting()->GetDepthCompareOp());
+}
+
+TEST_F(CommandParserTest, FrontCompareOp) {
+  std::string data = "front.compareOp VK_COMPARE_OP_EQUAL";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(CompareOp::kEqual,
+            cp.PipelineDataForTesting()->GetFrontCompareOp());
+}
+
+TEST_F(CommandParserTest, BackCompareOp) {
+  std::string data = "back.compareOp VK_COMPARE_OP_EQUAL";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(CompareOp::kEqual, cp.PipelineDataForTesting()->GetBackCompareOp());
+}
+
+struct CompareOpData {
+  const char* name;
+  CompareOp type;
+};
+using CommandParserCompareOpParsing = testing::TestWithParam<CompareOpData>;
+
+TEST_P(CommandParserCompareOpParsing, Parse) {
+  const auto& test_data = GetParam();
+
+  CommandParser cp;
+  CompareOp op = CompareOp::kNever;
+  Result r = cp.ParseCompareOpNameForTesting(test_data.name, &op);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(test_data.type, op);
+}
+
+INSTANTIATE_TEST_CASE_P(
+    CompareOpParsingTests,
+    CommandParserCompareOpParsing,
+    testing::Values(
+        CompareOpData{"VK_COMPARE_OP_NEVER", CompareOp::kNever},
+        CompareOpData{"VK_COMPARE_OP_LESS", CompareOp::kLess},
+        CompareOpData{"VK_COMPARE_OP_EQUAL", CompareOp::kEqual},
+        CompareOpData{"VK_COMPARE_OP_LESS_OR_EQUAL", CompareOp::kLessOrEqual},
+        CompareOpData{"VK_COMPARE_OP_GREATER", CompareOp::kGreater},
+        CompareOpData{"VK_COMPARE_OP_NOT_EQUAL", CompareOp::kNotEqual},
+        CompareOpData{"VK_COMPARE_OP_GREATER_OR_EQUAL",
+                      CompareOp::kGreaterOrEqual},
+        CompareOpData{"VK_COMPARE_OP_ALWAYS", CompareOp::kAlways}), );
+
+TEST_F(CommandParserTest, CompareOpParsingInvalid) {
+  CommandParser cp;
+  CompareOp op = CompareOp::kNever;
+  Result r = cp.ParseCompareOpNameForTesting("INVALID", &op);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Unknown CompareOp provided: INVALID", r.Error());
+}
+
+struct CompareOpTest {
+  const char* name;
+};
+using CommandParserCompareOpTests = testing::TestWithParam<CompareOpTest>;
+
+TEST_P(CommandParserCompareOpTests, MissingParam) {
+  const auto& test_data = GetParam();
+
+  std::string data = std::string(test_data.name);
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Missing parameter for ") + test_data.name + " command",
+            r.Error());
+}
+
+TEST_P(CommandParserCompareOpTests, IllegalParam) {
+  const auto& test_data = GetParam();
+
+  std::string data = std::string(test_data.name) + " 1.23";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Invalid parameter for ") + test_data.name + " command",
+            r.Error());
+}
+
+TEST_P(CommandParserCompareOpTests, ExtraParam) {
+  const auto& test_data = GetParam();
+
+  std::string data =
+      std::string(test_data.name) + " VK_COMPARE_OP_ALWAYS EXTRA";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Extra parameter for ") + test_data.name + " command",
+            r.Error());
+}
+
+INSTANTIATE_TEST_CASE_P(CompareOpTests,
+                        CommandParserCompareOpTests,
+                        testing::Values(CompareOpTest{"depthCompareOp"},
+                                        CompareOpTest{"front.compareOp"},
+                                        CompareOpTest{"back.compareOp"}), );
+
+TEST_F(CommandParserTest, FrontFailOp) {
+  std::string data = "front.failOp VK_STENCIL_OP_REPLACE";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(StencilOp::kReplace, cp.PipelineDataForTesting()->GetFrontFailOp());
+}
+
+TEST_F(CommandParserTest, FrontPassOp) {
+  std::string data = "front.passOp VK_STENCIL_OP_REPLACE";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(StencilOp::kReplace, cp.PipelineDataForTesting()->GetFrontPassOp());
+}
+
+TEST_F(CommandParserTest, FrontDepthFailOp) {
+  std::string data = "front.depthFailOp VK_STENCIL_OP_REPLACE";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(StencilOp::kReplace,
+            cp.PipelineDataForTesting()->GetFrontDepthFailOp());
+}
+
+TEST_F(CommandParserTest, BackFailOp) {
+  std::string data = "back.failOp VK_STENCIL_OP_REPLACE";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(StencilOp::kReplace, cp.PipelineDataForTesting()->GetBackFailOp());
+}
+
+TEST_F(CommandParserTest, BackPassOp) {
+  std::string data = "back.passOp VK_STENCIL_OP_REPLACE";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(StencilOp::kReplace, cp.PipelineDataForTesting()->GetBackPassOp());
+}
+
+TEST_F(CommandParserTest, BackDepthFailOp) {
+  std::string data = "back.depthFailOp VK_STENCIL_OP_REPLACE";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(StencilOp::kReplace,
+            cp.PipelineDataForTesting()->GetBackDepthFailOp());
+}
+
+struct StencilOpData {
+  const char* name;
+  StencilOp type;
+};
+using CommandParserStencilOpParsing = testing::TestWithParam<StencilOpData>;
+
+TEST_P(CommandParserStencilOpParsing, Parse) {
+  const auto& test_data = GetParam();
+
+  CommandParser cp;
+  StencilOp op = StencilOp::kKeep;
+  Result r = cp.ParseStencilOpNameForTesting(test_data.name, &op);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(test_data.type, op);
+}
+
+INSTANTIATE_TEST_CASE_P(
+    CompareOpParsingTests,
+    CommandParserStencilOpParsing,
+    testing::Values(StencilOpData{"VK_STENCIL_OP_KEEP", StencilOp::kKeep},
+                    StencilOpData{"VK_STENCIL_OP_ZERO", StencilOp::kZero},
+                    StencilOpData{"VK_STENCIL_OP_REPLACE", StencilOp::kReplace},
+                    StencilOpData{"VK_STENCIL_OP_INCREMENT_AND_CLAMP",
+                                  StencilOp::kIncrementAndClamp},
+                    StencilOpData{"VK_STENCIL_OP_DECREMENT_AND_CLAMP",
+                                  StencilOp::kDecrementAndClamp},
+                    StencilOpData{"VK_STENCIL_OP_INVERT", StencilOp::kInvert},
+                    StencilOpData{"VK_STENCIL_OP_INCREMENT_AND_WRAP",
+                                  StencilOp::kIncrementAndWrap},
+                    StencilOpData{"VK_STENCIL_OP_DECREMENT_AND_WRAP",
+                                  StencilOp::kDecrementAndWrap}), );
+
+TEST_F(CommandParserTest, StencilOpParsingInvalid) {
+  CommandParser cp;
+  StencilOp op = StencilOp::kKeep;
+  Result r = cp.ParseStencilOpNameForTesting("INVALID", &op);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Unknown StencilOp provided: INVALID", r.Error());
+}
+
+struct StencilOpTest {
+  const char* name;
+};
+using CommandParserStencilOpTests = testing::TestWithParam<StencilOpTest>;
+
+TEST_P(CommandParserStencilOpTests, MissingParam) {
+  const auto& test_data = GetParam();
+
+  std::string data = std::string(test_data.name);
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Missing parameter for ") + test_data.name + " command",
+            r.Error());
+}
+
+TEST_P(CommandParserStencilOpTests, IllegalParam) {
+  const auto& test_data = GetParam();
+
+  std::string data = std::string(test_data.name) + " 1.23";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Invalid parameter for ") + test_data.name + " command",
+            r.Error());
+}
+
+TEST_P(CommandParserStencilOpTests, ExtraParam) {
+  const auto& test_data = GetParam();
+
+  std::string data =
+      std::string(test_data.name) + " VK_STENCIL_OP_REPLACE EXTRA";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Extra parameter for ") + test_data.name + " command",
+            r.Error());
+}
+
+INSTANTIATE_TEST_CASE_P(StencilOpTests,
+                        CommandParserStencilOpTests,
+                        testing::Values(StencilOpTest{"front.passOp"},
+                                        StencilOpTest{"front.failOp"},
+                                        StencilOpTest{"front.depthFailOp"},
+                                        StencilOpTest{"back.passOp"},
+                                        StencilOpTest{"back.failOp"},
+                                        StencilOpTest{"back.depthFailOp"}), );
+
+TEST_F(CommandParserTest, FrontCompareMask) {
+  std::string data = "front.compareMask 123";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("front.compareMask not implemented", r.Error());
+}
+
+TEST_F(CommandParserTest, FrontWriteMask) {
+  std::string data = "front.writeMask 123";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("front.writeMask not implemented", r.Error());
+}
+
+TEST_F(CommandParserTest, BackCompareMask) {
+  std::string data = "back.compareMask 123";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("back.compareMask not implemented", r.Error());
+}
+
+TEST_F(CommandParserTest, BackWriteMask) {
+  std::string data = "back.writeMask 123";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("back.writeMask not implemented", r.Error());
+}
+
+TEST_F(CommandParserTest, FrontReference) {
+  std::string data = "front.reference 10";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(10U, cp.PipelineDataForTesting()->GetFrontReference());
+}
+
+TEST_F(CommandParserTest, BackReference) {
+  std::string data = "back.reference 10";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(10U, cp.PipelineDataForTesting()->GetBackReference());
+}
+
+struct ReferenceData {
+  const char* name;
+};
+
+using CommandParserReferenceTests = testing::TestWithParam<ReferenceData>;
+
+TEST_P(CommandParserReferenceTests, FrontReferenceMissingValue) {
+  const auto& test_data = GetParam();
+  std::string data = std::string(test_data.name);
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Missing parameter for ") + test_data.name + " command",
+            r.Error());
+}
+
+TEST_P(CommandParserReferenceTests, FrontReferenceExtraParameters) {
+  const auto& test_data = GetParam();
+  std::string data = std::string(test_data.name) + " 10 EXTRA";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Extra parameter for ") + test_data.name + " command",
+            r.Error());
+}
+
+TEST_P(CommandParserReferenceTests, FrontReferenceInvalidParameters) {
+  const auto& test_data = GetParam();
+  std::string data = std::string(test_data.name) + " INVALID";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ(std::string("Invalid parameter for ") + test_data.name + " command",
+            r.Error());
+}
+
+INSTANTIATE_TEST_CASE_P(ReferenceTest,
+                        CommandParserReferenceTests,
+                        testing::Values(ReferenceData{"front.reference"},
+                                        ReferenceData{"back.reference"}), );
+
+struct ColorMaskData {
+  const char* input;
+  uint8_t result;
+};
+using CommandParserColorMaskTests = testing::TestWithParam<ColorMaskData>;
+
+TEST_P(CommandParserColorMaskTests, ColorWriteMask) {
+  const auto& test_data = GetParam();
+  std::string data = "colorWriteMask " + std::string(test_data.input);
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(test_data.result, cp.PipelineDataForTesting()->GetColorWriteMask());
+}
+
+INSTANTIATE_TEST_CASE_P(
+    ColorMaskTests,
+    CommandParserColorMaskTests,
+    testing::Values(
+        ColorMaskData{"VK_COLOR_COMPONENT_R_BIT", kColorMaskR},
+        ColorMaskData{"VK_COLOR_COMPONENT_G_BIT", kColorMaskG},
+        ColorMaskData{"VK_COLOR_COMPONENT_B_BIT", kColorMaskB},
+        ColorMaskData{"VK_COLOR_COMPONENT_A_BIT", kColorMaskA},
+        ColorMaskData{"VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | "
+                      "VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT",
+                      kColorMaskR | kColorMaskG | kColorMaskB | kColorMaskA},
+        ColorMaskData{
+            "VK_COLOR_COMPONENT_A_BIT | VK_COLOR_COMPONENT_B_BIT | "
+            "VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT",
+            kColorMaskR | kColorMaskG | kColorMaskB | kColorMaskA}), );
+
+TEST_F(CommandParserTest, ColorWriteMaskInvalid) {
+  std::string data = "colorWriteMask INVALID";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Unknown parameter for colorWriteMask command", r.Error());
+}
+
+TEST_F(CommandParserTest, ColorWriteMaskInvalidAfterValid) {
+  std::string data = "colorWriteMask VK_COLOR_COMPONENT_G_BIT | INVALID";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Unknown parameter for colorWriteMask command", r.Error());
+}
+
+TEST_F(CommandParserTest, ColorWriteMaskMissingParam) {
+  std::string data = "colorWriteMask";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Missing parameter for colorWriteMask command", r.Error());
+}
+
+TEST_F(CommandParserTest, ColorWriteMaskExtraParam) {
+  std::string data =
+      "colorWriteMask VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_B_BIT "
+      "EXTRA";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Unknown parameter for colorWriteMask command", r.Error());
+}
+
+TEST_F(CommandParserTest, SSBO) {
+  std::string data = "ssbo 5 40";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsBuffer());
+
+  auto* cmd = cmds[0]->AsBuffer();
+  EXPECT_TRUE(cmd->IsSSBO());
+  EXPECT_EQ(static_cast<uint32_t>(0), cmd->GetDescriptorSet());
+  EXPECT_EQ(5U, cmd->GetBinding());
+  EXPECT_EQ(40U, cmd->GetSize());
+}
+
+TEST_F(CommandParserTest, SSBOWithDescriptorSet) {
+  std::string data = "ssbo 9:5 40";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsBuffer());
+
+  auto* cmd = cmds[0]->AsBuffer();
+  EXPECT_TRUE(cmd->IsSSBO());
+  EXPECT_EQ(9U, cmd->GetDescriptorSet());
+  EXPECT_EQ(5U, cmd->GetBinding());
+  EXPECT_EQ(40U, cmd->GetSize());
+}
+
+TEST_F(CommandParserTest, SSBOExtraParameter) {
+  std::string data = "ssbo 5 40 EXTRA";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Extra parameter for ssbo command", r.Error());
+}
+
+TEST_F(CommandParserTest, SSBOInvalidFloatBinding) {
+  std::string data = "ssbo 5.0 40";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid binding value for ssbo command", r.Error());
+}
+
+TEST_F(CommandParserTest, SSBOInvalidBinding) {
+  std::string data = "ssbo abc 40";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid binding value for ssbo command", r.Error());
+}
+
+TEST_F(CommandParserTest, SSBOInvalidFloatSize) {
+  std::string data = "ssbo 5 40.0";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid size value for ssbo command", r.Error());
+}
+
+TEST_F(CommandParserTest, SSBOInvalidSize) {
+  std::string data = "ssbo 5 abc";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid value for ssbo command", r.Error());
+}
+
+TEST_F(CommandParserTest, SSBOMissingSize) {
+  std::string data = "ssbo 5";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Missing size value for ssbo command", r.Error());
+}
+
+TEST_F(CommandParserTest, SSBOMissingBinding) {
+  std::string data = "ssbo";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Missing binding and size values for ssbo command", r.Error());
+}
+
+TEST_F(CommandParserTest, SSBOSubdataWithFloat) {
+  std::string data = "ssbo 6 subdata vec3 2 2.3 4.2 1.2";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsBuffer());
+
+  auto* cmd = cmds[0]->AsBuffer();
+  EXPECT_TRUE(cmd->IsSSBO());
+  EXPECT_EQ(static_cast<uint32_t>(0), cmd->GetDescriptorSet());
+  EXPECT_EQ(6U, cmd->GetBinding());
+  EXPECT_EQ(2U, cmd->GetOffset());
+  ASSERT_TRUE(cmd->IsSubdata());
+
+  const auto& type = cmd->GetDatumType();
+  EXPECT_TRUE(type.IsFloat());
+  EXPECT_EQ(1U, type.ColumnCount());
+  EXPECT_EQ(3U, type.RowCount());
+
+  const auto& values = cmd->GetValues();
+  std::vector<float> results = {2.3f, 4.2f, 1.2f};
+  ASSERT_EQ(results.size(), values.size());
+  for (size_t i = 0; i < results.size(); ++i) {
+    EXPECT_FLOAT_EQ(results[i], values[i].AsFloat());
+  }
+}
+
+TEST_F(CommandParserTest, SSBOSubdataWithDescriptorSet) {
+  std::string data = "ssbo 5:6 subdata vec3 2 2.3 4.2 1.2";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsBuffer());
+
+  auto* cmd = cmds[0]->AsBuffer();
+  EXPECT_TRUE(cmd->IsSSBO());
+  EXPECT_EQ(5U, cmd->GetDescriptorSet());
+  EXPECT_EQ(6U, cmd->GetBinding());
+  EXPECT_EQ(2U, cmd->GetOffset());
+  ASSERT_TRUE(cmd->IsSubdata());
+
+  const auto& type = cmd->GetDatumType();
+  EXPECT_TRUE(type.IsFloat());
+  EXPECT_EQ(1U, type.ColumnCount());
+  EXPECT_EQ(3U, type.RowCount());
+
+  const auto& values = cmd->GetValues();
+  std::vector<float> results = {2.3f, 4.2f, 1.2f};
+  ASSERT_EQ(results.size(), values.size());
+  for (size_t i = 0; i < results.size(); ++i) {
+    EXPECT_FLOAT_EQ(results[i], values[i].AsFloat());
+  }
+}
+
+TEST_F(CommandParserTest, SSBOSubdataWithInts) {
+  std::string data = "ssbo 6 subdata i16vec3 2 2 4 1";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsBuffer());
+
+  auto* cmd = cmds[0]->AsBuffer();
+  EXPECT_TRUE(cmd->IsSSBO());
+  EXPECT_EQ(static_cast<uint32_t>(0), cmd->GetDescriptorSet());
+  EXPECT_EQ(6U, cmd->GetBinding());
+  EXPECT_EQ(2U, cmd->GetOffset());
+  ASSERT_TRUE(cmd->IsSubdata());
+
+  const auto& type = cmd->GetDatumType();
+  EXPECT_TRUE(type.IsInt16());
+  EXPECT_EQ(1U, type.ColumnCount());
+  EXPECT_EQ(3U, type.RowCount());
+
+  const auto& values = cmd->GetValues();
+  std::vector<int16_t> results = {2, 4, 1};
+  ASSERT_EQ(results.size(), values.size());
+  for (size_t i = 0; i < results.size(); ++i) {
+    EXPECT_FLOAT_EQ(results[i], values[i].AsInt16());
+  }
+}
+
+TEST_F(CommandParserTest, SSBOSubdataWithMultipleVectors) {
+  std::string data = "ssbo 6 subdata i16vec3 2 2 4 1 3 6 8";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsBuffer());
+
+  auto* cmd = cmds[0]->AsBuffer();
+  EXPECT_TRUE(cmd->IsSSBO());
+  EXPECT_EQ(static_cast<uint32_t>(0), cmd->GetDescriptorSet());
+  EXPECT_EQ(6U, cmd->GetBinding());
+  EXPECT_EQ(2U, cmd->GetOffset());
+  ASSERT_TRUE(cmd->IsSubdata());
+
+  const auto& type = cmd->GetDatumType();
+  EXPECT_TRUE(type.IsInt16());
+  EXPECT_EQ(1U, type.ColumnCount());
+  EXPECT_EQ(3U, type.RowCount());
+
+  const auto& values = cmd->GetValues();
+  std::vector<int16_t> results = {2, 4, 1, 3, 6, 8};
+  ASSERT_EQ(results.size(), values.size());
+  for (size_t i = 0; i < results.size(); ++i) {
+    EXPECT_FLOAT_EQ(results[i], values[i].AsInt16());
+  }
+}
+
+TEST_F(CommandParserTest, SSBOSubdataMissingBinding) {
+  std::string data = "ssbo subdata i16vec3 2 2 3 2";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid binding value for ssbo command", r.Error());
+}
+
+TEST_F(CommandParserTest, SSBOSubdataWithInvalidBinding) {
+  std::string data = "ssbo INVALID subdata i16vec3 2 2 3 4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid binding value for ssbo command", r.Error());
+}
+
+TEST_F(CommandParserTest, SSBOSubdataMissingSubdataCommand) {
+  std::string data = "ssbo 6 INVALID i16vec3 2 2";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid value for ssbo command", r.Error());
+}
+
+TEST_F(CommandParserTest, SSBOSubdataWithBadType) {
+  std::string data = "ssbo 0 subdata INVALID 2 2 3 4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid type provided: INVALID", r.Error());
+}
+
+TEST_F(CommandParserTest, SSBOSubdataWithInvalidFloatOffset) {
+  std::string data = "ssbo 0 subdata vec2 2.0 3 2 4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid offset for ssbo command", r.Error());
+}
+
+TEST_F(CommandParserTest, SSBOSubdataWithInvalidStringOffset) {
+  std::string data = "ssbo 0 subdata vec2 asdf 3 2 4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid offset for ssbo command", r.Error());
+}
+
+TEST_F(CommandParserTest, SSBOSubdataWithMissingData) {
+  std::string data = "ssbo 6 subdata i16vec3 2 2";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Incorrect number of values provided to ssbo command", r.Error());
+}
+
+TEST_F(CommandParserTest, SSBOSubdataWithMissingAllData) {
+  std::string data = "ssbo 6 subdata i16vec3 2";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Incorrect number of values provided to ssbo command", r.Error());
+}
+
+TEST_F(CommandParserTest, Uniform) {
+  std::string data = "uniform vec3 2 2.1 3.2 4.3";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsBuffer());
+
+  auto* cmd = cmds[0]->AsBuffer();
+  EXPECT_TRUE(cmd->IsPushConstant());
+  EXPECT_EQ(2U, cmd->GetOffset());
+
+  const auto& type = cmd->GetDatumType();
+  EXPECT_TRUE(type.IsFloat());
+  EXPECT_EQ(1U, type.ColumnCount());
+  EXPECT_EQ(3U, type.RowCount());
+
+  const auto& values = cmd->GetValues();
+  std::vector<float> results = {2.1f, 3.2f, 4.3f};
+  ASSERT_EQ(results.size(), values.size());
+  for (size_t i = 0; i < results.size(); ++i) {
+    EXPECT_FLOAT_EQ(results[i], values[i].AsFloat());
+  }
+}
+
+TEST_F(CommandParserTest, UniformWithContinuation) {
+  std::string data = "uniform vec3 2 2.1 3.2 4.3 \\\n5.4 6.7 8.9";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsBuffer());
+
+  auto* cmd = cmds[0]->AsBuffer();
+  EXPECT_TRUE(cmd->IsPushConstant());
+  EXPECT_EQ(2U, cmd->GetOffset());
+
+  const auto& type = cmd->GetDatumType();
+  EXPECT_TRUE(type.IsFloat());
+  EXPECT_EQ(1U, type.ColumnCount());
+  EXPECT_EQ(3U, type.RowCount());
+
+  const auto& values = cmd->GetValues();
+  std::vector<float> results = {2.1f, 3.2f, 4.3f, 5.4f, 6.7f, 8.9f};
+  ASSERT_EQ(results.size(), values.size());
+  for (size_t i = 0; i < results.size(); ++i) {
+    EXPECT_FLOAT_EQ(results[i], values[i].AsFloat());
+  }
+}
+
+TEST_F(CommandParserTest, UniformInvalidType) {
+  std::string data = "uniform INVALID 0 2.1 3.2 4.3";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid type provided: INVALID", r.Error());
+}
+
+TEST_F(CommandParserTest, UniformInvalidFloatOffset) {
+  std::string data = "uniform vec3 5.5 2.1 3.2 4.3";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid offset value for uniform command", r.Error());
+}
+
+TEST_F(CommandParserTest, UniformInvalidStringOffset) {
+  std::string data = "uniform vec3 INVALID 2.1 3.2 4.3";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid offset value for uniform command", r.Error());
+}
+
+TEST_F(CommandParserTest, UniformMissingValues) {
+  std::string data = "uniform vec3 2 2.1 3.2 4.3 5.5";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Incorrect number of values provided to uniform command",
+            r.Error());
+}
+
+TEST_F(CommandParserTest, UniformUBO) {
+  std::string data = "uniform ubo 2 vec3 1 2.1 3.2 4.3";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsBuffer());
+
+  auto* cmd = cmds[0]->AsBuffer();
+  EXPECT_TRUE(cmd->IsUniform());
+  EXPECT_EQ(static_cast<uint32_t>(0), cmd->GetDescriptorSet());
+  EXPECT_EQ(2U, cmd->GetBinding());
+  EXPECT_EQ(1U, cmd->GetOffset());
+
+  const auto& type = cmd->GetDatumType();
+  EXPECT_TRUE(type.IsFloat());
+  EXPECT_EQ(1U, type.ColumnCount());
+  EXPECT_EQ(3U, type.RowCount());
+
+  const auto& values = cmd->GetValues();
+  std::vector<float> results = {2.1f, 3.2f, 4.3f};
+  ASSERT_EQ(results.size(), values.size());
+  for (size_t i = 0; i < results.size(); ++i) {
+    EXPECT_FLOAT_EQ(results[i], values[i].AsFloat());
+  }
+}
+
+TEST_F(CommandParserTest, UniformUBOWithDescriptorSet) {
+  std::string data = "uniform ubo 3:2 vec3 1 2.1 3.2 4.3";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsBuffer());
+
+  auto* cmd = cmds[0]->AsBuffer();
+  EXPECT_TRUE(cmd->IsUniform());
+  EXPECT_EQ(3U, cmd->GetDescriptorSet());
+  EXPECT_EQ(2U, cmd->GetBinding());
+  EXPECT_EQ(1U, cmd->GetOffset());
+
+  const auto& type = cmd->GetDatumType();
+  EXPECT_TRUE(type.IsFloat());
+  EXPECT_EQ(1U, type.ColumnCount());
+  EXPECT_EQ(3U, type.RowCount());
+
+  const auto& values = cmd->GetValues();
+  std::vector<float> results = {2.1f, 3.2f, 4.3f};
+  ASSERT_EQ(results.size(), values.size());
+  for (size_t i = 0; i < results.size(); ++i) {
+    EXPECT_FLOAT_EQ(results[i], values[i].AsFloat());
+  }
+}
+
+TEST_F(CommandParserTest, UniformUBOInvalidFloatBinding) {
+  std::string data = "uniform ubo 0.0 vec3 0 2.1 3.2 4.3";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid binding value for uniform ubo command", r.Error());
+}
+
+TEST_F(CommandParserTest, UniformUBOInvalidStringBinding) {
+  std::string data = "uniform ubo INVALID vec3 0 2.1 3.2 4.3";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid binding value for uniform ubo command", r.Error());
+}
+
+TEST_F(CommandParserTest, UniformUBOInvalidType) {
+  std::string data = "uniform ubo 0 INVALID 0 2.1 3.2 4.3";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid type provided: INVALID", r.Error());
+}
+
+TEST_F(CommandParserTest, UniformUBOInvalidFloatOffset) {
+  std::string data = "uniform ubo 0 vec3 5.5 2.1 3.2 4.3";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid offset value for uniform command", r.Error());
+}
+
+TEST_F(CommandParserTest, UniformUBOInvalidStringOffset) {
+  std::string data = "uniform ubo 0 vec3 INVALID 2.1 3.2 4.3";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid offset value for uniform command", r.Error());
+}
+
+TEST_F(CommandParserTest, UniformUBOMissingValues) {
+  std::string data = "uniform ubo 0 vec3 2 2.1 3.2 4.3 5.5";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Incorrect number of values provided to uniform command",
+            r.Error());
+}
+
+TEST_F(CommandParserTest, ToleranceSingleFloatValue) {
+  std::string data = "tolerance 0.5";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsTolerance());
+
+  auto* cmd = cmds[0]->AsTolerance();
+  const auto& tolerances = cmd->GetTolerances();
+
+  ASSERT_EQ(1U, tolerances.size());
+  EXPECT_FALSE(tolerances[0].is_percent);
+  EXPECT_FLOAT_EQ(0.5, tolerances[0].value);
+}
+
+TEST_F(CommandParserTest, ToleranceSingleFloatPercent) {
+  std::string data = "tolerance 0.5%";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsTolerance());
+
+  auto* cmd = cmds[0]->AsTolerance();
+  const auto& tolerances = cmd->GetTolerances();
+
+  ASSERT_EQ(1U, tolerances.size());
+  EXPECT_TRUE(tolerances[0].is_percent);
+  EXPECT_FLOAT_EQ(0.5, tolerances[0].value);
+}
+
+TEST_F(CommandParserTest, ToleranceSingleIntValue) {
+  std::string data = "tolerance 5";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsTolerance());
+
+  auto* cmd = cmds[0]->AsTolerance();
+  const auto& tolerances = cmd->GetTolerances();
+
+  ASSERT_EQ(1U, tolerances.size());
+  EXPECT_FALSE(tolerances[0].is_percent);
+  EXPECT_FLOAT_EQ(5.0, tolerances[0].value);
+}
+
+TEST_F(CommandParserTest, ToleranceSingleIntPercent) {
+  std::string data = "tolerance 5%";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsTolerance());
+
+  auto* cmd = cmds[0]->AsTolerance();
+  const auto& tolerances = cmd->GetTolerances();
+
+  ASSERT_EQ(1U, tolerances.size());
+  EXPECT_TRUE(tolerances[0].is_percent);
+  EXPECT_FLOAT_EQ(5.0, tolerances[0].value);
+}
+
+TEST_F(CommandParserTest, ToleranceMultiFloatValue) {
+  std::string data = "tolerance 0.5 2.4 3.9 99.7";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsTolerance());
+
+  auto* cmd = cmds[0]->AsTolerance();
+  const auto& tolerances = cmd->GetTolerances();
+
+  std::vector<double> results = {0.5, 2.4, 3.9, 99.7};
+  ASSERT_EQ(results.size(), tolerances.size());
+  for (size_t i = 0; i < results.size(); ++i) {
+    EXPECT_FALSE(tolerances[0].is_percent);
+    EXPECT_FLOAT_EQ(results[i], tolerances[i].value);
+  }
+}
+
+TEST_F(CommandParserTest, ToleranceMultiFloatValueWithPercent) {
+  std::string data = "tolerance 0.5% 2.4 3.9% 99.7";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsTolerance());
+
+  auto* cmd = cmds[0]->AsTolerance();
+  const auto& tolerances = cmd->GetTolerances();
+
+  std::vector<double> results = {0.5, 2.4, 3.9, 99.7};
+  ASSERT_EQ(results.size(), tolerances.size());
+  for (size_t i = 0; i < results.size(); ++i) {
+    if (i % 2 == 0)
+      EXPECT_TRUE(tolerances[i].is_percent);
+    else
+      EXPECT_FALSE(tolerances[i].is_percent);
+
+    EXPECT_FLOAT_EQ(results[i], tolerances[i].value);
+  }
+}
+
+TEST_F(CommandParserTest, ToleranceMultiIntValue) {
+  std::string data = "tolerance 5 4 3 99";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsTolerance());
+
+  auto* cmd = cmds[0]->AsTolerance();
+  const auto& tolerances = cmd->GetTolerances();
+
+  std::vector<double> results = {5.0, 4.0, 3.0, 99.0};
+  ASSERT_EQ(results.size(), tolerances.size());
+  for (size_t i = 0; i < results.size(); ++i) {
+    EXPECT_FALSE(tolerances[0].is_percent);
+    EXPECT_FLOAT_EQ(results[i], tolerances[i].value);
+  }
+}
+
+TEST_F(CommandParserTest, ToleranceMultiIntValueWithPercent) {
+  std::string data = "tolerance 5% 4 3% 99";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsTolerance());
+
+  auto* cmd = cmds[0]->AsTolerance();
+  const auto& tolerances = cmd->GetTolerances();
+
+  std::vector<double> results = {5.0, 4.0, 3.0, 99.0};
+  ASSERT_EQ(results.size(), tolerances.size());
+  for (size_t i = 0; i < results.size(); ++i) {
+    if (i % 2 == 0)
+      EXPECT_TRUE(tolerances[i].is_percent);
+    else
+      EXPECT_FALSE(tolerances[i].is_percent);
+
+    EXPECT_FLOAT_EQ(results[i], tolerances[i].value);
+  }
+}
+
+TEST_F(CommandParserTest, ToleranceInvalidValue1) {
+  std::string data = "tolerance INVALID";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid value for tolerance command", r.Error());
+}
+
+TEST_F(CommandParserTest, ToleranceInvalidJustPercent) {
+  std::string data = "tolerance %";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid value for tolerance command", r.Error());
+}
+
+TEST_F(CommandParserTest, ToleranceInvalidValue2) {
+  std::string data = "tolerance 1 INVALID 3 4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid value for tolerance command", r.Error());
+}
+
+TEST_F(CommandParserTest, ToleranceInvalidValue3) {
+  std::string data = "tolerance 1 2 INVALID 4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid value for tolerance command", r.Error());
+}
+
+TEST_F(CommandParserTest, ToleranceInvalidValue4) {
+  std::string data = "tolerance 1 2 3 INVALID";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid value for tolerance command", r.Error());
+}
+
+TEST_F(CommandParserTest, ToleranceMissingValues) {
+  std::string data = "tolerance";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Missing value for tolerance command", r.Error());
+}
+
+TEST_F(CommandParserTest, ToleranceTooManyValues) {
+  std::string data = "tolerance 1 2 3 4 5";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Extra parameter for tolerance command", r.Error());
+}
+
+TEST_F(CommandParserTest, ToleranceInvalidWithNumber) {
+  std::string data = "tolerance 1INVALID";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid value for tolerance command", r.Error());
+}
+
+TEST_F(CommandParserTest, ToleranceInvalidWithMissingValue) {
+  std::string data = "tolerance 1, , 3, 4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid number of tolerance parameters provided", r.Error());
+}
+
+TEST_F(CommandParserTest, ToleranceWithCommas) {
+  std::string data = "tolerance 1,2, 3 ,4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsTolerance());
+
+  auto* cmd = cmds[0]->AsTolerance();
+  const auto& tolerances = cmd->GetTolerances();
+
+  std::vector<double> results = {1.0, 2.0, 3.0, 4.0};
+  ASSERT_EQ(results.size(), tolerances.size());
+  for (size_t i = 0; i < results.size(); ++i) {
+    EXPECT_FALSE(tolerances[0].is_percent);
+    EXPECT_FLOAT_EQ(results[i], tolerances[i].value);
+  }
+}
+
+TEST_F(CommandParserTest, ProbeSSBOWithDescriptorSet) {
+  std::string data = "probe ssbo vec3 3:6 2 >= 2.3 4.2 1.2";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsProbeSSBO());
+
+  auto* cmd = cmds[0]->AsProbeSSBO();
+  EXPECT_EQ(3U, cmd->GetDescriptorSet());
+  EXPECT_EQ(6U, cmd->GetBinding());
+  EXPECT_EQ(2U, cmd->GetOffset());
+  EXPECT_EQ(ProbeSSBOCommand::Comparator::kGreaterOrEqual,
+            cmd->GetComparator());
+
+  const auto& type = cmd->GetDatumType();
+  EXPECT_TRUE(type.IsFloat());
+  EXPECT_EQ(1U, type.ColumnCount());
+  EXPECT_EQ(3U, type.RowCount());
+
+  const auto& values = cmd->GetValues();
+  std::vector<float> results = {2.3f, 4.2f, 1.2f};
+  ASSERT_EQ(results.size(), values.size());
+  for (size_t i = 0; i < results.size(); ++i) {
+    EXPECT_FLOAT_EQ(results[i], values[i].AsFloat());
+  }
+}
+
+TEST_F(CommandParserTest, ProbeSSBOWithFloats) {
+  std::string data = "probe ssbo vec3 6 2 >= 2.3 4.2 1.2";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsProbeSSBO());
+
+  auto* cmd = cmds[0]->AsProbeSSBO();
+  EXPECT_EQ(static_cast<uint32_t>(0), cmd->GetDescriptorSet());
+  EXPECT_EQ(6U, cmd->GetBinding());
+  EXPECT_EQ(2U, cmd->GetOffset());
+  EXPECT_EQ(ProbeSSBOCommand::Comparator::kGreaterOrEqual,
+            cmd->GetComparator());
+
+  const auto& type = cmd->GetDatumType();
+  EXPECT_TRUE(type.IsFloat());
+  EXPECT_EQ(1U, type.ColumnCount());
+  EXPECT_EQ(3U, type.RowCount());
+
+  const auto& values = cmd->GetValues();
+  std::vector<float> results = {2.3f, 4.2f, 1.2f};
+  ASSERT_EQ(results.size(), values.size());
+  for (size_t i = 0; i < results.size(); ++i) {
+    EXPECT_FLOAT_EQ(results[i], values[i].AsFloat());
+  }
+}
+
+TEST_F(CommandParserTest, MultiProbeSSBOWithFloats) {
+  std::string data =
+      "probe ssbo vec3 6 2 >= 2.3 4.2 1.2\nprobe ssbo vec3 6 2 >= 2.3 4.2 1.2";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(2U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsProbeSSBO());
+
+  auto* cmd = cmds[0]->AsProbeSSBO();
+  EXPECT_EQ(6U, cmd->GetBinding());
+  EXPECT_EQ(2U, cmd->GetOffset());
+  EXPECT_EQ(ProbeSSBOCommand::Comparator::kGreaterOrEqual,
+            cmd->GetComparator());
+
+  const auto& type = cmd->GetDatumType();
+  EXPECT_TRUE(type.IsFloat());
+  EXPECT_EQ(1U, type.ColumnCount());
+  EXPECT_EQ(3U, type.RowCount());
+
+  const auto& values = cmd->GetValues();
+  std::vector<float> results = {2.3f, 4.2f, 1.2f};
+  ASSERT_EQ(results.size(), values.size());
+  for (size_t i = 0; i < results.size(); ++i) {
+    EXPECT_FLOAT_EQ(results[i], values[i].AsFloat());
+  }
+}
+
+TEST_F(CommandParserTest, ProbeSSBOWithInts) {
+  std::string data = "probe ssbo i16vec3 6 2 <= 2 4 1";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsProbeSSBO());
+
+  auto* cmd = cmds[0]->AsProbeSSBO();
+  EXPECT_EQ(static_cast<uint32_t>(0), cmd->GetDescriptorSet());
+  EXPECT_EQ(6U, cmd->GetBinding());
+  EXPECT_EQ(2U, cmd->GetOffset());
+  EXPECT_EQ(ProbeSSBOCommand::Comparator::kLessOrEqual, cmd->GetComparator());
+
+  const auto& type = cmd->GetDatumType();
+  EXPECT_TRUE(type.IsInt16());
+  EXPECT_EQ(1U, type.ColumnCount());
+  EXPECT_EQ(3U, type.RowCount());
+
+  const auto& values = cmd->GetValues();
+  std::vector<int16_t> results = {2, 4, 1};
+  ASSERT_EQ(results.size(), values.size());
+  for (size_t i = 0; i < results.size(); ++i) {
+    EXPECT_FLOAT_EQ(results[i], values[i].AsInt16());
+  }
+}
+
+TEST_F(CommandParserTest, ProbeSSBOWithMultipleVectors) {
+  std::string data = "probe ssbo i16vec3 6 2 == 2 4 1 3 6 8";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& cmds = cp.Commands();
+  ASSERT_EQ(1U, cmds.size());
+  ASSERT_TRUE(cmds[0]->IsProbeSSBO());
+
+  auto* cmd = cmds[0]->AsProbeSSBO();
+  EXPECT_EQ(static_cast<uint32_t>(0), cmd->GetDescriptorSet());
+  EXPECT_EQ(6U, cmd->GetBinding());
+  EXPECT_EQ(2U, cmd->GetOffset());
+  EXPECT_EQ(ProbeSSBOCommand::Comparator::kEqual, cmd->GetComparator());
+
+  const auto& type = cmd->GetDatumType();
+  EXPECT_TRUE(type.IsInt16());
+  EXPECT_EQ(1U, type.ColumnCount());
+  EXPECT_EQ(3U, type.RowCount());
+
+  const auto& values = cmd->GetValues();
+  std::vector<int16_t> results = {2, 4, 1, 3, 6, 8};
+  ASSERT_EQ(results.size(), values.size());
+  for (size_t i = 0; i < results.size(); ++i) {
+    EXPECT_FLOAT_EQ(results[i], values[i].AsInt16());
+  }
+}
+
+TEST_F(CommandParserTest, ProbeSSBOMissingBinding) {
+  std::string data = "probe ssbo i16vec3 2 == 2 3 2";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid value for probe ssbo command", r.Error());
+}
+
+TEST_F(CommandParserTest, ProbeSSBOWithInvalidBinding) {
+  std::string data = "probe ssbo i16vec3 INVALID 2 == 2 3 4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid binding value for probe ssbo command", r.Error());
+}
+
+TEST_F(CommandParserTest, ProbeSSBOWithBadType) {
+  std::string data = "probe ssbo INVALID 0 2 == 2 3 4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid type provided: INVALID", r.Error());
+}
+
+TEST_F(CommandParserTest, ProbeSSBOWithInvalidFloatOffset) {
+  std::string data = "probe ssbo vec2 0 2.0 == 3 2 4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid offset for probe ssbo command", r.Error());
+}
+
+TEST_F(CommandParserTest, ProbeSSBOWithInvalidStringOffset) {
+  std::string data = "probe ssbo vec2 0 INVALID == 3 2 4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid value for probe ssbo command", r.Error());
+}
+
+TEST_F(CommandParserTest, ProbeSSBOWithInvalidComparator) {
+  std::string data = "probe ssbo vec2 6 2 INVALID 3 2 4";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid comparator", r.Error());
+}
+
+TEST_F(CommandParserTest, ProbeSSBOWithMissingData) {
+  std::string data = "probe ssbo i16vec3 6 2 == 2";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Incorrect number of values provided to probe ssbo command",
+            r.Error());
+}
+
+TEST_F(CommandParserTest, ProbeSSBOWithMissingAllData) {
+  std::string data = "probe ssbo i16vec3 6 2 ==";
+
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Incorrect number of values provided to probe ssbo command",
+            r.Error());
+}
+
+struct ComparatorTest {
+  const char* name;
+  ProbeSSBOCommand::Comparator op;
+};
+using CommandParserComparatorTests = testing::TestWithParam<ComparatorTest>;
+
+TEST_P(CommandParserComparatorTests, Comparator) {
+  const auto& test_data = GetParam();
+
+  CommandParser cp;
+  ProbeSSBOCommand::Comparator result;
+  Result r = cp.ParseComparatorForTesting(test_data.name, &result);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(test_data.op, result);
+}
+
+INSTANTIATE_TEST_CASE_P(
+    ComparatorTests,
+    CommandParserComparatorTests,
+    testing::Values(
+        ComparatorTest{"==", ProbeSSBOCommand::Comparator::kEqual},
+        ComparatorTest{"!=", ProbeSSBOCommand::Comparator::kNotEqual},
+        ComparatorTest{"~=", ProbeSSBOCommand::Comparator::kFuzzyEqual},
+        ComparatorTest{"<", ProbeSSBOCommand::Comparator::kLess},
+        ComparatorTest{"<=", ProbeSSBOCommand::Comparator::kLessOrEqual},
+        ComparatorTest{">", ProbeSSBOCommand::Comparator::kGreater},
+        ComparatorTest{">=", ProbeSSBOCommand::Comparator::kGreaterOrEqual}), );
+
+TEST_F(CommandParserTest, ComparatorInvalid) {
+  CommandParser cp;
+  ProbeSSBOCommand::Comparator result;
+  Result r = cp.ParseComparatorForTesting("INVALID", &result);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid comparator", r.Error());
+}
+
+}  // namespace vkscript
+}  // namespace amber
diff --git a/src/vkscript/datum_type_parser.cc b/src/vkscript/datum_type_parser.cc
new file mode 100644
index 0000000..ca0c6e5
--- /dev/null
+++ b/src/vkscript/datum_type_parser.cc
@@ -0,0 +1,240 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vkscript/datum_type_parser.h"
+
+namespace amber {
+namespace vkscript {
+
+DatumTypeParser::DatumTypeParser() = default;
+
+DatumTypeParser::~DatumTypeParser() = default;
+
+Result DatumTypeParser::Parse(const std::string& data) {
+  // TODO(dsinclair): Might want to make this nicer in the future, but this
+  // works and is easy for now.
+  if (data == "int") {
+    type_.SetType(DataType::kInt32);
+  } else if (data == "uint") {
+    type_.SetType(DataType::kUint32);
+  } else if (data == "int8_t") {
+    type_.SetType(DataType::kInt8);
+  } else if (data == "uint8_t") {
+    type_.SetType(DataType::kUint8);
+  } else if (data == "int16_t") {
+    type_.SetType(DataType::kInt16);
+  } else if (data == "uint16_t") {
+    type_.SetType(DataType::kUint16);
+  } else if (data == "int64_t") {
+    type_.SetType(DataType::kInt64);
+  } else if (data == "uint64_t") {
+    type_.SetType(DataType::kUint64);
+  } else if (data == "float") {
+    type_.SetType(DataType::kFloat);
+  } else if (data == "double") {
+    type_.SetType(DataType::kDouble);
+  } else if (data == "vec2") {
+    type_.SetType(DataType::kFloat);
+    type_.SetRowCount(2);
+  } else if (data == "vec3") {
+    type_.SetType(DataType::kFloat);
+    type_.SetRowCount(3);
+  } else if (data == "vec4") {
+    type_.SetType(DataType::kFloat);
+    type_.SetRowCount(4);
+  } else if (data == "dvec2") {
+    type_.SetType(DataType::kDouble);
+    type_.SetRowCount(2);
+  } else if (data == "dvec3") {
+    type_.SetType(DataType::kDouble);
+    type_.SetRowCount(3);
+  } else if (data == "dvec4") {
+    type_.SetType(DataType::kDouble);
+    type_.SetRowCount(4);
+  } else if (data == "ivec2") {
+    type_.SetType(DataType::kInt32);
+    type_.SetRowCount(2);
+  } else if (data == "ivec3") {
+    type_.SetType(DataType::kInt32);
+    type_.SetRowCount(3);
+  } else if (data == "ivec4") {
+    type_.SetType(DataType::kInt32);
+    type_.SetRowCount(4);
+  } else if (data == "uvec2") {
+    type_.SetType(DataType::kUint32);
+    type_.SetRowCount(2);
+  } else if (data == "uvec3") {
+    type_.SetType(DataType::kUint32);
+    type_.SetRowCount(3);
+  } else if (data == "uvec4") {
+    type_.SetType(DataType::kUint32);
+    type_.SetRowCount(4);
+  } else if (data == "i8vec2") {
+    type_.SetType(DataType::kInt8);
+    type_.SetRowCount(2);
+  } else if (data == "i8vec3") {
+    type_.SetType(DataType::kInt8);
+    type_.SetRowCount(3);
+  } else if (data == "i8vec4") {
+    type_.SetType(DataType::kInt8);
+    type_.SetRowCount(4);
+  } else if (data == "u8vec2") {
+    type_.SetType(DataType::kUint8);
+    type_.SetRowCount(2);
+  } else if (data == "u8vec3") {
+    type_.SetType(DataType::kUint8);
+    type_.SetRowCount(3);
+  } else if (data == "u8vec4") {
+    type_.SetType(DataType::kUint8);
+    type_.SetRowCount(4);
+  } else if (data == "i16vec2") {
+    type_.SetType(DataType::kInt16);
+    type_.SetRowCount(2);
+  } else if (data == "i16vec3") {
+    type_.SetType(DataType::kInt16);
+    type_.SetRowCount(3);
+  } else if (data == "i16vec4") {
+    type_.SetType(DataType::kInt16);
+    type_.SetRowCount(4);
+  } else if (data == "u16vec2") {
+    type_.SetType(DataType::kUint16);
+    type_.SetRowCount(2);
+  } else if (data == "u16vec3") {
+    type_.SetType(DataType::kUint16);
+    type_.SetRowCount(3);
+  } else if (data == "u16vec4") {
+    type_.SetType(DataType::kUint16);
+    type_.SetRowCount(4);
+  } else if (data == "i64vec2") {
+    type_.SetType(DataType::kInt64);
+    type_.SetRowCount(2);
+  } else if (data == "i64vec3") {
+    type_.SetType(DataType::kInt64);
+    type_.SetRowCount(3);
+  } else if (data == "i64vec4") {
+    type_.SetType(DataType::kInt64);
+    type_.SetRowCount(4);
+  } else if (data == "u64vec2") {
+    type_.SetType(DataType::kUint64);
+    type_.SetRowCount(2);
+  } else if (data == "u64vec3") {
+    type_.SetType(DataType::kUint64);
+    type_.SetRowCount(3);
+  } else if (data == "u64vec4") {
+    type_.SetType(DataType::kUint64);
+    type_.SetRowCount(4);
+  } else if (data == "mat2") {
+    type_.SetType(DataType::kFloat);
+    type_.SetColumnCount(2);
+    type_.SetRowCount(2);
+  } else if (data == "mat2x2") {
+    type_.SetType(DataType::kFloat);
+    type_.SetColumnCount(2);
+    type_.SetRowCount(2);
+  } else if (data == "mat2x3") {
+    type_.SetType(DataType::kFloat);
+    type_.SetColumnCount(2);
+    type_.SetRowCount(3);
+  } else if (data == "mat2x4") {
+    type_.SetType(DataType::kFloat);
+    type_.SetColumnCount(2);
+    type_.SetRowCount(4);
+  } else if (data == "mat3") {
+    type_.SetType(DataType::kFloat);
+    type_.SetColumnCount(3);
+    type_.SetRowCount(3);
+  } else if (data == "mat3x2") {
+    type_.SetType(DataType::kFloat);
+    type_.SetColumnCount(3);
+    type_.SetRowCount(2);
+  } else if (data == "mat3x3") {
+    type_.SetType(DataType::kFloat);
+    type_.SetColumnCount(3);
+    type_.SetRowCount(3);
+  } else if (data == "mat3x4") {
+    type_.SetType(DataType::kFloat);
+    type_.SetColumnCount(3);
+    type_.SetRowCount(4);
+  } else if (data == "mat4") {
+    type_.SetType(DataType::kFloat);
+    type_.SetColumnCount(4);
+    type_.SetRowCount(4);
+  } else if (data == "mat4x2") {
+    type_.SetType(DataType::kFloat);
+    type_.SetColumnCount(4);
+    type_.SetRowCount(2);
+  } else if (data == "mat4x3") {
+    type_.SetType(DataType::kFloat);
+    type_.SetColumnCount(4);
+    type_.SetRowCount(3);
+  } else if (data == "mat4x4") {
+    type_.SetType(DataType::kFloat);
+    type_.SetColumnCount(4);
+    type_.SetRowCount(4);
+  } else if (data == "dmat2") {
+    type_.SetType(DataType::kDouble);
+    type_.SetColumnCount(2);
+    type_.SetRowCount(2);
+  } else if (data == "dmat2x2") {
+    type_.SetType(DataType::kDouble);
+    type_.SetColumnCount(2);
+    type_.SetRowCount(2);
+  } else if (data == "dmat2x3") {
+    type_.SetType(DataType::kDouble);
+    type_.SetColumnCount(2);
+    type_.SetRowCount(3);
+  } else if (data == "dmat2x4") {
+    type_.SetType(DataType::kDouble);
+    type_.SetColumnCount(2);
+    type_.SetRowCount(4);
+  } else if (data == "dmat3") {
+    type_.SetType(DataType::kDouble);
+    type_.SetColumnCount(3);
+    type_.SetRowCount(3);
+  } else if (data == "dmat3x2") {
+    type_.SetType(DataType::kDouble);
+    type_.SetColumnCount(3);
+    type_.SetRowCount(2);
+  } else if (data == "dmat3x3") {
+    type_.SetType(DataType::kDouble);
+    type_.SetColumnCount(3);
+    type_.SetRowCount(3);
+  } else if (data == "dmat3x4") {
+    type_.SetType(DataType::kDouble);
+    type_.SetColumnCount(3);
+    type_.SetRowCount(4);
+  } else if (data == "dmat4") {
+    type_.SetType(DataType::kDouble);
+    type_.SetColumnCount(4);
+    type_.SetRowCount(4);
+  } else if (data == "dmat4x2") {
+    type_.SetType(DataType::kDouble);
+    type_.SetColumnCount(4);
+    type_.SetRowCount(2);
+  } else if (data == "dmat4x3") {
+    type_.SetType(DataType::kDouble);
+    type_.SetColumnCount(4);
+    type_.SetRowCount(3);
+  } else if (data == "dmat4x4") {
+    type_.SetType(DataType::kDouble);
+    type_.SetColumnCount(4);
+    type_.SetRowCount(4);
+  } else {
+    return Result("Invalid type provided: " + data);
+  }
+  return {};
+}
+
+}  // namespace vkscript
+}  // namespace amber
diff --git a/src/vkscript/datum_type_parser.h b/src/vkscript/datum_type_parser.h
new file mode 100644
index 0000000..4689e33
--- /dev/null
+++ b/src/vkscript/datum_type_parser.h
@@ -0,0 +1,39 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VKSCRIPT_DATUM_TYPE_PARSER_H_
+#define SRC_VKSCRIPT_DATUM_TYPE_PARSER_H_
+
+#include "amber/result.h"
+#include "src/datum_type.h"
+
+namespace amber {
+namespace vkscript {
+
+class DatumTypeParser {
+ public:
+  DatumTypeParser();
+  ~DatumTypeParser();
+
+  Result Parse(const std::string& data);
+  const DatumType& GetType() const { return type_; }
+
+ private:
+  DatumType type_;
+};
+
+}  // namespace vkscript
+}  // namespace amber
+
+#endif  // SRC_VKSCRIPT_DATUM_TYPE_PARSER_H_
diff --git a/src/vkscript/datum_type_parser_test.cc b/src/vkscript/datum_type_parser_test.cc
new file mode 100644
index 0000000..27b6f50
--- /dev/null
+++ b/src/vkscript/datum_type_parser_test.cc
@@ -0,0 +1,130 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vkscript/datum_type_parser.h"
+#include "gtest/gtest.h"
+
+namespace amber {
+namespace vkscript {
+
+using DatumTypeParserTest = testing::Test;
+
+TEST_F(DatumTypeParserTest, EmptyType) {
+  DatumTypeParser tp;
+  Result r = tp.Parse("");
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid type provided: ", r.Error());
+}
+
+TEST_F(DatumTypeParserTest, InvalidType) {
+  DatumTypeParser tp;
+  Result r = tp.Parse("INVALID");
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid type provided: INVALID", r.Error());
+}
+
+struct DatumTypeData {
+  const char* name;
+  DataType type;
+  uint32_t column_count;
+  uint32_t row_count;
+};
+using DatumTypeDataTest = testing::TestWithParam<DatumTypeData>;
+
+TEST_P(DatumTypeDataTest, Parser) {
+  const auto& test_data = GetParam();
+
+  DatumTypeParser tp;
+  Result r = tp.Parse(test_data.name);
+  const auto& t = tp.GetType();
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+  EXPECT_EQ(test_data.type, t.GetType());
+  EXPECT_EQ(test_data.column_count, t.ColumnCount());
+  EXPECT_EQ(test_data.row_count, t.RowCount());
+}
+
+INSTANTIATE_TEST_CASE_P(
+    DatumTypeParserTest1,
+    DatumTypeDataTest,
+    testing::Values(DatumTypeData{"int", DataType::kInt32, 1, 1},
+                    DatumTypeData{"uint", DataType::kUint32, 1, 1},
+                    DatumTypeData{"int8_t", DataType::kInt8, 1, 1},
+                    DatumTypeData{"uint8_t", DataType::kUint8, 1, 1},
+                    DatumTypeData{"int16_t", DataType::kInt16, 1, 1},
+                    DatumTypeData{"uint16_t", DataType::kUint16, 1, 1},
+                    DatumTypeData{"int64_t", DataType::kInt64, 1, 1},
+                    DatumTypeData{"uint64_t", DataType::kUint64, 1, 1},
+                    DatumTypeData{"float", DataType::kFloat, 1, 1},
+                    DatumTypeData{"double", DataType::kDouble, 1, 1},
+                    DatumTypeData{"vec2", DataType::kFloat, 1, 2},
+                    DatumTypeData{"vec3", DataType::kFloat, 1, 3},
+                    DatumTypeData{"vec4", DataType::kFloat, 1, 4},
+                    DatumTypeData{"dvec2", DataType::kDouble, 1, 2},
+                    DatumTypeData{"dvec3", DataType::kDouble, 1, 3},
+                    DatumTypeData{"dvec4", DataType::kDouble, 1, 4},
+                    DatumTypeData{"ivec2", DataType::kInt32, 1, 2},
+                    DatumTypeData{"ivec3", DataType::kInt32, 1, 3},
+                    DatumTypeData{"ivec4", DataType::kInt32, 1, 4},
+                    DatumTypeData{"uvec2", DataType::kUint32, 1, 2},
+                    DatumTypeData{"uvec3", DataType::kUint32, 1, 3},
+                    DatumTypeData{"uvec4", DataType::kUint32, 1, 4},
+                    DatumTypeData{"i8vec2", DataType::kInt8, 1, 2},
+                    DatumTypeData{"i8vec3", DataType::kInt8, 1, 3},
+                    DatumTypeData{"i8vec4", DataType::kInt8, 1, 4},
+                    DatumTypeData{"u8vec2", DataType::kUint8, 1, 2},
+                    DatumTypeData{"u8vec3", DataType::kUint8, 1, 3},
+                    DatumTypeData{"u8vec4", DataType::kUint8, 1, 4},
+                    DatumTypeData{"i16vec2", DataType::kInt16, 1, 2}), );
+
+INSTANTIATE_TEST_CASE_P(
+    DatumTypeParserTest2,
+    DatumTypeDataTest,
+    testing::Values(DatumTypeData{"i16vec3", DataType::kInt16, 1, 3},
+                    DatumTypeData{"i16vec4", DataType::kInt16, 1, 4},
+                    DatumTypeData{"u16vec2", DataType::kUint16, 1, 2},
+                    DatumTypeData{"u16vec3", DataType::kUint16, 1, 3},
+                    DatumTypeData{"u16vec4", DataType::kUint16, 1, 4},
+                    DatumTypeData{"i64vec2", DataType::kInt64, 1, 2},
+                    DatumTypeData{"i64vec3", DataType::kInt64, 1, 3},
+                    DatumTypeData{"i64vec4", DataType::kInt64, 1, 4},
+                    DatumTypeData{"u64vec2", DataType::kUint64, 1, 2},
+                    DatumTypeData{"u64vec3", DataType::kUint64, 1, 3},
+                    DatumTypeData{"u64vec4", DataType::kUint64, 1, 4},
+                    DatumTypeData{"mat2", DataType::kFloat, 2, 2},
+                    DatumTypeData{"mat2x2", DataType::kFloat, 2, 2},
+                    DatumTypeData{"mat2x3", DataType::kFloat, 2, 3},
+                    DatumTypeData{"mat2x4", DataType::kFloat, 2, 4},
+                    DatumTypeData{"mat3", DataType::kFloat, 3, 3},
+                    DatumTypeData{"mat3x2", DataType::kFloat, 3, 2},
+                    DatumTypeData{"mat3x3", DataType::kFloat, 3, 3},
+                    DatumTypeData{"mat3x4", DataType::kFloat, 3, 4},
+                    DatumTypeData{"mat4", DataType::kFloat, 4, 4},
+                    DatumTypeData{"mat4x2", DataType::kFloat, 4, 2},
+                    DatumTypeData{"mat4x3", DataType::kFloat, 4, 3},
+                    DatumTypeData{"mat4x4", DataType::kFloat, 4, 4},
+                    DatumTypeData{"dmat2", DataType::kDouble, 2, 2},
+                    DatumTypeData{"dmat2x2", DataType::kDouble, 2, 2},
+                    DatumTypeData{"dmat2x3", DataType::kDouble, 2, 3},
+                    DatumTypeData{"dmat2x4", DataType::kDouble, 2, 4},
+                    DatumTypeData{"dmat3", DataType::kDouble, 3, 3},
+                    DatumTypeData{"dmat3x2", DataType::kDouble, 3, 2},
+                    DatumTypeData{"dmat3x3", DataType::kDouble, 3, 3},
+                    DatumTypeData{"dmat3x4", DataType::kDouble, 3, 4},
+                    DatumTypeData{"dmat4", DataType::kDouble, 4, 4},
+                    DatumTypeData{"dmat4x2", DataType::kDouble, 4, 2},
+                    DatumTypeData{"dmat4x3", DataType::kDouble, 4, 3},
+                    DatumTypeData{"dmat4x4", DataType::kDouble, 4, 4}), );
+
+}  // namespace vkscript
+}  // namespace amber
diff --git a/src/vkscript/executor.cc b/src/vkscript/executor.cc
new file mode 100644
index 0000000..bd6bad0
--- /dev/null
+++ b/src/vkscript/executor.cc
@@ -0,0 +1,154 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vkscript/executor.h"
+
+#include <cassert>
+#include <vector>
+
+#include "src/engine.h"
+#include "src/vkscript/nodes.h"
+#include "src/vkscript/script.h"
+
+namespace amber {
+namespace vkscript {
+
+Executor::Executor() : amber::Executor() {}
+
+Executor::~Executor() = default;
+
+Result Executor::Execute(Engine* engine, const amber::Script* src_script) {
+  if (!src_script->IsVkScript())
+    return Result("VkScript Executor called with non-vkscript source");
+
+  const Script* script = ToVkScript(src_script);
+
+  // Process Requirement nodes
+  for (const auto& node : script->Nodes()) {
+    if (!node->IsRequire())
+      continue;
+
+    for (const auto& require : node->AsRequire()->Requirements()) {
+      Result r =
+          engine->AddRequirement(require.GetFeature(), require.GetFormat());
+      if (!r.IsSuccess())
+        return r;
+    }
+  }
+
+  // Process Shader nodes
+  PipelineType pipeline_type = PipelineType::kGraphics;
+  for (const auto& node : script->Nodes()) {
+    if (!node->IsShader())
+      continue;
+
+    const auto shader = node->AsShader();
+    Result r = engine->SetShader(shader->GetShaderType(), shader->GetData());
+    if (!r.IsSuccess())
+      return r;
+
+    if (shader->GetShaderType() == ShaderType::kCompute)
+      pipeline_type = PipelineType::kCompute;
+  }
+
+  // TODO(jaebaek): Support multiple pipelines.
+  Result r = engine->CreatePipeline(pipeline_type);
+  if (!r.IsSuccess())
+    return r;
+
+  // Process VertexData nodes
+  for (const auto& node : script->Nodes()) {
+    if (!node->IsVertexData())
+      continue;
+
+    const auto data = node->AsVertexData();
+    const auto& headers = data->GetHeaders();
+    const auto& rows = data->GetRows();
+    for (size_t i = 0; i < headers.size(); ++i) {
+      std::vector<Value> values;
+      for (const auto& row : rows) {
+        const auto& cell = row[i];
+        for (size_t z = 0; z < cell.size(); ++z)
+          values.push_back(cell.GetValue(z));
+      }
+
+      r = engine->SetBuffer(BufferType::kVertexData, headers[i].location,
+                            *(headers[i].format), values);
+      if (!r.IsSuccess())
+        return r;
+    }
+  }
+
+  // Process Indices nodes
+  for (const auto& node : script->Nodes()) {
+    if (!node->IsIndices())
+      continue;
+
+    std::vector<Value> values;
+    for (uint16_t index : node->AsIndices()->Indices()) {
+      values.push_back(Value());
+      values.back().SetIntValue(index);
+    }
+
+    r = engine->SetBuffer(BufferType::kIndices, 0, Format(), values);
+    if (!r.IsSuccess())
+      return r;
+  }
+
+  // Process Test nodes
+  for (const auto& node : script->Nodes()) {
+    if (!node->IsTest())
+      continue;
+
+    for (const auto& cmd : node->AsTest()->GetCommands()) {
+      if (cmd->IsClear()) {
+        r = engine->ExecuteClear(cmd->AsClear());
+      } else if (cmd->IsClearColor()) {
+        r = engine->ExecuteClearColor(cmd->AsClearColor());
+      } else if (cmd->IsClearDepth()) {
+        r = engine->ExecuteClearDepth(cmd->AsClearDepth());
+      } else if (cmd->IsClearStencil()) {
+        r = engine->ExecuteClearStencil(cmd->AsClearStencil());
+      } else if (cmd->IsDrawRect()) {
+        r = engine->ExecuteDrawRect(cmd->AsDrawRect());
+      } else if (cmd->IsDrawArrays()) {
+        r = engine->ExecuteDrawArrays(cmd->AsDrawArrays());
+      } else if (cmd->IsCompute()) {
+        r = engine->ExecuteCompute(cmd->AsCompute());
+      } else if (cmd->IsEntryPoint()) {
+        r = engine->ExecuteEntryPoint(cmd->AsEntryPoint());
+      } else if (cmd->IsPatchParameterVertices()) {
+        r = engine->ExecutePatchParameterVertices(
+            cmd->AsPatchParameterVertices());
+      } else if (cmd->IsProbe()) {
+        r = engine->ExecuteProbe(cmd->AsProbe());
+      } else if (cmd->IsProbeSSBO()) {
+        r = engine->ExecuteProbeSSBO(cmd->AsProbeSSBO());
+      } else if (cmd->IsBuffer()) {
+        r = engine->ExecuteBuffer(cmd->AsBuffer());
+      } else if (cmd->IsTolerance()) {
+        r = engine->ExecuteTolerance(cmd->AsTolerance());
+      } else {
+        return Result("Unknown command type");
+      }
+
+      if (!r.IsSuccess())
+        return r;
+    }
+  }
+  return {};
+}
+
+}  // namespace vkscript
+}  // namespace amber
diff --git a/src/vkscript/executor.h b/src/vkscript/executor.h
new file mode 100644
index 0000000..005ece5
--- /dev/null
+++ b/src/vkscript/executor.h
@@ -0,0 +1,35 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VKSCRIPT_EXECUTOR_H_
+#define SRC_VKSCRIPT_EXECUTOR_H_
+
+#include "amber/result.h"
+#include "src/executor.h"
+
+namespace amber {
+namespace vkscript {
+
+class Executor : public amber::Executor {
+ public:
+  Executor();
+  ~Executor() override;
+
+  Result Execute(Engine* engine, const amber::Script* script) override;
+};
+
+}  // namespace vkscript
+}  // namespace amber
+
+#endif  // SRC_VKSCRIPT_EXECUTOR_H_
diff --git a/src/vkscript/executor_test.cc b/src/vkscript/executor_test.cc
new file mode 100644
index 0000000..53af91e
--- /dev/null
+++ b/src/vkscript/executor_test.cc
@@ -0,0 +1,988 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vkscript/executor.h"
+#include "gtest/gtest.h"
+#include "src/engine.h"
+#include "src/make_unique.h"
+#include "src/vkscript/parser.h"
+
+namespace amber {
+namespace vkscript {
+namespace {
+
+class EngineStub : public Engine {
+ public:
+  struct Require {
+    Require(Feature feat, Format* fmt) : feature(feat), format(fmt) {}
+
+    Feature feature;
+    Format* format;
+  };
+
+  EngineStub() : Engine() {}
+  ~EngineStub() override = default;
+
+  // Engine
+  Result Initialize() override { return {}; }
+
+  Result InitializeWithDevice(void*) override { return {}; }
+
+  Result Shutdown() override { return {}; }
+
+  void FailRequirements() { fail_requirements_ = true; }
+  const std::vector<Require>& GetRequirements() const { return requirements_; }
+  Result AddRequirement(Feature feature, const Format* format) override {
+    if (fail_requirements_)
+      return Result("requirements failed");
+
+    requirements_.emplace_back(feature, const_cast<Format*>(format));
+    return {};
+  }
+  Result CreatePipeline(PipelineType) override { return {}; }
+
+  void FailShaderCommand() { fail_shader_command_ = true; }
+  const std::vector<ShaderType>& GetShaderTypesSeen() const {
+    return shaders_seen_;
+  }
+  Result SetShader(ShaderType type, const std::vector<uint32_t>&) override {
+    if (fail_shader_command_)
+      return Result("shader command failed");
+
+    shaders_seen_.push_back(type);
+    return {};
+  }
+
+  uint8_t GetBufferCallCount() const { return buffer_call_count_; }
+  BufferType GetBufferType(size_t idx) const { return buffer_types_[idx]; }
+  uint8_t GetBufferLocation(size_t idx) const { return buffer_locations_[idx]; }
+  Format* GetBufferFormat(size_t idx) { return &(buffer_formats_[idx]); }
+  const std::vector<Value>& GetBufferValues(size_t idx) const {
+    return buffer_values_[idx];
+  }
+  Result SetBuffer(BufferType type,
+                   uint8_t location,
+                   const Format& format,
+                   const std::vector<Value>& data) override {
+    ++buffer_call_count_;
+    buffer_types_.push_back(type);
+    buffer_locations_.push_back(location);
+    buffer_formats_.push_back(format);
+    buffer_values_.push_back(data);
+    return {};
+  }
+
+  void FailClearColorCommand() { fail_clear_color_command_ = true; }
+  bool DidClearColorCommand() { return did_clear_color_command_ = true; }
+  ClearColorCommand* GetLastClearColorCommand() { return last_clear_color_; }
+  Result ExecuteClearColor(const ClearColorCommand* cmd) override {
+    did_clear_color_command_ = true;
+
+    if (fail_clear_color_command_)
+      return Result("clear color command failed");
+
+    last_clear_color_ = const_cast<ClearColorCommand*>(cmd);
+    return {};
+  }
+
+  void FailClearStencilCommand() { fail_clear_stencil_command_ = true; }
+  bool DidClearStencilCommand() const { return did_clear_stencil_command_; }
+  Result ExecuteClearStencil(const ClearStencilCommand*) override {
+    did_clear_stencil_command_ = true;
+
+    if (fail_clear_stencil_command_)
+      return Result("clear stencil command failed");
+
+    return {};
+  }
+
+  void FailClearDepthCommand() { fail_clear_depth_command_ = true; }
+  bool DidClearDepthCommand() const { return did_clear_depth_command_; }
+  Result ExecuteClearDepth(const ClearDepthCommand*) override {
+    did_clear_depth_command_ = true;
+
+    if (fail_clear_depth_command_)
+      return Result("clear depth command failed");
+
+    return {};
+  }
+
+  void FailClearCommand() { fail_clear_command_ = true; }
+  bool DidClearCommand() const { return did_clear_command_; }
+  Result ExecuteClear(const ClearCommand*) override {
+    did_clear_command_ = true;
+
+    if (fail_clear_command_)
+      return Result("clear command failed");
+    return {};
+  }
+
+  void FailDrawRectCommand() { fail_draw_rect_command_ = true; }
+  bool DidDrawRectCommand() const { return did_draw_rect_command_; }
+  Result ExecuteDrawRect(const DrawRectCommand*) override {
+    did_draw_rect_command_ = true;
+
+    if (fail_draw_rect_command_)
+      return Result("draw rect command failed");
+    return {};
+  }
+
+  void FailDrawArraysCommand() { fail_draw_arrays_command_ = true; }
+  bool DidDrawArraysCommand() const { return did_draw_arrays_command_; }
+  Result ExecuteDrawArrays(const DrawArraysCommand*) override {
+    did_draw_arrays_command_ = true;
+
+    if (fail_draw_arrays_command_)
+      return Result("draw arrays command failed");
+    return {};
+  }
+
+  void FailComputeCommand() { fail_compute_command_ = true; }
+  bool DidComputeCommand() const { return did_compute_command_; }
+  Result ExecuteCompute(const ComputeCommand*) override {
+    did_compute_command_ = true;
+
+    if (fail_compute_command_)
+      return Result("compute command failed");
+    return {};
+  }
+
+  void FailEntryPointCommand() { fail_entry_point_command_ = true; }
+  bool DidEntryPointCommand() const { return did_entry_point_command_; }
+  Result ExecuteEntryPoint(const EntryPointCommand*) override {
+    did_entry_point_command_ = true;
+
+    if (fail_entry_point_command_)
+      return Result("entrypoint command failed");
+    return {};
+  }
+
+  void FailPatchParameterVerticesCommand() { fail_patch_command_ = true; }
+  bool DidPatchParameterVerticesCommand() const { return did_patch_command_; }
+  Result ExecutePatchParameterVertices(
+      const PatchParameterVerticesCommand*) override {
+    did_patch_command_ = true;
+
+    if (fail_patch_command_)
+      return Result("patch command failed");
+    return {};
+  }
+
+  void FailProbeCommand() { fail_probe_command_ = true; }
+  bool DidProbeCommand() const { return did_probe_commmand_; }
+  Result ExecuteProbe(const ProbeCommand*) override {
+    did_probe_commmand_ = true;
+
+    if (fail_probe_command_)
+      return Result("probe command failed");
+    return {};
+  }
+
+  void FailProbeSSBOCommand() { fail_probe_ssbo_command_ = true; }
+  bool DidProbeSSBOCommand() const { return did_probe_ssbo_command_; }
+  Result ExecuteProbeSSBO(const ProbeSSBOCommand*) override {
+    did_probe_ssbo_command_ = true;
+
+    if (fail_probe_ssbo_command_)
+      return Result("probe ssbo command failed");
+    return {};
+  }
+
+  void FailBufferCommand() { fail_buffer_command_ = true; }
+  bool DidBufferCommand() const { return did_buffer_command_; }
+  Result ExecuteBuffer(const BufferCommand*) override {
+    did_buffer_command_ = true;
+
+    if (fail_buffer_command_)
+      return Result("buffer command failed");
+    return {};
+  }
+
+  void FailToleranceCommand() { fail_tolerance_command_ = true; }
+  bool DidToleranceCommand() const { return did_tolerance_command_; }
+  Result ExecuteTolerance(const ToleranceCommand*) override {
+    did_tolerance_command_ = true;
+
+    if (fail_tolerance_command_)
+      return Result("tolerance command failed");
+    return {};
+  }
+
+ private:
+  bool fail_requirements_ = false;
+  bool fail_shader_command_ = false;
+  bool fail_clear_command_ = false;
+  bool fail_clear_color_command_ = false;
+  bool fail_clear_stencil_command_ = false;
+  bool fail_clear_depth_command_ = false;
+  bool fail_draw_rect_command_ = false;
+  bool fail_draw_arrays_command_ = false;
+  bool fail_compute_command_ = false;
+  bool fail_entry_point_command_ = false;
+  bool fail_patch_command_ = false;
+  bool fail_probe_command_ = false;
+  bool fail_probe_ssbo_command_ = false;
+  bool fail_buffer_command_ = false;
+  bool fail_tolerance_command_ = false;
+
+  bool did_clear_command_ = false;
+  bool did_clear_color_command_ = false;
+  bool did_clear_stencil_command_ = false;
+  bool did_clear_depth_command_ = false;
+  bool did_draw_rect_command_ = false;
+  bool did_draw_arrays_command_ = false;
+  bool did_compute_command_ = false;
+  bool did_entry_point_command_ = false;
+  bool did_patch_command_ = false;
+  bool did_probe_commmand_ = false;
+  bool did_probe_ssbo_command_ = false;
+  bool did_buffer_command_ = false;
+  bool did_tolerance_command_ = false;
+
+  uint8_t buffer_call_count_ = 0;
+  std::vector<uint8_t> buffer_locations_;
+  std::vector<BufferType> buffer_types_;
+  std::vector<Format> buffer_formats_;
+  std::vector<std::vector<Value>> buffer_values_;
+
+  std::vector<ShaderType> shaders_seen_;
+  std::vector<Require> requirements_;
+
+  ClearColorCommand* last_clear_color_ = nullptr;
+};
+
+class EngineCountingStub : public Engine {
+ public:
+  EngineCountingStub() : Engine() {}
+  ~EngineCountingStub() override = default;
+
+  // Engine
+  Result Initialize() override { return {}; }
+
+  Result InitializeWithDevice(void*) override { return {}; }
+
+  Result Shutdown() override { return {}; }
+
+  int32_t GetRequireStageIdx() const { return require_stage_; }
+  uint32_t GetRequireCount() const { return require_stage_count_; }
+  Result AddRequirement(Feature, const Format*) override {
+    ++require_stage_count_;
+    require_stage_ = stage_count_++;
+    return {};
+  }
+  Result CreatePipeline(PipelineType) override { return {}; }
+
+  int32_t GetShaderStageIdx() const { return shader_stage_; }
+  uint32_t GetShaderCount() const { return shader_stage_count_; }
+  Result SetShader(ShaderType, const std::vector<uint32_t>&) override {
+    ++shader_stage_count_;
+    shader_stage_ = stage_count_++;
+    return {};
+  }
+  Result SetBuffer(BufferType,
+                   uint8_t,
+                   const Format&,
+                   const std::vector<Value>&) override {
+    return {};
+  }
+
+  Result ExecuteClearColor(const ClearColorCommand*) override { return {}; }
+  Result ExecuteClearStencil(const ClearStencilCommand*) override { return {}; }
+  Result ExecuteClearDepth(const ClearDepthCommand*) override { return {}; }
+  Result ExecuteClear(const ClearCommand*) override { return {}; }
+  Result ExecuteDrawRect(const DrawRectCommand*) override { return {}; }
+  Result ExecuteDrawArrays(const DrawArraysCommand*) override { return {}; }
+  Result ExecuteCompute(const ComputeCommand*) override { return {}; }
+  Result ExecuteEntryPoint(const EntryPointCommand*) override { return {}; }
+  Result ExecutePatchParameterVertices(
+      const PatchParameterVerticesCommand*) override {
+    return {};
+  }
+  Result ExecuteProbe(const ProbeCommand*) override { return {}; }
+  Result ExecuteProbeSSBO(const ProbeSSBOCommand*) override { return {}; }
+  Result ExecuteBuffer(const BufferCommand*) override { return {}; }
+  Result ExecuteTolerance(const ToleranceCommand*) override { return {}; }
+
+ private:
+  int32_t stage_count_ = 0;
+  int32_t require_stage_ = -1;
+  int32_t shader_stage_ = -1;
+  uint32_t require_stage_count_ = 0;
+  uint32_t shader_stage_count_ = 0;
+};
+
+class VkScriptExecutorTest : public testing::Test {
+ public:
+  VkScriptExecutorTest() = default;
+  ~VkScriptExecutorTest() = default;
+
+  std::unique_ptr<Engine> MakeEngine() { return MakeUnique<EngineStub>(); }
+  std::unique_ptr<Engine> MakeCountingEngine() {
+    return MakeUnique<EngineCountingStub>();
+  }
+  EngineStub* ToStub(Engine* engine) {
+    return static_cast<EngineStub*>(engine);
+  }
+
+  EngineCountingStub* ToCountingStub(Engine* engine) {
+    return static_cast<EngineCountingStub*>(engine);
+  }
+};
+
+}  // namespace
+
+TEST_F(VkScriptExecutorTest, ExecutesRequirements) {
+  std::string input = R"(
+[require]
+robustBufferAccess
+logicOp)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_TRUE(r.IsSuccess());
+
+  auto requirements = ToStub(engine.get())->GetRequirements();
+  ASSERT_EQ(2U, requirements.size());
+  EXPECT_EQ(Feature::kRobustBufferAccess, requirements[0].feature);
+  EXPECT_EQ(Feature::kLogicOp, requirements[1].feature);
+}
+
+TEST_F(VkScriptExecutorTest, ExecutesAllRequirementsFirst) {
+  std::string input = R"(
+[require]
+robustBufferAccess
+logicOp
+[vertex shader passthrough]
+[require]
+framebuffer R32G32B32A32_SINT
+)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeCountingEngine();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_TRUE(r.IsSuccess());
+
+  auto count = ToCountingStub(engine.get());
+  // All requires run first (3 requirements), so they get in before shader
+  EXPECT_EQ(2U, count->GetRequireStageIdx());
+  EXPECT_EQ(3U, count->GetShaderStageIdx());
+  EXPECT_EQ(3U, count->GetRequireCount());
+  EXPECT_EQ(1U, count->GetShaderCount());
+}
+
+TEST_F(VkScriptExecutorTest, ExecuteRequirementsFailed) {
+  std::string input = R"(
+[require]
+robustBufferAccess
+logicOp)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+  ToStub(engine.get())->FailRequirements();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("requirements failed", r.Error());
+}
+
+TEST_F(VkScriptExecutorTest, ExecutesRequirementsWithFormat) {
+  std::string input = R"(
+[require]
+framebuffer R32G32B32A32_SINT)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_TRUE(r.IsSuccess());
+
+  auto requirements = ToStub(engine.get())->GetRequirements();
+  ASSERT_EQ(1U, requirements.size());
+  EXPECT_EQ(Feature::kFramebuffer, requirements[0].feature);
+  EXPECT_EQ(FormatType::kR32G32B32A32_SINT,
+            requirements[0].format->GetFormatType());
+}
+
+TEST_F(VkScriptExecutorTest, ExecutesShaders) {
+  std::string input = R"(
+[vertex shader passthrough]
+[fragment shader]
+#version 430
+void main() {}
+)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_TRUE(r.IsSuccess());
+
+  auto shader_types = ToStub(engine.get())->GetShaderTypesSeen();
+  ASSERT_EQ(2U, shader_types.size());
+  EXPECT_EQ(ShaderType::kVertex, shader_types[0]);
+  EXPECT_EQ(ShaderType::kFragment, shader_types[1]);
+}
+
+TEST_F(VkScriptExecutorTest, ShaderFailure) {
+  std::string input = R"(
+[vertex shader passthrough]
+[fragment shader]
+#version 430
+void main() {}
+)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+  ToStub(engine.get())->FailShaderCommand();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("shader command failed", r.Error());
+}
+
+TEST_F(VkScriptExecutorTest, ClearCommand) {
+  std::string input = R"(
+[test]
+clear)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_TRUE(r.IsSuccess());
+  EXPECT_TRUE(ToStub(engine.get())->DidClearCommand());
+}
+
+TEST_F(VkScriptExecutorTest, ClearCommandFailure) {
+  std::string input = R"(
+[test]
+clear)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+  ToStub(engine.get())->FailClearCommand();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("clear command failed", r.Error());
+}
+
+TEST_F(VkScriptExecutorTest, ClearColorCommand) {
+  std::string input = R"(
+[test]
+clear color 244 123 123 13)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_TRUE(r.IsSuccess());
+  ASSERT_TRUE(ToStub(engine.get())->DidClearColorCommand());
+
+  auto* cmd = ToStub(engine.get())->GetLastClearColorCommand();
+  ASSERT_TRUE(cmd != nullptr);
+  ASSERT_TRUE(cmd->IsClearColor());
+
+  EXPECT_EQ(244U, cmd->GetR());
+  EXPECT_EQ(123U, cmd->GetG());
+  EXPECT_EQ(123U, cmd->GetB());
+  EXPECT_EQ(13U, cmd->GetA());
+}
+
+TEST_F(VkScriptExecutorTest, ClearColorCommandFailure) {
+  std::string input = R"(
+[test]
+clear color 123 123 123 123)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+  ToStub(engine.get())->FailClearColorCommand();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("clear color command failed", r.Error());
+}
+
+TEST_F(VkScriptExecutorTest, ClearDepthCommand) {
+  std::string input = R"(
+[test]
+clear depth 24)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_TRUE(r.IsSuccess());
+  ASSERT_TRUE(ToStub(engine.get())->DidClearDepthCommand());
+}
+
+TEST_F(VkScriptExecutorTest, ClearDepthCommandFailure) {
+  std::string input = R"(
+[test]
+clear depth 24)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+  ToStub(engine.get())->FailClearDepthCommand();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("clear depth command failed", r.Error());
+}
+
+TEST_F(VkScriptExecutorTest, ClearStencilCommand) {
+  std::string input = R"(
+[test]
+clear stencil 24)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_TRUE(r.IsSuccess());
+  ASSERT_TRUE(ToStub(engine.get())->DidClearStencilCommand());
+}
+
+TEST_F(VkScriptExecutorTest, ClearStencilCommandFailure) {
+  std::string input = R"(
+[test]
+clear stencil 24)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+  ToStub(engine.get())->FailClearStencilCommand();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("clear stencil command failed", r.Error());
+}
+
+TEST_F(VkScriptExecutorTest, DrawRectCommand) {
+  std::string input = R"(
+[test]
+draw rect 2 4 10 20)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_TRUE(r.IsSuccess());
+  ASSERT_TRUE(ToStub(engine.get())->DidDrawRectCommand());
+}
+
+TEST_F(VkScriptExecutorTest, DrawRectCommandFailure) {
+  std::string input = R"(
+[test]
+draw rect 2 4 10 20)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+  ToStub(engine.get())->FailDrawRectCommand();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("draw rect command failed", r.Error());
+}
+
+TEST_F(VkScriptExecutorTest, DrawArraysCommand) {
+  std::string input = R"(
+[test]
+draw arrays TRIANGLE_LIST 0 0)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_TRUE(r.IsSuccess());
+  ASSERT_TRUE(ToStub(engine.get())->DidDrawArraysCommand());
+}
+
+TEST_F(VkScriptExecutorTest, DrawArraysCommandFailure) {
+  std::string input = R"(
+[test]
+draw arrays TRIANGLE_LIST 0 0)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+  ToStub(engine.get())->FailDrawArraysCommand();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("draw arrays command failed", r.Error());
+}
+
+TEST_F(VkScriptExecutorTest, ComputeCommand) {
+  std::string input = R"(
+[test]
+compute 2 3 4)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_TRUE(r.IsSuccess());
+  ASSERT_TRUE(ToStub(engine.get())->DidComputeCommand());
+}
+
+TEST_F(VkScriptExecutorTest, ComputeCommandFailure) {
+  std::string input = R"(
+[test]
+compute 2 3 4)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+  ToStub(engine.get())->FailComputeCommand();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("compute command failed", r.Error());
+}
+
+TEST_F(VkScriptExecutorTest, EntryPointCommand) {
+  std::string input = R"(
+[test]
+vertex entrypoint main)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_TRUE(r.IsSuccess());
+  ASSERT_TRUE(ToStub(engine.get())->DidEntryPointCommand());
+}
+
+TEST_F(VkScriptExecutorTest, EntryPointCommandFailure) {
+  std::string input = R"(
+[test]
+vertex entrypoint main)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+  ToStub(engine.get())->FailEntryPointCommand();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("entrypoint command failed", r.Error());
+}
+
+TEST_F(VkScriptExecutorTest, PatchParameterVerticesCommand) {
+  std::string input = R"(
+[test]
+patch parameter vertices 10)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_TRUE(r.IsSuccess());
+  ASSERT_TRUE(ToStub(engine.get())->DidPatchParameterVerticesCommand());
+}
+
+TEST_F(VkScriptExecutorTest, PatchParameterVerticesCommandFailure) {
+  std::string input = R"(
+[test]
+patch parameter vertices 10)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+  ToStub(engine.get())->FailPatchParameterVerticesCommand();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("patch command failed", r.Error());
+}
+
+TEST_F(VkScriptExecutorTest, ProbeCommand) {
+  std::string input = R"(
+[test]
+probe rect rgba 2 3 40 40 0.2 0.4 0.4 0.3)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_TRUE(r.IsSuccess());
+  ASSERT_TRUE(ToStub(engine.get())->DidProbeCommand());
+}
+
+TEST_F(VkScriptExecutorTest, ProbeCommandFailure) {
+  std::string input = R"(
+[test]
+probe rect rgba 2 3 40 40 0.2 0.4 0.4 0.3)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+  ToStub(engine.get())->FailProbeCommand();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("probe command failed", r.Error());
+}
+
+TEST_F(VkScriptExecutorTest, BufferCommand) {
+  std::string input = R"(
+[test]
+ssbo 0 24)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_TRUE(r.IsSuccess());
+  ASSERT_TRUE(ToStub(engine.get())->DidBufferCommand());
+}
+
+TEST_F(VkScriptExecutorTest, BufferCommandFailure) {
+  std::string input = R"(
+[test]
+ssbo 0 24)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+  ToStub(engine.get())->FailBufferCommand();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("buffer command failed", r.Error());
+}
+
+TEST_F(VkScriptExecutorTest, ToleranceCommand) {
+  std::string input = R"(
+[test]
+tolerance 2 4 5 8)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_TRUE(r.IsSuccess());
+  ASSERT_TRUE(ToStub(engine.get())->DidToleranceCommand());
+}
+
+TEST_F(VkScriptExecutorTest, ToleranceCommandFailure) {
+  std::string input = R"(
+[test]
+tolerance 2 3 4 5)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+  ToStub(engine.get())->FailToleranceCommand();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("tolerance command failed", r.Error());
+}
+
+TEST_F(VkScriptExecutorTest, ProbeSSBOCommand) {
+  std::string input = R"(
+[test]
+probe ssbo vec3 0 2 <= 2 3 4)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_TRUE(r.IsSuccess());
+  ASSERT_TRUE(ToStub(engine.get())->DidProbeSSBOCommand());
+}
+
+TEST_F(VkScriptExecutorTest, ProbeSSBOCommandFailure) {
+  std::string input = R"(
+[test]
+probe ssbo vec3 0 2 <= 2 3 4)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+
+  auto engine = MakeEngine();
+  ToStub(engine.get())->FailProbeSSBOCommand();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("probe ssbo command failed", r.Error());
+}
+
+TEST_F(VkScriptExecutorTest, VertexData) {
+  std::string input = R"(
+[vertex data]
+9/R32G32B32_SFLOAT  1/R8G8B8_UNORM
+-1    -1 0.25       255 128 64
+0.25  -1 0.25       255 0 0
+)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+  auto engine = MakeEngine();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_TRUE(r.IsSuccess());
+
+  auto stub = ToStub(engine.get());
+  ASSERT_EQ(2U, stub->GetBufferCallCount());
+
+  EXPECT_EQ(FormatType::kR32G32B32_SFLOAT,
+            stub->GetBufferFormat(0)->GetFormatType());
+  EXPECT_EQ(BufferType::kVertexData, stub->GetBufferType(0));
+  EXPECT_EQ(9U, stub->GetBufferLocation(0));
+
+  const auto& data1 = stub->GetBufferValues(0);
+  std::vector<float> results1 = {-1, -1, 0.25, 0.25, -1, 0.25};
+  ASSERT_EQ(results1.size(), data1.size());
+  for (size_t i = 0; i < results1.size(); ++i) {
+    ASSERT_TRUE(data1[i].IsFloat());
+    EXPECT_FLOAT_EQ(results1[i], data1[i].AsFloat());
+  }
+
+  EXPECT_EQ(FormatType::kR8G8B8_UNORM,
+            stub->GetBufferFormat(1)->GetFormatType());
+  EXPECT_EQ(BufferType::kVertexData, stub->GetBufferType(1));
+  EXPECT_EQ(1U, stub->GetBufferLocation(1));
+
+  const auto& data2 = stub->GetBufferValues(1);
+  std::vector<uint8_t> results2 = {255, 128, 64, 255, 0, 0};
+  ASSERT_EQ(results2.size(), data2.size());
+  for (size_t i = 0; i < results2.size(); ++i) {
+    ASSERT_TRUE(data2[i].IsInteger());
+    EXPECT_EQ(results2[i], data2[i].AsUint8());
+  }
+}
+
+TEST_F(VkScriptExecutorTest, IndexBuffer) {
+  std::string input = R"(
+[indices]
+1 2 3 4 5 6
+)";
+
+  Parser parser;
+  ASSERT_TRUE(parser.Parse(input).IsSuccess());
+  auto engine = MakeEngine();
+
+  Executor ex;
+  Result r = ex.Execute(engine.get(), parser.GetScript());
+  ASSERT_TRUE(r.IsSuccess());
+
+  auto stub = ToStub(engine.get());
+  ASSERT_EQ(1U, stub->GetBufferCallCount());
+
+  EXPECT_EQ(BufferType::kIndices, stub->GetBufferType(0));
+
+  const auto& data = stub->GetBufferValues(0);
+  std::vector<uint8_t> results = {1, 2, 3, 4, 5, 6};
+  ASSERT_EQ(results.size(), data.size());
+  for (size_t i = 0; i < results.size(); ++i) {
+    ASSERT_TRUE(data[i].IsInteger());
+    EXPECT_EQ(results[i], data[i].AsUint8());
+  }
+}
+
+}  // namespace vkscript
+}  // namespace amber
diff --git a/src/vkscript/format_parser.cc b/src/vkscript/format_parser.cc
new file mode 100644
index 0000000..57da26a
--- /dev/null
+++ b/src/vkscript/format_parser.cc
@@ -0,0 +1,505 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vkscript/format_parser.h"
+
+#include <cassert>
+#include <cstdlib>
+
+#include "src/make_unique.h"
+
+namespace amber {
+namespace vkscript {
+
+FormatParser::FormatParser() = default;
+
+FormatParser::~FormatParser() = default;
+
+std::unique_ptr<Format> FormatParser::Parse(const std::string& data) {
+  if (data.empty())
+    return nullptr;
+
+  auto fmt = MakeUnique<Format>();
+
+  // See if this is a custom glsl string format.
+  if (data.find('/', 0) != std::string::npos)
+    return ParseGlslFormat(data);
+
+  FormatType type = NameToType(data);
+  if (type == FormatType::kUnknown)
+    return nullptr;
+
+  fmt->SetFormatType(type);
+
+  size_t cur_pos = 0;
+  while (cur_pos < data.length()) {
+    size_t next_pos = data.find('_', cur_pos);
+    if (next_pos == std::string::npos)
+      next_pos = data.length();
+
+    ProcessChunk(fmt.get(), data.substr(cur_pos, next_pos - cur_pos).c_str());
+    cur_pos = next_pos + 1;
+  }
+
+  assert(pieces_.empty());
+
+  return fmt;
+}
+
+void FormatParser::AddPiece(FormatComponentType type, uint8_t bits) {
+  pieces_.emplace_back(type, bits);
+}
+
+void FormatParser::FlushPieces(Format* fmt, FormatMode mode) {
+  for (const auto& piece : pieces_)
+    fmt->AddComponent(piece.type, mode, piece.num_bits);
+
+  pieces_.clear();
+}
+
+// TODO(dsinclair): Remove asserts return something ...?
+void FormatParser::ProcessChunk(Format* fmt, const std::string& data) {
+  assert(data.size() > 0);
+
+  if (data[0] == 'P') {
+    if (data == "PACK8")
+      fmt->SetPackSize(8);
+    else if (data == "PACK16")
+      fmt->SetPackSize(16);
+    else if (data == "PACK32")
+      fmt->SetPackSize(32);
+    else
+      assert(false);
+
+    return;
+  }
+
+  if (data[0] == 'U') {
+    if (data == "UINT")
+      FlushPieces(fmt, FormatMode::kUInt);
+    else if (data == "UNORM")
+      FlushPieces(fmt, FormatMode::kUNorm);
+    else if (data == "UFLOAT")
+      FlushPieces(fmt, FormatMode::kUFloat);
+    else if (data == "USCALED")
+      FlushPieces(fmt, FormatMode::kUScaled);
+    else
+      assert(false);
+
+    return;
+  }
+
+  if (data[0] == 'S') {
+    if (data == "SINT")
+      FlushPieces(fmt, FormatMode::kSInt);
+    else if (data == "SNORM")
+      FlushPieces(fmt, FormatMode::kSNorm);
+    else if (data == "SSCALED")
+      FlushPieces(fmt, FormatMode::kSScaled);
+    else if (data == "SFLOAT")
+      FlushPieces(fmt, FormatMode::kSFloat);
+    else if (data == "SRGB")
+      FlushPieces(fmt, FormatMode::kSRGB);
+    else if (data == "S8")
+      AddPiece(FormatComponentType::kS, 8);
+    else
+      assert(false);
+
+    return;
+  }
+
+  size_t cur_pos = 0;
+  while (cur_pos < data.size()) {
+    FormatComponentType type = FormatComponentType::kA;
+    switch (data[cur_pos++]) {
+      case 'X':
+        type = FormatComponentType::kX;
+        break;
+      case 'D':
+        type = FormatComponentType::kD;
+        break;
+      case 'R':
+        type = FormatComponentType::kR;
+        break;
+      case 'G':
+        type = FormatComponentType::kG;
+        break;
+      case 'B':
+        type = FormatComponentType::kB;
+        break;
+      case 'A':
+        type = FormatComponentType::kA;
+        break;
+      default:
+        assert(false);
+    }
+
+    assert(cur_pos < data.size());
+
+    char* next_str;
+    const char* str = data.c_str() + cur_pos;
+
+    long val = std::strtol(str, &next_str, 10);
+    AddPiece(type, static_cast<uint8_t>(val));
+
+    cur_pos += static_cast<size_t>(next_str - str);
+  }
+}
+
+FormatType FormatParser::NameToType(const std::string& data) {
+  if (data == "A1R5G5B5_UNORM_PACK16")
+    return FormatType::kA1R5G5B5_UNORM_PACK16;
+  if (data == "A2B10G10R10_SINT_PACK32")
+    return FormatType::kA2B10G10R10_SINT_PACK32;
+  if (data == "A2B10G10R10_SNORM_PACK32")
+    return FormatType::kA2B10G10R10_SNORM_PACK32;
+  if (data == "A2B10G10R10_SSCALED_PACK32")
+    return FormatType::kA2B10G10R10_SSCALED_PACK32;
+  if (data == "A2B10G10R10_UINT_PACK32")
+    return FormatType::kA2B10G10R10_UINT_PACK32;
+  if (data == "A2B10G10R10_UNORM_PACK32")
+    return FormatType::kA2B10G10R10_UNORM_PACK32;
+  if (data == "A2B10G10R10_USCALED_PACK32")
+    return FormatType::kA2B10G10R10_USCALED_PACK32;
+  if (data == "A2R10G10B10_SINT_PACK32")
+    return FormatType::kA2R10G10B10_SINT_PACK32;
+  if (data == "A2R10G10B10_SNORM_PACK32")
+    return FormatType::kA2R10G10B10_SNORM_PACK32;
+  if (data == "A2R10G10B10_SSCALED_PACK32")
+    return FormatType::kA2R10G10B10_SSCALED_PACK32;
+  if (data == "A2R10G10B10_UINT_PACK32")
+    return FormatType::kA2R10G10B10_UINT_PACK32;
+  if (data == "A2R10G10B10_UNORM_PACK32")
+    return FormatType::kA2R10G10B10_UNORM_PACK32;
+  if (data == "A2R10G10B10_USCALED_PACK32")
+    return FormatType::kA2R10G10B10_USCALED_PACK32;
+  if (data == "A8B8G8R8_SINT_PACK32")
+    return FormatType::kA8B8G8R8_SINT_PACK32;
+  if (data == "A8B8G8R8_SNORM_PACK32")
+    return FormatType::kA8B8G8R8_SNORM_PACK32;
+  if (data == "A8B8G8R8_SRGB_PACK32")
+    return FormatType::kA8B8G8R8_SRGB_PACK32;
+  if (data == "A8B8G8R8_SSCALED_PACK32")
+    return FormatType::kA8B8G8R8_SSCALED_PACK32;
+  if (data == "A8B8G8R8_UINT_PACK32")
+    return FormatType::kA8B8G8R8_UINT_PACK32;
+  if (data == "A8B8G8R8_UNORM_PACK32")
+    return FormatType::kA8B8G8R8_UNORM_PACK32;
+  if (data == "A8B8G8R8_USCALED_PACK32")
+    return FormatType::kA8B8G8R8_USCALED_PACK32;
+  if (data == "B10G11R11_UFLOAT_PACK32")
+    return FormatType::kB10G11R11_UFLOAT_PACK32;
+  if (data == "B4G4R4A4_UNORM_PACK16")
+    return FormatType::kB4G4R4A4_UNORM_PACK16;
+  if (data == "B5G5R5A1_UNORM_PACK16")
+    return FormatType::kB5G5R5A1_UNORM_PACK16;
+  if (data == "B5G6R5_UNORM_PACK16")
+    return FormatType::kB5G6R5_UNORM_PACK16;
+  if (data == "B8G8R8A8_SINT")
+    return FormatType::kB8G8R8A8_SINT;
+  if (data == "B8G8R8A8_SNORM")
+    return FormatType::kB8G8R8A8_SNORM;
+  if (data == "B8G8R8A8_SRGB")
+    return FormatType::kB8G8R8A8_SRGB;
+  if (data == "B8G8R8A8_SSCALED")
+    return FormatType::kB8G8R8A8_SSCALED;
+  if (data == "B8G8R8A8_UINT")
+    return FormatType::kB8G8R8A8_UINT;
+  if (data == "B8G8R8A8_UNORM")
+    return FormatType::kB8G8R8A8_UNORM;
+  if (data == "B8G8R8A8_USCALED")
+    return FormatType::kB8G8R8A8_USCALED;
+  if (data == "B8G8R8_SINT")
+    return FormatType::kB8G8R8_SINT;
+  if (data == "B8G8R8_SNORM")
+    return FormatType::kB8G8R8_SNORM;
+  if (data == "B8G8R8_SRGB")
+    return FormatType::kB8G8R8_SRGB;
+  if (data == "B8G8R8_SSCALED")
+    return FormatType::kB8G8R8_SSCALED;
+  if (data == "B8G8R8_UINT")
+    return FormatType::kB8G8R8_UINT;
+  if (data == "B8G8R8_UNORM")
+    return FormatType::kB8G8R8_UNORM;
+  if (data == "B8G8R8_USCALED")
+    return FormatType::kB8G8R8_USCALED;
+  if (data == "D16_UNORM")
+    return FormatType::kD16_UNORM;
+  if (data == "D16_UNORM_S8_UINT")
+    return FormatType::kD16_UNORM_S8_UINT;
+  if (data == "D24_UNORM_S8_UINT")
+    return FormatType::kD24_UNORM_S8_UINT;
+  if (data == "D32_SFLOAT")
+    return FormatType::kD32_SFLOAT;
+  if (data == "D32_SFLOAT_S8_UINT")
+    return FormatType::kD32_SFLOAT_S8_UINT;
+  if (data == "R16G16B16A16_SFLOAT")
+    return FormatType::kR16G16B16A16_SFLOAT;
+  if (data == "R16G16B16A16_SINT")
+    return FormatType::kR16G16B16A16_SINT;
+  if (data == "R16G16B16A16_SNORM")
+    return FormatType::kR16G16B16A16_SNORM;
+  if (data == "R16G16B16A16_SSCALED")
+    return FormatType::kR16G16B16A16_SSCALED;
+  if (data == "R16G16B16A16_UINT")
+    return FormatType::kR16G16B16A16_UINT;
+  if (data == "R16G16B16A16_UNORM")
+    return FormatType::kR16G16B16A16_UNORM;
+  if (data == "R16G16B16A16_USCALED")
+    return FormatType::kR16G16B16A16_USCALED;
+  if (data == "R16G16B16_SFLOAT")
+    return FormatType::kR16G16B16_SFLOAT;
+  if (data == "R16G16B16_SINT")
+    return FormatType::kR16G16B16_SINT;
+  if (data == "R16G16B16_SNORM")
+    return FormatType::kR16G16B16_SNORM;
+  if (data == "R16G16B16_SSCALED")
+    return FormatType::kR16G16B16_SSCALED;
+  if (data == "R16G16B16_UINT")
+    return FormatType::kR16G16B16_UINT;
+  if (data == "R16G16B16_UNORM")
+    return FormatType::kR16G16B16_UNORM;
+  if (data == "R16G16B16_USCALED")
+    return FormatType::kR16G16B16_USCALED;
+  if (data == "R16G16_SFLOAT")
+    return FormatType::kR16G16_SFLOAT;
+  if (data == "R16G16_SINT")
+    return FormatType::kR16G16_SINT;
+  if (data == "R16G16_SNORM")
+    return FormatType::kR16G16_SNORM;
+  if (data == "R16G16_SSCALED")
+    return FormatType::kR16G16_SSCALED;
+  if (data == "R16G16_UINT")
+    return FormatType::kR16G16_UINT;
+  if (data == "R16G16_UNORM")
+    return FormatType::kR16G16_UNORM;
+  if (data == "R16G16_USCALED")
+    return FormatType::kR16G16_USCALED;
+  if (data == "R16_SFLOAT")
+    return FormatType::kR16_SFLOAT;
+  if (data == "R16_SINT")
+    return FormatType::kR16_SINT;
+  if (data == "R16_SNORM")
+    return FormatType::kR16_SNORM;
+  if (data == "R16_SSCALED")
+    return FormatType::kR16_SSCALED;
+  if (data == "R16_UINT")
+    return FormatType::kR16_UINT;
+  if (data == "R16_UNORM")
+    return FormatType::kR16_UNORM;
+  if (data == "R16_USCALED")
+    return FormatType::kR16_USCALED;
+  if (data == "R32G32B32A32_SFLOAT")
+    return FormatType::kR32G32B32A32_SFLOAT;
+  if (data == "R32G32B32A32_SINT")
+    return FormatType::kR32G32B32A32_SINT;
+  if (data == "R32G32B32A32_UINT")
+    return FormatType::kR32G32B32A32_UINT;
+  if (data == "R32G32B32_SFLOAT")
+    return FormatType::kR32G32B32_SFLOAT;
+  if (data == "R32G32B32_SINT")
+    return FormatType::kR32G32B32_SINT;
+  if (data == "R32G32B32_UINT")
+    return FormatType::kR32G32B32_UINT;
+  if (data == "R32G32_SFLOAT")
+    return FormatType::kR32G32_SFLOAT;
+  if (data == "R32G32_SINT")
+    return FormatType::kR32G32_SINT;
+  if (data == "R32G32_UINT")
+    return FormatType::kR32G32_UINT;
+  if (data == "R32_SFLOAT")
+    return FormatType::kR32_SFLOAT;
+  if (data == "R32_SINT")
+    return FormatType::kR32_SINT;
+  if (data == "R32_UINT")
+    return FormatType::kR32_UINT;
+  if (data == "R4G4B4A4_UNORM_PACK16")
+    return FormatType::kR4G4B4A4_UNORM_PACK16;
+  if (data == "R4G4_UNORM_PACK8")
+    return FormatType::kR4G4_UNORM_PACK8;
+  if (data == "R5G5B5A1_UNORM_PACK16")
+    return FormatType::kR5G5B5A1_UNORM_PACK16;
+  if (data == "R5G6B5_UNORM_PACK16")
+    return FormatType::kR5G6B5_UNORM_PACK16;
+  if (data == "R64G64B64A64_SFLOAT")
+    return FormatType::kR64G64B64A64_SFLOAT;
+  if (data == "R64G64B64A64_SINT")
+    return FormatType::kR64G64B64A64_SINT;
+  if (data == "R64G64B64A64_UINT")
+    return FormatType::kR64G64B64A64_UINT;
+  if (data == "R64G64B64_SFLOAT")
+    return FormatType::kR64G64B64_SFLOAT;
+  if (data == "R64G64B64_SINT")
+    return FormatType::kR64G64B64_SINT;
+  if (data == "R64G64B64_UINT")
+    return FormatType::kR64G64B64_UINT;
+  if (data == "R64G64_SFLOAT")
+    return FormatType::kR64G64_SFLOAT;
+  if (data == "R64G64_SINT")
+    return FormatType::kR64G64_SINT;
+  if (data == "R64G64_UINT")
+    return FormatType::kR64G64_UINT;
+  if (data == "R64_SFLOAT")
+    return FormatType::kR64_SFLOAT;
+  if (data == "R64_SINT")
+    return FormatType::kR64_SINT;
+  if (data == "R64_UINT")
+    return FormatType::kR64_UINT;
+  if (data == "R8G8B8A8_SINT")
+    return FormatType::kR8G8B8A8_SINT;
+  if (data == "R8G8B8A8_SNORM")
+    return FormatType::kR8G8B8A8_SNORM;
+  if (data == "R8G8B8A8_SRGB")
+    return FormatType::kR8G8B8A8_SRGB;
+  if (data == "R8G8B8A8_SSCALED")
+    return FormatType::kR8G8B8A8_SSCALED;
+  if (data == "R8G8B8A8_UINT")
+    return FormatType::kR8G8B8A8_UINT;
+  if (data == "R8G8B8A8_UNORM")
+    return FormatType::kR8G8B8A8_UNORM;
+  if (data == "R8G8B8A8_USCALED")
+    return FormatType::kR8G8B8A8_USCALED;
+  if (data == "R8G8B8_SINT")
+    return FormatType::kR8G8B8_SINT;
+  if (data == "R8G8B8_SNORM")
+    return FormatType::kR8G8B8_SNORM;
+  if (data == "R8G8B8_SRGB")
+    return FormatType::kR8G8B8_SRGB;
+  if (data == "R8G8B8_SSCALED")
+    return FormatType::kR8G8B8_SSCALED;
+  if (data == "R8G8B8_UINT")
+    return FormatType::kR8G8B8_UINT;
+  if (data == "R8G8B8_UNORM")
+    return FormatType::kR8G8B8_UNORM;
+  if (data == "R8G8B8_USCALED")
+    return FormatType::kR8G8B8_USCALED;
+  if (data == "R8G8_SINT")
+    return FormatType::kR8G8_SINT;
+  if (data == "R8G8_SNORM")
+    return FormatType::kR8G8_SNORM;
+  if (data == "R8G8_SRGB")
+    return FormatType::kR8G8_SRGB;
+  if (data == "R8G8_SSCALED")
+    return FormatType::kR8G8_SSCALED;
+  if (data == "R8G8_UINT")
+    return FormatType::kR8G8_UINT;
+  if (data == "R8G8_UNORM")
+    return FormatType::kR8G8_UNORM;
+  if (data == "R8G8_USCALED")
+    return FormatType::kR8G8_USCALED;
+  if (data == "R8_SINT")
+    return FormatType::kR8_SINT;
+  if (data == "R8_SNORM")
+    return FormatType::kR8_SNORM;
+  if (data == "R8_SRGB")
+    return FormatType::kR8_SRGB;
+  if (data == "R8_SSCALED")
+    return FormatType::kR8_SSCALED;
+  if (data == "R8_UINT")
+    return FormatType::kR8_UINT;
+  if (data == "R8_UNORM")
+    return FormatType::kR8_UNORM;
+  if (data == "R8_USCALED")
+    return FormatType::kR8_USCALED;
+  if (data == "S8_UINT")
+    return FormatType::kS8_UINT;
+  if (data == "X8_D24_UNORM_PACK32")
+    return FormatType::kX8_D24_UNORM_PACK32;
+
+  return FormatType::kUnknown;
+}
+
+std::unique_ptr<Format> FormatParser::ParseGlslFormat(const std::string& fmt) {
+  size_t pos = fmt.find('/');
+  std::string gl_type = fmt.substr(0, pos);
+  std::string glsl_type = fmt.substr(pos + 1);
+
+  uint8_t bits = 0;
+  FormatMode mode = FormatMode::kUNorm;
+
+  static const struct {
+    const char* name;
+    uint8_t bits;
+    bool is_signed;
+    bool is_int;
+  } types[] = {
+      {"byte", 8, true, true},     {"ubyte", 8, false, true},
+      {"short", 16, true, true},   {"ushort", 16, false, true},
+      {"int", 32, true, true},     {"uint", 32, false, true},
+      {"half", 16, true, false},   {"float", 32, true, false},
+      {"double", 64, true, false},
+  };
+  for (auto& type : types) {
+    if (gl_type == std::string(type.name)) {
+      if (type.is_int)
+        mode = type.is_signed ? FormatMode::kSInt : FormatMode::kUInt;
+      else
+        mode = FormatMode::kSFloat;
+
+      bits = type.bits;
+    }
+  }
+
+  // Failed to find gl type.
+  if (mode == FormatMode::kUNorm)
+    return nullptr;
+
+  if (fmt.length() < 4)
+    return nullptr;
+
+  int8_t num_components = 0;
+  if (glsl_type == "float" || glsl_type == "double" || glsl_type == "int" ||
+      glsl_type == "uint") {
+    num_components = 1;
+  } else if (glsl_type.substr(0, 3) == "vec") {
+    num_components =
+        static_cast<int8_t>(strtol(glsl_type.c_str() + 3, nullptr, 10));
+    if (num_components < 2)
+      return nullptr;
+  } else if ((glsl_type[0] == 'd' || glsl_type[0] == 'i' ||
+              glsl_type[0] == 'u') &&
+             glsl_type.substr(1, 3) == "vec") {
+    num_components =
+        static_cast<int8_t>(strtol(glsl_type.c_str() + 4, nullptr, 10));
+    if (num_components < 2)
+      return nullptr;
+  }
+  if (num_components > 4)
+    return nullptr;
+
+  std::string new_name = "R" + std::to_string(bits);
+  --num_components;
+
+  if (num_components-- > 0)
+    new_name += "G" + std::to_string(bits);
+  if (num_components-- > 0)
+    new_name += "B" + std::to_string(bits);
+  if (num_components-- > 0)
+    new_name += "A" + std::to_string(bits);
+
+  new_name += "_";
+  if (mode == FormatMode::kSInt)
+    new_name += "SINT";
+  else if (mode == FormatMode::kUInt)
+    new_name += "UINT";
+  else if (mode == FormatMode::kSFloat)
+    new_name += "SFLOAT";
+  else
+    return nullptr;
+
+  return Parse(new_name);
+}
+
+}  // namespace vkscript
+}  // namespace amber
diff --git a/src/vkscript/format_parser.h b/src/vkscript/format_parser.h
new file mode 100644
index 0000000..327ade3
--- /dev/null
+++ b/src/vkscript/format_parser.h
@@ -0,0 +1,56 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VKSCRIPT_FORMAT_PARSER_H_
+#define SRC_VKSCRIPT_FORMAT_PARSER_H_
+
+#include <memory>
+#include <string>
+
+#include "src/format.h"
+
+namespace amber {
+
+class Format;
+
+namespace vkscript {
+
+class FormatParser {
+ public:
+  FormatParser();
+  ~FormatParser();
+
+  std::unique_ptr<Format> Parse(const std::string& fmt);
+
+ private:
+  std::unique_ptr<Format> ParseGlslFormat(const std::string& fmt);
+  void ProcessChunk(Format*, const std::string&);
+  FormatType NameToType(const std::string& data);
+  void AddPiece(FormatComponentType type, uint8_t bits);
+  void FlushPieces(Format* fmt, FormatMode mode);
+
+  struct Pieces {
+    Pieces(FormatComponentType t, uint8_t bits) : type(t), num_bits(bits) {}
+
+    FormatComponentType type;
+    uint8_t num_bits;
+  };
+
+  std::vector<Pieces> pieces_;
+};
+
+}  // namespace vkscript
+}  // namespace amber
+
+#endif  // SRC_VKSCRIPT_FORMAT_PARSER_H_
diff --git a/src/vkscript/format_parser_test.cc b/src/vkscript/format_parser_test.cc
new file mode 100644
index 0000000..d5bee85
--- /dev/null
+++ b/src/vkscript/format_parser_test.cc
@@ -0,0 +1,1278 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vkscript/format_parser.h"
+#include "gtest/gtest.h"
+
+namespace amber {
+namespace vkscript {
+
+using FormatParserTest = testing::Test;
+
+TEST_F(FormatParserTest, Formats) {
+  struct {
+    const char* name;
+    FormatType type;
+    uint8_t pack_size;
+    uint8_t component_count;
+    struct {
+      FormatComponentType type;
+      FormatMode mode;
+      uint8_t num_bits;
+    } components[4];
+  } formats[] = {
+      {"A1R5G5B5_UNORM_PACK16",
+       FormatType::kA1R5G5B5_UNORM_PACK16,
+       16U,
+       4U,
+       {
+           {FormatComponentType::kA, FormatMode::kUNorm, 1},
+           {FormatComponentType::kR, FormatMode::kUNorm, 5},
+           {FormatComponentType::kG, FormatMode::kUNorm, 5},
+           {FormatComponentType::kB, FormatMode::kUNorm, 5},
+       }},
+      {"A2B10G10R10_SINT_PACK32",
+       FormatType::kA2B10G10R10_SINT_PACK32,
+       32U,
+       4U,
+       {
+           {FormatComponentType::kA, FormatMode::kSInt, 2},
+           {FormatComponentType::kB, FormatMode::kSInt, 10},
+           {FormatComponentType::kG, FormatMode::kSInt, 10},
+           {FormatComponentType::kR, FormatMode::kSInt, 10},
+       }},
+      {"A2B10G10R10_SNORM_PACK32",
+       FormatType::kA2B10G10R10_SNORM_PACK32,
+       32U,
+       4U,
+       {
+           {FormatComponentType::kA, FormatMode::kSNorm, 2},
+           {FormatComponentType::kB, FormatMode::kSNorm, 10},
+           {FormatComponentType::kG, FormatMode::kSNorm, 10},
+           {FormatComponentType::kR, FormatMode::kSNorm, 10},
+       }},
+      {"A2B10G10R10_SSCALED_PACK32",
+       FormatType::kA2B10G10R10_SSCALED_PACK32,
+       32U,
+       4U,
+       {
+           {FormatComponentType::kA, FormatMode::kSScaled, 2},
+           {FormatComponentType::kB, FormatMode::kSScaled, 10},
+           {FormatComponentType::kG, FormatMode::kSScaled, 10},
+           {FormatComponentType::kR, FormatMode::kSScaled, 10},
+       }},
+      {"A2B10G10R10_UINT_PACK32",
+       FormatType::kA2B10G10R10_UINT_PACK32,
+       32U,
+       4U,
+       {
+           {FormatComponentType::kA, FormatMode::kUInt, 2},
+           {FormatComponentType::kB, FormatMode::kUInt, 10},
+           {FormatComponentType::kG, FormatMode::kUInt, 10},
+           {FormatComponentType::kR, FormatMode::kUInt, 10},
+       }},
+      {"A2B10G10R10_UNORM_PACK32",
+       FormatType::kA2B10G10R10_UNORM_PACK32,
+       32U,
+       4U,
+       {
+           {FormatComponentType::kA, FormatMode::kUNorm, 2},
+           {FormatComponentType::kB, FormatMode::kUNorm, 10},
+           {FormatComponentType::kG, FormatMode::kUNorm, 10},
+           {FormatComponentType::kR, FormatMode::kUNorm, 10},
+       }},
+      {"A2B10G10R10_USCALED_PACK32",
+       FormatType::kA2B10G10R10_USCALED_PACK32,
+       32U,
+       4U,
+       {
+           {FormatComponentType::kA, FormatMode::kUScaled, 2},
+           {FormatComponentType::kB, FormatMode::kUScaled, 10},
+           {FormatComponentType::kG, FormatMode::kUScaled, 10},
+           {FormatComponentType::kR, FormatMode::kUScaled, 10},
+       }},
+      {"A2R10G10B10_SINT_PACK32",
+       FormatType::kA2R10G10B10_SINT_PACK32,
+       32U,
+       4U,
+       {
+           {FormatComponentType::kA, FormatMode::kSInt, 2},
+           {FormatComponentType::kR, FormatMode::kSInt, 10},
+           {FormatComponentType::kG, FormatMode::kSInt, 10},
+           {FormatComponentType::kB, FormatMode::kSInt, 10},
+       }},
+      {"A2R10G10B10_SNORM_PACK32",
+       FormatType::kA2R10G10B10_SNORM_PACK32,
+       32U,
+       4U,
+       {
+           {FormatComponentType::kA, FormatMode::kSNorm, 2},
+           {FormatComponentType::kR, FormatMode::kSNorm, 10},
+           {FormatComponentType::kG, FormatMode::kSNorm, 10},
+           {FormatComponentType::kB, FormatMode::kSNorm, 10},
+       }},
+      {"A2R10G10B10_SSCALED_PACK32",
+       FormatType::kA2R10G10B10_SSCALED_PACK32,
+       32U,
+       4U,
+       {
+           {FormatComponentType::kA, FormatMode::kSScaled, 2},
+           {FormatComponentType::kR, FormatMode::kSScaled, 10},
+           {FormatComponentType::kG, FormatMode::kSScaled, 10},
+           {FormatComponentType::kB, FormatMode::kSScaled, 10},
+       }},
+      {"A2R10G10B10_UINT_PACK32",
+       FormatType::kA2R10G10B10_UINT_PACK32,
+       32U,
+       4U,
+       {
+           {FormatComponentType::kA, FormatMode::kUInt, 2},
+           {FormatComponentType::kR, FormatMode::kUInt, 10},
+           {FormatComponentType::kG, FormatMode::kUInt, 10},
+           {FormatComponentType::kB, FormatMode::kUInt, 10},
+       }},
+      {"A2R10G10B10_UNORM_PACK32",
+       FormatType::kA2R10G10B10_UNORM_PACK32,
+       32U,
+       4U,
+       {
+           {FormatComponentType::kA, FormatMode::kUNorm, 2},
+           {FormatComponentType::kR, FormatMode::kUNorm, 10},
+           {FormatComponentType::kG, FormatMode::kUNorm, 10},
+           {FormatComponentType::kB, FormatMode::kUNorm, 10},
+       }},
+      {"A2R10G10B10_USCALED_PACK32",
+       FormatType::kA2R10G10B10_USCALED_PACK32,
+       32U,
+       4U,
+       {
+           {FormatComponentType::kA, FormatMode::kUScaled, 2},
+           {FormatComponentType::kR, FormatMode::kUScaled, 10},
+           {FormatComponentType::kG, FormatMode::kUScaled, 10},
+           {FormatComponentType::kB, FormatMode::kUScaled, 10},
+       }},
+      {"A8B8G8R8_SINT_PACK32",
+       FormatType::kA8B8G8R8_SINT_PACK32,
+       32U,
+       4U,
+       {
+           {FormatComponentType::kA, FormatMode::kSInt, 8},
+           {FormatComponentType::kB, FormatMode::kSInt, 8},
+           {FormatComponentType::kG, FormatMode::kSInt, 8},
+           {FormatComponentType::kR, FormatMode::kSInt, 8},
+       }},
+      {"A8B8G8R8_SNORM_PACK32",
+       FormatType::kA8B8G8R8_SNORM_PACK32,
+       32U,
+       4U,
+       {
+           {FormatComponentType::kA, FormatMode::kSNorm, 8},
+           {FormatComponentType::kB, FormatMode::kSNorm, 8},
+           {FormatComponentType::kG, FormatMode::kSNorm, 8},
+           {FormatComponentType::kR, FormatMode::kSNorm, 8},
+       }},
+      {"A8B8G8R8_SRGB_PACK32",
+       FormatType::kA8B8G8R8_SRGB_PACK32,
+       32U,
+       4U,
+       {
+           {FormatComponentType::kA, FormatMode::kSRGB, 8},
+           {FormatComponentType::kB, FormatMode::kSRGB, 8},
+           {FormatComponentType::kG, FormatMode::kSRGB, 8},
+           {FormatComponentType::kR, FormatMode::kSRGB, 8},
+       }},
+      {"A8B8G8R8_SSCALED_PACK32",
+       FormatType::kA8B8G8R8_SSCALED_PACK32,
+       32U,
+       4U,
+       {
+           {FormatComponentType::kA, FormatMode::kSScaled, 8},
+           {FormatComponentType::kB, FormatMode::kSScaled, 8},
+           {FormatComponentType::kG, FormatMode::kSScaled, 8},
+           {FormatComponentType::kR, FormatMode::kSScaled, 8},
+       }},
+      {"A8B8G8R8_UINT_PACK32",
+       FormatType::kA8B8G8R8_UINT_PACK32,
+       32U,
+       4U,
+       {
+           {FormatComponentType::kA, FormatMode::kUInt, 8},
+           {FormatComponentType::kB, FormatMode::kUInt, 8},
+           {FormatComponentType::kG, FormatMode::kUInt, 8},
+           {FormatComponentType::kR, FormatMode::kUInt, 8},
+       }},
+      {"A8B8G8R8_UNORM_PACK32",
+       FormatType::kA8B8G8R8_UNORM_PACK32,
+       32U,
+       4U,
+       {
+           {FormatComponentType::kA, FormatMode::kUNorm, 8},
+           {FormatComponentType::kB, FormatMode::kUNorm, 8},
+           {FormatComponentType::kG, FormatMode::kUNorm, 8},
+           {FormatComponentType::kR, FormatMode::kUNorm, 8},
+       }},
+      {"A8B8G8R8_USCALED_PACK32",
+       FormatType::kA8B8G8R8_USCALED_PACK32,
+       32U,
+       4U,
+       {
+           {FormatComponentType::kA, FormatMode::kUScaled, 8},
+           {FormatComponentType::kB, FormatMode::kUScaled, 8},
+           {FormatComponentType::kG, FormatMode::kUScaled, 8},
+           {FormatComponentType::kR, FormatMode::kUScaled, 8},
+       }},
+      {"B10G11R11_UFLOAT_PACK32",
+       FormatType::kB10G11R11_UFLOAT_PACK32,
+       32U,
+       3U,
+       {
+           {FormatComponentType::kB, FormatMode::kUFloat, 10},
+           {FormatComponentType::kG, FormatMode::kUFloat, 11},
+           {FormatComponentType::kR, FormatMode::kUFloat, 11},
+       }},
+      {"B4G4R4A4_UNORM_PACK16",
+       FormatType::kB4G4R4A4_UNORM_PACK16,
+       16U,
+       4U,
+       {
+           {FormatComponentType::kB, FormatMode::kUNorm, 4},
+           {FormatComponentType::kG, FormatMode::kUNorm, 4},
+           {FormatComponentType::kR, FormatMode::kUNorm, 4},
+           {FormatComponentType::kA, FormatMode::kUNorm, 4},
+       }},
+      {"B5G5R5A1_UNORM_PACK16",
+       FormatType::kB5G5R5A1_UNORM_PACK16,
+       16U,
+       4U,
+       {
+           {FormatComponentType::kB, FormatMode::kUNorm, 5},
+           {FormatComponentType::kG, FormatMode::kUNorm, 5},
+           {FormatComponentType::kR, FormatMode::kUNorm, 5},
+           {FormatComponentType::kA, FormatMode::kUNorm, 1},
+       }},
+      {"B5G6R5_UNORM_PACK16",
+       FormatType::kB5G6R5_UNORM_PACK16,
+       16U,
+       3U,
+       {
+           {FormatComponentType::kB, FormatMode::kUNorm, 5},
+           {FormatComponentType::kG, FormatMode::kUNorm, 6},
+           {FormatComponentType::kR, FormatMode::kUNorm, 5},
+       }},
+      {"B8G8R8A8_SINT",
+       FormatType::kB8G8R8A8_SINT,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kB, FormatMode::kSInt, 8},
+           {FormatComponentType::kG, FormatMode::kSInt, 8},
+           {FormatComponentType::kR, FormatMode::kSInt, 8},
+           {FormatComponentType::kA, FormatMode::kSInt, 8},
+       }},
+      {"B8G8R8A8_SNORM",
+       FormatType::kB8G8R8A8_SNORM,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kB, FormatMode::kSNorm, 8},
+           {FormatComponentType::kG, FormatMode::kSNorm, 8},
+           {FormatComponentType::kR, FormatMode::kSNorm, 8},
+           {FormatComponentType::kA, FormatMode::kSNorm, 8},
+       }},
+      {"B8G8R8A8_SRGB",
+       FormatType::kB8G8R8A8_SRGB,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kB, FormatMode::kSRGB, 8},
+           {FormatComponentType::kG, FormatMode::kSRGB, 8},
+           {FormatComponentType::kR, FormatMode::kSRGB, 8},
+           {FormatComponentType::kA, FormatMode::kSRGB, 8},
+       }},
+      {"B8G8R8A8_SSCALED",
+       FormatType::kB8G8R8A8_SSCALED,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kB, FormatMode::kSScaled, 8},
+           {FormatComponentType::kG, FormatMode::kSScaled, 8},
+           {FormatComponentType::kR, FormatMode::kSScaled, 8},
+           {FormatComponentType::kA, FormatMode::kSScaled, 8},
+       }},
+      {"B8G8R8A8_UINT",
+       FormatType::kB8G8R8A8_UINT,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kB, FormatMode::kUInt, 8},
+           {FormatComponentType::kG, FormatMode::kUInt, 8},
+           {FormatComponentType::kR, FormatMode::kUInt, 8},
+           {FormatComponentType::kA, FormatMode::kUInt, 8},
+       }},
+      {"B8G8R8A8_UNORM",
+       FormatType::kB8G8R8A8_UNORM,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kB, FormatMode::kUNorm, 8},
+           {FormatComponentType::kG, FormatMode::kUNorm, 8},
+           {FormatComponentType::kR, FormatMode::kUNorm, 8},
+           {FormatComponentType::kA, FormatMode::kUNorm, 8},
+       }},
+      {"B8G8R8A8_USCALED",
+       FormatType::kB8G8R8A8_USCALED,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kB, FormatMode::kUScaled, 8},
+           {FormatComponentType::kG, FormatMode::kUScaled, 8},
+           {FormatComponentType::kR, FormatMode::kUScaled, 8},
+           {FormatComponentType::kA, FormatMode::kUScaled, 8},
+       }},
+      {"B8G8R8_SINT",
+       FormatType::kB8G8R8_SINT,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kB, FormatMode::kSInt, 8},
+           {FormatComponentType::kG, FormatMode::kSInt, 8},
+           {FormatComponentType::kR, FormatMode::kSInt, 8},
+       }},
+      {"B8G8R8_SNORM",
+       FormatType::kB8G8R8_SNORM,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kB, FormatMode::kSNorm, 8},
+           {FormatComponentType::kG, FormatMode::kSNorm, 8},
+           {FormatComponentType::kR, FormatMode::kSNorm, 8},
+       }},
+      {"B8G8R8_SRGB",
+       FormatType::kB8G8R8_SRGB,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kB, FormatMode::kSRGB, 8},
+           {FormatComponentType::kG, FormatMode::kSRGB, 8},
+           {FormatComponentType::kR, FormatMode::kSRGB, 8},
+       }},
+      {"B8G8R8_SSCALED",
+       FormatType::kB8G8R8_SSCALED,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kB, FormatMode::kSScaled, 8},
+           {FormatComponentType::kG, FormatMode::kSScaled, 8},
+           {FormatComponentType::kR, FormatMode::kSScaled, 8},
+       }},
+      {"B8G8R8_UINT",
+       FormatType::kB8G8R8_UINT,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kB, FormatMode::kUInt, 8},
+           {FormatComponentType::kG, FormatMode::kUInt, 8},
+           {FormatComponentType::kR, FormatMode::kUInt, 8},
+       }},
+      {"B8G8R8_UNORM",
+       FormatType::kB8G8R8_UNORM,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kB, FormatMode::kUNorm, 8},
+           {FormatComponentType::kG, FormatMode::kUNorm, 8},
+           {FormatComponentType::kR, FormatMode::kUNorm, 8},
+       }},
+      {"B8G8R8_USCALED",
+       FormatType::kB8G8R8_USCALED,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kB, FormatMode::kUScaled, 8},
+           {FormatComponentType::kG, FormatMode::kUScaled, 8},
+           {FormatComponentType::kR, FormatMode::kUScaled, 8},
+       }},
+      {"D16_UNORM",
+       FormatType::kD16_UNORM,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kD, FormatMode::kUNorm, 16},
+       }},
+      {"D16_UNORM_S8_UINT",
+       FormatType::kD16_UNORM_S8_UINT,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kD, FormatMode::kUNorm, 16},
+           {FormatComponentType::kS, FormatMode::kUInt, 8},
+       }},
+      {"D24_UNORM_S8_UINT",
+       FormatType::kD24_UNORM_S8_UINT,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kD, FormatMode::kUNorm, 24},
+           {FormatComponentType::kS, FormatMode::kUInt, 8},
+       }},
+      {"D32_SFLOAT",
+       FormatType::kD32_SFLOAT,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kD, FormatMode::kSFloat, 32},
+       }},
+      {"D32_SFLOAT_S8_UINT",
+       FormatType::kD32_SFLOAT_S8_UINT,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kD, FormatMode::kSFloat, 32},
+           {FormatComponentType::kS, FormatMode::kUInt, 8},
+       }},
+      {"R16G16B16A16_SFLOAT",
+       FormatType::kR16G16B16A16_SFLOAT,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kSFloat, 16},
+           {FormatComponentType::kG, FormatMode::kSFloat, 16},
+           {FormatComponentType::kB, FormatMode::kSFloat, 16},
+           {FormatComponentType::kA, FormatMode::kSFloat, 16},
+       }},
+      {"R16G16B16A16_SINT",
+       FormatType::kR16G16B16A16_SINT,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kSInt, 16},
+           {FormatComponentType::kG, FormatMode::kSInt, 16},
+           {FormatComponentType::kB, FormatMode::kSInt, 16},
+           {FormatComponentType::kA, FormatMode::kSInt, 16},
+       }},
+      {"R16G16B16A16_SNORM",
+       FormatType::kR16G16B16A16_SNORM,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kSNorm, 16},
+           {FormatComponentType::kG, FormatMode::kSNorm, 16},
+           {FormatComponentType::kB, FormatMode::kSNorm, 16},
+           {FormatComponentType::kA, FormatMode::kSNorm, 16},
+       }},
+      {"R16G16B16A16_SSCALED",
+       FormatType::kR16G16B16A16_SSCALED,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kSScaled, 16},
+           {FormatComponentType::kG, FormatMode::kSScaled, 16},
+           {FormatComponentType::kB, FormatMode::kSScaled, 16},
+           {FormatComponentType::kA, FormatMode::kSScaled, 16},
+       }},
+      {"R16G16B16A16_UINT",
+       FormatType::kR16G16B16A16_UINT,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kUInt, 16},
+           {FormatComponentType::kG, FormatMode::kUInt, 16},
+           {FormatComponentType::kB, FormatMode::kUInt, 16},
+           {FormatComponentType::kA, FormatMode::kUInt, 16},
+       }},
+      {"R16G16B16A16_UNORM",
+       FormatType::kR16G16B16A16_UNORM,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kUNorm, 16},
+           {FormatComponentType::kG, FormatMode::kUNorm, 16},
+           {FormatComponentType::kB, FormatMode::kUNorm, 16},
+           {FormatComponentType::kA, FormatMode::kUNorm, 16},
+       }},
+      {"R16G16B16A16_USCALED",
+       FormatType::kR16G16B16A16_USCALED,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kUScaled, 16},
+           {FormatComponentType::kG, FormatMode::kUScaled, 16},
+           {FormatComponentType::kB, FormatMode::kUScaled, 16},
+           {FormatComponentType::kA, FormatMode::kUScaled, 16},
+       }},
+      {"R16G16B16_SFLOAT",
+       FormatType::kR16G16B16_SFLOAT,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kSFloat, 16},
+           {FormatComponentType::kG, FormatMode::kSFloat, 16},
+           {FormatComponentType::kB, FormatMode::kSFloat, 16},
+       }},
+      {"R16G16B16_SINT",
+       FormatType::kR16G16B16_SINT,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kSInt, 16},
+           {FormatComponentType::kG, FormatMode::kSInt, 16},
+           {FormatComponentType::kB, FormatMode::kSInt, 16},
+       }},
+      {"R16G16B16_SNORM",
+       FormatType::kR16G16B16_SNORM,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kSNorm, 16},
+           {FormatComponentType::kG, FormatMode::kSNorm, 16},
+           {FormatComponentType::kB, FormatMode::kSNorm, 16},
+       }},
+      {"R16G16B16_SSCALED",
+       FormatType::kR16G16B16_SSCALED,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kSScaled, 16},
+           {FormatComponentType::kG, FormatMode::kSScaled, 16},
+           {FormatComponentType::kB, FormatMode::kSScaled, 16},
+       }},
+      {"R16G16B16_UINT",
+       FormatType::kR16G16B16_UINT,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kUInt, 16},
+           {FormatComponentType::kG, FormatMode::kUInt, 16},
+           {FormatComponentType::kB, FormatMode::kUInt, 16},
+       }},
+      {"R16G16B16_UNORM",
+       FormatType::kR16G16B16_UNORM,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kUNorm, 16},
+           {FormatComponentType::kG, FormatMode::kUNorm, 16},
+           {FormatComponentType::kB, FormatMode::kUNorm, 16},
+       }},
+      {"R16G16B16_USCALED",
+       FormatType::kR16G16B16_USCALED,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kUScaled, 16},
+           {FormatComponentType::kG, FormatMode::kUScaled, 16},
+           {FormatComponentType::kB, FormatMode::kUScaled, 16},
+       }},
+      {"R16G16_SFLOAT",
+       FormatType::kR16G16_SFLOAT,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kSFloat, 16},
+           {FormatComponentType::kG, FormatMode::kSFloat, 16},
+       }},
+      {"R16G16_SINT",
+       FormatType::kR16G16_SINT,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kSInt, 16},
+           {FormatComponentType::kG, FormatMode::kSInt, 16},
+       }},
+      {"R16G16_SNORM",
+       FormatType::kR16G16_SNORM,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kSNorm, 16},
+           {FormatComponentType::kG, FormatMode::kSNorm, 16},
+       }},
+      {"R16G16_SSCALED",
+       FormatType::kR16G16_SSCALED,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kSScaled, 16},
+           {FormatComponentType::kG, FormatMode::kSScaled, 16},
+       }},
+      {"R16G16_UINT",
+       FormatType::kR16G16_UINT,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kUInt, 16},
+           {FormatComponentType::kG, FormatMode::kUInt, 16},
+       }},
+      {"R16G16_UNORM",
+       FormatType::kR16G16_UNORM,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kUNorm, 16},
+           {FormatComponentType::kG, FormatMode::kUNorm, 16},
+       }},
+      {"R16G16_USCALED",
+       FormatType::kR16G16_USCALED,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kUScaled, 16},
+           {FormatComponentType::kG, FormatMode::kUScaled, 16},
+       }},
+      {"R16_SFLOAT",
+       FormatType::kR16_SFLOAT,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kR, FormatMode::kSFloat, 16},
+       }},
+      {"R16_SINT",
+       FormatType::kR16_SINT,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kR, FormatMode::kSInt, 16},
+       }},
+      {"R16_SNORM",
+       FormatType::kR16_SNORM,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kR, FormatMode::kSNorm, 16},
+       }},
+      {"R16_SSCALED",
+       FormatType::kR16_SSCALED,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kR, FormatMode::kSScaled, 16},
+       }},
+      {"R16_UINT",
+       FormatType::kR16_UINT,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kR, FormatMode::kUInt, 16},
+       }},
+      {"R16_UNORM",
+       FormatType::kR16_UNORM,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kR, FormatMode::kUNorm, 16},
+       }},
+      {"R16_USCALED",
+       FormatType::kR16_USCALED,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kR, FormatMode::kUScaled, 16},
+       }},
+      {"R32G32B32A32_SFLOAT",
+       FormatType::kR32G32B32A32_SFLOAT,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kSFloat, 32},
+           {FormatComponentType::kG, FormatMode::kSFloat, 32},
+           {FormatComponentType::kB, FormatMode::kSFloat, 32},
+           {FormatComponentType::kA, FormatMode::kSFloat, 32},
+       }},
+      {"R32G32B32A32_SINT",
+       FormatType::kR32G32B32A32_SINT,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kSInt, 32},
+           {FormatComponentType::kG, FormatMode::kSInt, 32},
+           {FormatComponentType::kB, FormatMode::kSInt, 32},
+           {FormatComponentType::kA, FormatMode::kSInt, 32},
+       }},
+      {"R32G32B32A32_UINT",
+       FormatType::kR32G32B32A32_UINT,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kUInt, 32},
+           {FormatComponentType::kG, FormatMode::kUInt, 32},
+           {FormatComponentType::kB, FormatMode::kUInt, 32},
+           {FormatComponentType::kA, FormatMode::kUInt, 32},
+       }},
+      {"R32G32B32_SFLOAT",
+       FormatType::kR32G32B32_SFLOAT,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kSFloat, 32},
+           {FormatComponentType::kG, FormatMode::kSFloat, 32},
+           {FormatComponentType::kB, FormatMode::kSFloat, 32},
+       }},
+      {"R32G32B32_SINT",
+       FormatType::kR32G32B32_SINT,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kSInt, 32},
+           {FormatComponentType::kG, FormatMode::kSInt, 32},
+           {FormatComponentType::kB, FormatMode::kSInt, 32},
+       }},
+      {"R32G32B32_UINT",
+       FormatType::kR32G32B32_UINT,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kUInt, 32},
+           {FormatComponentType::kG, FormatMode::kUInt, 32},
+           {FormatComponentType::kB, FormatMode::kUInt, 32},
+       }},
+      {"R32G32_SFLOAT",
+       FormatType::kR32G32_SFLOAT,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kSFloat, 32},
+           {FormatComponentType::kG, FormatMode::kSFloat, 32},
+       }},
+      {"R32G32_SINT",
+       FormatType::kR32G32_SINT,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kSInt, 32},
+           {FormatComponentType::kG, FormatMode::kSInt, 32},
+       }},
+      {"R32G32_UINT",
+       FormatType::kR32G32_UINT,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kUInt, 32},
+           {FormatComponentType::kG, FormatMode::kUInt, 32},
+       }},
+      {"R32_SFLOAT",
+       FormatType::kR32_SFLOAT,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kR, FormatMode::kSFloat, 32},
+       }},
+      {"R32_SINT",
+       FormatType::kR32_SINT,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kR, FormatMode::kSInt, 32},
+       }},
+      {"R32_UINT",
+       FormatType::kR32_UINT,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kR, FormatMode::kUInt, 32},
+       }},
+      {"R4G4B4A4_UNORM_PACK16",
+       FormatType::kR4G4B4A4_UNORM_PACK16,
+       16U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kUNorm, 4},
+           {FormatComponentType::kG, FormatMode::kUNorm, 4},
+           {FormatComponentType::kB, FormatMode::kUNorm, 4},
+           {FormatComponentType::kA, FormatMode::kUNorm, 4},
+       }},
+      {"R4G4_UNORM_PACK8",
+       FormatType::kR4G4_UNORM_PACK8,
+       8U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kUNorm, 4},
+           {FormatComponentType::kG, FormatMode::kUNorm, 4},
+       }},
+      {"R5G5B5A1_UNORM_PACK16",
+       FormatType::kR5G5B5A1_UNORM_PACK16,
+       16U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kUNorm, 5},
+           {FormatComponentType::kG, FormatMode::kUNorm, 5},
+           {FormatComponentType::kB, FormatMode::kUNorm, 5},
+           {FormatComponentType::kA, FormatMode::kUNorm, 1},
+       }},
+      {"R5G6B5_UNORM_PACK16",
+       FormatType::kR5G6B5_UNORM_PACK16,
+       16U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kUNorm, 5},
+           {FormatComponentType::kG, FormatMode::kUNorm, 6},
+           {FormatComponentType::kB, FormatMode::kUNorm, 5},
+       }},
+      {"R64G64B64A64_SFLOAT",
+       FormatType::kR64G64B64A64_SFLOAT,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kSFloat, 64},
+           {FormatComponentType::kG, FormatMode::kSFloat, 64},
+           {FormatComponentType::kB, FormatMode::kSFloat, 64},
+           {FormatComponentType::kA, FormatMode::kSFloat, 64},
+       }},
+      {"R64G64B64A64_SINT",
+       FormatType::kR64G64B64A64_SINT,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kSInt, 64},
+           {FormatComponentType::kG, FormatMode::kSInt, 64},
+           {FormatComponentType::kB, FormatMode::kSInt, 64},
+           {FormatComponentType::kA, FormatMode::kSInt, 64},
+       }},
+      {"R64G64B64A64_UINT",
+       FormatType::kR64G64B64A64_UINT,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kUInt, 64},
+           {FormatComponentType::kG, FormatMode::kUInt, 64},
+           {FormatComponentType::kB, FormatMode::kUInt, 64},
+           {FormatComponentType::kA, FormatMode::kUInt, 64},
+       }},
+      {"R64G64B64_SFLOAT",
+       FormatType::kR64G64B64_SFLOAT,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kSFloat, 64},
+           {FormatComponentType::kG, FormatMode::kSFloat, 64},
+           {FormatComponentType::kB, FormatMode::kSFloat, 64},
+       }},
+      {"R64G64B64_SINT",
+       FormatType::kR64G64B64_SINT,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kSInt, 64},
+           {FormatComponentType::kG, FormatMode::kSInt, 64},
+           {FormatComponentType::kB, FormatMode::kSInt, 64},
+       }},
+      {"R64G64B64_UINT",
+       FormatType::kR64G64B64_UINT,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kUInt, 64},
+           {FormatComponentType::kG, FormatMode::kUInt, 64},
+           {FormatComponentType::kB, FormatMode::kUInt, 64},
+       }},
+      {"R64G64_SFLOAT",
+       FormatType::kR64G64_SFLOAT,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kSFloat, 64},
+           {FormatComponentType::kG, FormatMode::kSFloat, 64},
+       }},
+      {"R64G64_SINT",
+       FormatType::kR64G64_SINT,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kSInt, 64},
+           {FormatComponentType::kG, FormatMode::kSInt, 64},
+       }},
+      {"R64G64_UINT",
+       FormatType::kR64G64_UINT,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kUInt, 64},
+           {FormatComponentType::kG, FormatMode::kUInt, 64},
+       }},
+      {"R64_SFLOAT",
+       FormatType::kR64_SFLOAT,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kR, FormatMode::kSFloat, 64},
+       }},
+      {"R64_SINT",
+       FormatType::kR64_SINT,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kR, FormatMode::kSInt, 64},
+       }},
+      {"R64_UINT",
+       FormatType::kR64_UINT,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kR, FormatMode::kUInt, 64},
+       }},
+      {"R8G8B8A8_SINT",
+       FormatType::kR8G8B8A8_SINT,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kSInt, 8},
+           {FormatComponentType::kG, FormatMode::kSInt, 8},
+           {FormatComponentType::kB, FormatMode::kSInt, 8},
+           {FormatComponentType::kA, FormatMode::kSInt, 8},
+       }},
+      {"R8G8B8A8_SNORM",
+       FormatType::kR8G8B8A8_SNORM,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kSNorm, 8},
+           {FormatComponentType::kG, FormatMode::kSNorm, 8},
+           {FormatComponentType::kB, FormatMode::kSNorm, 8},
+           {FormatComponentType::kA, FormatMode::kSNorm, 8},
+       }},
+      {"R8G8B8A8_SRGB",
+       FormatType::kR8G8B8A8_SRGB,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kSRGB, 8},
+           {FormatComponentType::kG, FormatMode::kSRGB, 8},
+           {FormatComponentType::kB, FormatMode::kSRGB, 8},
+           {FormatComponentType::kA, FormatMode::kSRGB, 8},
+       }},
+      {"R8G8B8A8_SSCALED",
+       FormatType::kR8G8B8A8_SSCALED,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kSScaled, 8},
+           {FormatComponentType::kG, FormatMode::kSScaled, 8},
+           {FormatComponentType::kB, FormatMode::kSScaled, 8},
+           {FormatComponentType::kA, FormatMode::kSScaled, 8},
+       }},
+      {"R8G8B8A8_UINT",
+       FormatType::kR8G8B8A8_UINT,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kUInt, 8},
+           {FormatComponentType::kG, FormatMode::kUInt, 8},
+           {FormatComponentType::kB, FormatMode::kUInt, 8},
+           {FormatComponentType::kA, FormatMode::kUInt, 8},
+       }},
+      {"R8G8B8A8_UNORM",
+       FormatType::kR8G8B8A8_UNORM,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kUNorm, 8},
+           {FormatComponentType::kG, FormatMode::kUNorm, 8},
+           {FormatComponentType::kB, FormatMode::kUNorm, 8},
+           {FormatComponentType::kA, FormatMode::kUNorm, 8},
+       }},
+      {"R8G8B8A8_USCALED",
+       FormatType::kR8G8B8A8_USCALED,
+       0U,
+       4U,
+       {
+           {FormatComponentType::kR, FormatMode::kUScaled, 8},
+           {FormatComponentType::kG, FormatMode::kUScaled, 8},
+           {FormatComponentType::kB, FormatMode::kUScaled, 8},
+           {FormatComponentType::kA, FormatMode::kUScaled, 8},
+       }},
+      {"R8G8B8_SINT",
+       FormatType::kR8G8B8_SINT,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kSInt, 8},
+           {FormatComponentType::kG, FormatMode::kSInt, 8},
+           {FormatComponentType::kB, FormatMode::kSInt, 8},
+       }},
+      {"R8G8B8_SNORM",
+       FormatType::kR8G8B8_SNORM,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kSNorm, 8},
+           {FormatComponentType::kG, FormatMode::kSNorm, 8},
+           {FormatComponentType::kB, FormatMode::kSNorm, 8},
+       }},
+      {"R8G8B8_SRGB",
+       FormatType::kR8G8B8_SRGB,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kSRGB, 8},
+           {FormatComponentType::kG, FormatMode::kSRGB, 8},
+           {FormatComponentType::kB, FormatMode::kSRGB, 8},
+       }},
+      {"R8G8B8_SSCALED",
+       FormatType::kR8G8B8_SSCALED,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kSScaled, 8},
+           {FormatComponentType::kG, FormatMode::kSScaled, 8},
+           {FormatComponentType::kB, FormatMode::kSScaled, 8},
+       }},
+      {"R8G8B8_UINT",
+       FormatType::kR8G8B8_UINT,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kUInt, 8},
+           {FormatComponentType::kG, FormatMode::kUInt, 8},
+           {FormatComponentType::kB, FormatMode::kUInt, 8},
+       }},
+      {"R8G8B8_UNORM",
+       FormatType::kR8G8B8_UNORM,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kUNorm, 8},
+           {FormatComponentType::kG, FormatMode::kUNorm, 8},
+           {FormatComponentType::kB, FormatMode::kUNorm, 8},
+       }},
+      {"R8G8B8_USCALED",
+       FormatType::kR8G8B8_USCALED,
+       0U,
+       3U,
+       {
+           {FormatComponentType::kR, FormatMode::kUScaled, 8},
+           {FormatComponentType::kG, FormatMode::kUScaled, 8},
+           {FormatComponentType::kB, FormatMode::kUScaled, 8},
+       }},
+      {"R8G8_SINT",
+       FormatType::kR8G8_SINT,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kSInt, 8},
+           {FormatComponentType::kG, FormatMode::kSInt, 8},
+       }},
+      {"R8G8_SNORM",
+       FormatType::kR8G8_SNORM,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kSNorm, 8},
+           {FormatComponentType::kG, FormatMode::kSNorm, 8},
+       }},
+      {"R8G8_SRGB",
+       FormatType::kR8G8_SRGB,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kSRGB, 8},
+           {FormatComponentType::kG, FormatMode::kSRGB, 8},
+       }},
+      {"R8G8_SSCALED",
+       FormatType::kR8G8_SSCALED,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kSScaled, 8},
+           {FormatComponentType::kG, FormatMode::kSScaled, 8},
+       }},
+      {"R8G8_UINT",
+       FormatType::kR8G8_UINT,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kUInt, 8},
+           {FormatComponentType::kG, FormatMode::kUInt, 8},
+       }},
+      {"R8G8_UNORM",
+       FormatType::kR8G8_UNORM,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kUNorm, 8},
+           {FormatComponentType::kG, FormatMode::kUNorm, 8},
+       }},
+      {"R8G8_USCALED",
+       FormatType::kR8G8_USCALED,
+       0U,
+       2U,
+       {
+           {FormatComponentType::kR, FormatMode::kUScaled, 8},
+           {FormatComponentType::kG, FormatMode::kUScaled, 8},
+       }},
+      {"R8_SINT",
+       FormatType::kR8_SINT,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kR, FormatMode::kSInt, 8},
+       }},
+      {"R8_SNORM",
+       FormatType::kR8_SNORM,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kR, FormatMode::kSNorm, 8},
+       }},
+      {"R8_SRGB",
+       FormatType::kR8_SRGB,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kR, FormatMode::kSRGB, 8},
+       }},
+      {"R8_SSCALED",
+       FormatType::kR8_SSCALED,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kR, FormatMode::kSScaled, 8},
+       }},
+      {"R8_UINT",
+       FormatType::kR8_UINT,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kR, FormatMode::kUInt, 8},
+       }},
+      {"R8_UNORM",
+       FormatType::kR8_UNORM,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kR, FormatMode::kUNorm, 8},
+       }},
+      {"R8_USCALED",
+       FormatType::kR8_USCALED,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kR, FormatMode::kUScaled, 8},
+       }},
+      {"S8_UINT",
+       FormatType::kS8_UINT,
+       0U,
+       1U,
+       {
+           {FormatComponentType::kS, FormatMode::kUInt, 8},
+       }},
+      {"X8_D24_UNORM_PACK32",
+       FormatType::kX8_D24_UNORM_PACK32,
+       32U,
+       2U,
+       {
+           {FormatComponentType::kX, FormatMode::kUNorm, 8},
+           {FormatComponentType::kD, FormatMode::kUNorm, 24},
+       }},
+  };
+
+  for (const auto& fmt : formats) {
+    FormatParser parser;
+    auto format = parser.Parse(fmt.name);
+
+    ASSERT_TRUE(format != nullptr) << fmt.name;
+    EXPECT_EQ(fmt.type, format->GetFormatType()) << fmt.name;
+    EXPECT_EQ(fmt.pack_size, format->GetPackSize()) << fmt.name;
+
+    auto& comps = format->GetComponents();
+    ASSERT_EQ(fmt.component_count, comps.size());
+
+    for (size_t i = 0; i < fmt.component_count; ++i) {
+      EXPECT_EQ(fmt.components[i].type, comps[i].type) << fmt.name;
+      EXPECT_EQ(fmt.components[i].mode, comps[i].mode) << fmt.name;
+      EXPECT_EQ(fmt.components[i].num_bits, comps[i].num_bits) << fmt.name;
+    }
+  }
+}
+
+TEST_F(FormatParserTest, InvalidFormat) {
+  FormatParser parser;
+  auto format = parser.Parse("BLAH_BLAH_BLAH");
+  EXPECT_TRUE(format == nullptr);
+}
+
+TEST_F(FormatParserTest, EmptyFormat) {
+  FormatParser parser;
+  auto format = parser.Parse("");
+  EXPECT_TRUE(format == nullptr);
+}
+
+TEST_F(FormatParserTest, GlslString) {
+  FormatParser parser;
+  auto format = parser.Parse("float/vec3");
+  ASSERT_TRUE(format != nullptr);
+
+  EXPECT_EQ(FormatType::kR32G32B32_SFLOAT, format->GetFormatType());
+  EXPECT_EQ(static_cast<size_t>(0U), format->GetPackSize());
+
+  auto& comps = format->GetComponents();
+  ASSERT_EQ(3U, comps.size());
+
+  EXPECT_EQ(FormatComponentType::kR, comps[0].type);
+  EXPECT_EQ(FormatMode::kSFloat, comps[0].mode);
+  EXPECT_EQ(32U, comps[0].num_bits);
+  EXPECT_EQ(FormatComponentType::kG, comps[1].type);
+  EXPECT_EQ(FormatMode::kSFloat, comps[1].mode);
+  EXPECT_EQ(32U, comps[1].num_bits);
+  EXPECT_EQ(FormatComponentType::kB, comps[2].type);
+  EXPECT_EQ(FormatMode::kSFloat, comps[2].mode);
+  EXPECT_EQ(32U, comps[2].num_bits);
+}
+
+TEST_F(FormatParserTest, GlslStrings) {
+  struct {
+    const char* name;
+    FormatType type;
+  } strs[] = {
+      {"float/vec4", FormatType::kR32G32B32A32_SFLOAT},
+      {"float/ivec3", FormatType::kR32G32B32_SFLOAT},
+      {"float/dvec2", FormatType::kR32G32_SFLOAT},
+      {"float/uvec2", FormatType::kR32G32_SFLOAT},
+      {"float/float", FormatType::kR32_SFLOAT},
+      {"double/double", FormatType::kR64_SFLOAT},
+      {"half/float", FormatType::kR16_SFLOAT},
+      {"byte/int", FormatType::kR8_SINT},
+      {"ubyte/uint", FormatType::kR8_UINT},
+      {"short/int", FormatType::kR16_SINT},
+      {"ushort/uint", FormatType::kR16_UINT},
+      {"int/int", FormatType::kR32_SINT},
+      {"uint/uint", FormatType::kR32_UINT},
+  };
+
+  for (const auto& str : strs) {
+    FormatParser parser;
+    auto format = parser.Parse(str.name);
+    ASSERT_FALSE(format == nullptr);
+
+    EXPECT_EQ(str.type, format->GetFormatType()) << str.name;
+  }
+}
+
+TEST_F(FormatParserTest, GlslStringInvalid) {
+  struct {
+    const char* name;
+  } strs[] = {
+      {"flot/vec3"},
+      {"float/vec1"},
+      {"float/vec22"},
+      {"float/dvec0"},
+  };
+
+  for (const auto& str : strs) {
+    FormatParser parser;
+    auto format = parser.Parse(str.name);
+    EXPECT_TRUE(format == nullptr);
+  }
+}
+
+}  // namespace vkscript
+}  // namespace amber
diff --git a/src/vkscript/nodes.cc b/src/vkscript/nodes.cc
new file mode 100644
index 0000000..3386011
--- /dev/null
+++ b/src/vkscript/nodes.cc
@@ -0,0 +1,95 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vkscript/nodes.h"
+
+#include <cassert>
+
+namespace amber {
+namespace vkscript {
+
+Node::Node(NodeType type) : node_type_(type) {}
+
+Node::~Node() = default;
+
+IndicesNode* Node::AsIndices() {
+  return static_cast<IndicesNode*>(this);
+}
+
+ShaderNode* Node::AsShader() {
+  return static_cast<ShaderNode*>(this);
+}
+
+RequireNode* Node::AsRequire() {
+  return static_cast<RequireNode*>(this);
+}
+
+TestNode* Node::AsTest() {
+  return static_cast<TestNode*>(this);
+}
+
+VertexDataNode* Node::AsVertexData() {
+  return static_cast<VertexDataNode*>(this);
+}
+
+ShaderNode::ShaderNode(ShaderType type, std::vector<uint32_t> shader)
+    : Node(NodeType::kShader), type_(type), shader_(std::move(shader)) {}
+
+ShaderNode::~ShaderNode() = default;
+
+RequireNode::RequireNode() : Node(NodeType::kRequire) {}
+
+RequireNode::~RequireNode() = default;
+
+RequireNode::Requirement::Requirement(Feature feature) : feature_(feature) {}
+
+RequireNode::Requirement::Requirement(Feature feature,
+                                      std::unique_ptr<Format> format)
+    : feature_(feature), format_(std::move(format)) {}
+
+RequireNode::Requirement::Requirement(Requirement&&) = default;
+
+RequireNode::Requirement::~Requirement() = default;
+
+void RequireNode::AddRequirement(Feature feature) {
+  requirements_.emplace_back(feature);
+}
+
+void RequireNode::AddRequirement(Feature feature,
+                                 std::unique_ptr<Format> format) {
+  requirements_.emplace_back(feature, std::move(format));
+}
+
+IndicesNode::IndicesNode(const std::vector<uint16_t>& indices)
+    : Node(NodeType::kIndices), indices_(indices) {}
+
+IndicesNode::~IndicesNode() = default;
+
+TestNode::TestNode(std::vector<std::unique_ptr<Command>> cmds)
+    : Node(NodeType::kTest), commands_(std::move(cmds)) {}
+
+TestNode::~TestNode() = default;
+
+VertexDataNode::VertexDataNode() : Node(NodeType::kVertexData) {}
+
+VertexDataNode::~VertexDataNode() = default;
+
+VertexDataNode::Cell::Cell() = default;
+
+VertexDataNode::Cell::Cell(const VertexDataNode::Cell&) = default;
+
+VertexDataNode::Cell::~Cell() = default;
+
+}  // namespace vkscript
+}  // namespace amber
diff --git a/src/vkscript/nodes.h b/src/vkscript/nodes.h
new file mode 100644
index 0000000..f75f7c0
--- /dev/null
+++ b/src/vkscript/nodes.h
@@ -0,0 +1,171 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VKSCRIPT_NODES_H_
+#define SRC_VKSCRIPT_NODES_H_
+
+#include <cstdint>
+#include <memory>
+#include <vector>
+
+#include "src/command.h"
+#include "src/feature.h"
+#include "src/format.h"
+#include "src/tokenizer.h"
+#include "src/vkscript/section_parser.h"
+
+namespace amber {
+namespace vkscript {
+
+class IndicesNode;
+class RequireNode;
+class ShaderNode;
+class TestNode;
+class VertexDataNode;
+
+class Node {
+ public:
+  virtual ~Node();
+
+  bool IsIndices() const { return node_type_ == NodeType::kIndices; }
+  bool IsRequire() const { return node_type_ == NodeType::kRequire; }
+  bool IsShader() const { return node_type_ == NodeType::kShader; }
+  bool IsTest() const { return node_type_ == NodeType::kTest; }
+  bool IsVertexData() const { return node_type_ == NodeType::kVertexData; }
+
+  IndicesNode* AsIndices();
+  RequireNode* AsRequire();
+  ShaderNode* AsShader();
+  TestNode* AsTest();
+  VertexDataNode* AsVertexData();
+
+ protected:
+  Node(NodeType type);
+
+ private:
+  NodeType node_type_;
+};
+
+class ShaderNode : public Node {
+ public:
+  ShaderNode(ShaderType type, std::vector<uint32_t> shader);
+  ~ShaderNode() override;
+
+  ShaderType GetShaderType() const { return type_; }
+  const std::vector<uint32_t>& GetData() const { return shader_; }
+
+ private:
+  ShaderType type_;
+  std::vector<uint32_t> shader_;
+};
+
+class RequireNode : public Node {
+ public:
+  class Requirement {
+   public:
+    Requirement(Feature feature);
+    Requirement(Feature feature, std::unique_ptr<Format> format);
+    Requirement(Requirement&&);
+    ~Requirement();
+
+    Feature GetFeature() const { return feature_; }
+    const Format* GetFormat() const { return format_.get(); }
+
+   private:
+    Feature feature_;
+    std::unique_ptr<Format> format_;
+  };
+
+  RequireNode();
+  ~RequireNode() override;
+
+  void AddRequirement(Feature feature);
+  void AddRequirement(Feature feature, std::unique_ptr<Format> format);
+
+  const std::vector<Requirement>& Requirements() const { return requirements_; }
+
+  void AddExtension(const std::string& ext) { extensions_.push_back(ext); }
+  const std::vector<std::string>& Extensions() const { return extensions_; }
+
+ private:
+  std::vector<Requirement> requirements_;
+  std::vector<std::string> extensions_;
+};
+
+class IndicesNode : public Node {
+ public:
+  IndicesNode(const std::vector<uint16_t>& indices);
+  ~IndicesNode() override;
+
+  const std::vector<uint16_t>& Indices() const { return indices_; }
+
+ private:
+  std::vector<uint16_t> indices_;
+};
+
+class VertexDataNode : public Node {
+ public:
+  struct Header {
+    uint8_t location;
+    std::unique_ptr<Format> format;
+  };
+
+  class Cell {
+   public:
+    Cell();
+    Cell(const Cell&);
+    ~Cell();
+
+    size_t size() const { return data_.size(); }
+    void AppendValue(Value&& v) { data_.emplace_back(std::move(v)); }
+    const Value& GetValue(size_t idx) const { return data_[idx]; }
+
+   private:
+    std::vector<Value> data_;
+  };
+
+  VertexDataNode();
+  ~VertexDataNode() override;
+
+  const std::vector<Header>& GetHeaders() const { return headers_; }
+  void SetHeaders(std::vector<Header> headers) {
+    headers_ = std::move(headers);
+  }
+
+  void AddRow(std::vector<Cell> row) { rows_.push_back(std::move(row)); }
+
+  const std::vector<std::vector<Cell>>& GetRows() const { return rows_; }
+
+ private:
+  std::vector<Header> headers_;
+  std::vector<std::vector<Cell>> rows_;
+};
+
+class TestNode : public Node {
+ public:
+  TestNode(std::vector<std::unique_ptr<Command>> cmds);
+  ~TestNode() override;
+
+  const std::vector<std::unique_ptr<Command>>& GetCommands() const {
+    return commands_;
+  }
+
+ private:
+  std::vector<std::unique_ptr<Command>> commands_;
+};
+
+}  // namespace vkscript
+}  // namespace amber
+
+#endif  // SRC_VKSCRIPT_NODES_H_
diff --git a/src/vkscript/parser.cc b/src/vkscript/parser.cc
new file mode 100644
index 0000000..ddbf5c4
--- /dev/null
+++ b/src/vkscript/parser.cc
@@ -0,0 +1,395 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vkscript/parser.h"
+
+#include <algorithm>
+#include <cassert>
+#include <limits>
+
+#include "src/shader_compiler.h"
+#include "src/tokenizer.h"
+#include "src/vkscript/command_parser.h"
+#include "src/vkscript/format_parser.h"
+#include "src/vkscript/nodes.h"
+
+namespace amber {
+namespace vkscript {
+namespace {
+
+Feature NameToFeature(const std::string& name) {
+  if (name == "robustBufferAccess")
+    return Feature::kRobustBufferAccess;
+  if (name == "fullDrawIndexUint32")
+    return Feature::kFullDrawIndexUint32;
+  if (name == "imageCubeArray")
+    return Feature::kImageCubeArray;
+  if (name == "independentBlend")
+    return Feature::kIndependentBlend;
+  if (name == "geometryShader")
+    return Feature::kGeometryShader;
+  if (name == "tessellationShader")
+    return Feature::kTessellationShader;
+  if (name == "sampleRateShading")
+    return Feature::kSampleRateShading;
+  if (name == "dualSrcBlend")
+    return Feature::kDualSrcBlend;
+  if (name == "logicOp")
+    return Feature::kLogicOp;
+  if (name == "multiDrawIndirect")
+    return Feature::kMultiDrawIndirect;
+  if (name == "drawIndirectFirstInstance")
+    return Feature::kDrawIndirectFirstInstance;
+  if (name == "depthClamp")
+    return Feature::kDepthClamp;
+  if (name == "depthBiasClamp")
+    return Feature::kDepthBiasClamp;
+  if (name == "fillModeNonSolid")
+    return Feature::kFillModeNonSolid;
+  if (name == "depthBounds")
+    return Feature::kDepthBounds;
+  if (name == "wideLines")
+    return Feature::kWideLines;
+  if (name == "largePoints")
+    return Feature::kLargePoints;
+  if (name == "alphaToOne")
+    return Feature::kAlphaToOne;
+  if (name == "multiViewport")
+    return Feature::kMultiViewport;
+  if (name == "samplerAnisotropy")
+    return Feature::kSamplerAnisotropy;
+  if (name == "textureCompressionETC2")
+    return Feature::kTextureCompressionETC2;
+  if (name == "textureCompressionASTC_LDR")
+    return Feature::kTextureCompressionASTC_LDR;
+  if (name == "textureCompressionBC")
+    return Feature::kTextureCompressionBC;
+  if (name == "occlusionQueryPrecise")
+    return Feature::kOcclusionQueryPrecise;
+  if (name == "pipelineStatisticsQuery")
+    return Feature::kPipelineStatisticsQuery;
+  if (name == "vertexPipelineStoresAndAtomics")
+    return Feature::kVertexPipelineStoresAndAtomics;
+  if (name == "fragmentStoresAndAtomics")
+    return Feature::kFragmentStoresAndAtomics;
+  if (name == "shaderTessellationAndGeometryPointSize")
+    return Feature::kShaderTessellationAndGeometryPointSize;
+  if (name == "shaderImageGatherExtended")
+    return Feature::kShaderImageGatherExtended;
+  if (name == "shaderStorageImageExtendedFormats")
+    return Feature::kShaderStorageImageExtendedFormats;
+  if (name == "shaderStorageImageMultisample")
+    return Feature::kShaderStorageImageMultisample;
+  if (name == "shaderStorageImageReadWithoutFormat")
+    return Feature::kShaderStorageImageReadWithoutFormat;
+  if (name == "shaderStorageImageWriteWithoutFormat")
+    return Feature::kShaderStorageImageWriteWithoutFormat;
+  if (name == "shaderUniformBufferArrayDynamicIndexing")
+    return Feature::kShaderUniformBufferArrayDynamicIndexing;
+  if (name == "shaderSampledImageArrayDynamicIndexing")
+    return Feature::kShaderSampledImageArrayDynamicIndexing;
+  if (name == "shaderStorageBufferArrayDynamicIndexing")
+    return Feature::kShaderStorageBufferArrayDynamicIndexing;
+  if (name == "shaderStorageImageArrayDynamicIndexing")
+    return Feature::kShaderStorageImageArrayDynamicIndexing;
+  if (name == "shaderClipDistance")
+    return Feature::kShaderClipDistance;
+  if (name == "shaderCullDistance")
+    return Feature::kShaderCullDistance;
+  if (name == "shaderFloat64")
+    return Feature::kShaderFloat64;
+  if (name == "shaderInt64")
+    return Feature::kShaderInt64;
+  if (name == "shaderInt16")
+    return Feature::kShaderInt16;
+  if (name == "shaderResourceResidency")
+    return Feature::kShaderResourceResidency;
+  if (name == "shaderResourceMinLod")
+    return Feature::kShaderResourceMinLod;
+  if (name == "sparseBinding")
+    return Feature::kSparseBinding;
+  if (name == "sparseResidencyBuffer")
+    return Feature::kSparseResidencyBuffer;
+  if (name == "sparseResidencyImage2D")
+    return Feature::kSparseResidencyImage2D;
+  if (name == "sparseResidencyImage3D")
+    return Feature::kSparseResidencyImage3D;
+  if (name == "sparseResidency2Samples")
+    return Feature::kSparseResidency2Samples;
+  if (name == "sparseResidency4Samples")
+    return Feature::kSparseResidency4Samples;
+  if (name == "sparseResidency8Samples")
+    return Feature::kSparseResidency8Samples;
+  if (name == "sparseResidency16Samples")
+    return Feature::kSparseResidency16Samples;
+  if (name == "sparseResidencyAliased")
+    return Feature::kSparseResidencyAliased;
+  if (name == "variableMultisampleRate")
+    return Feature::kVariableMultisampleRate;
+  if (name == "inheritedQueries")
+    return Feature::kInheritedQueries;
+  if (name == "framebuffer")
+    return Feature::kFramebuffer;
+  if (name == "depthstencil")
+    return Feature::kDepthStencil;
+  return Feature::kUnknown;
+}
+
+}  // namespace
+
+Parser::Parser() : amber::Parser() {}
+
+Parser::~Parser() = default;
+
+Result Parser::Parse(const std::string& input) {
+  SectionParser section_parser;
+  Result r = section_parser.Parse(input);
+  if (!r.IsSuccess())
+    return r;
+
+  for (const auto& section : section_parser.Sections()) {
+    r = ProcessSection(section);
+    if (!r.IsSuccess())
+      return r;
+  }
+
+  return {};
+}
+
+Result Parser::ProcessSection(const SectionParser::Section& section) {
+  // Should never get here, but skip it anyway.
+  if (section.section_type == NodeType::kComment)
+    return {};
+
+  if (SectionParser::HasShader(section.section_type))
+    return ProcessShaderBlock(section);
+  if (section.section_type == NodeType::kRequire)
+    return ProcessRequireBlock(section.contents);
+  if (section.section_type == NodeType::kIndices)
+    return ProcessIndicesBlock(section.contents);
+  if (section.section_type == NodeType::kVertexData)
+    return ProcessVertexDataBlock(section.contents);
+  if (section.section_type == NodeType::kTest)
+    return ProcessTestBlock(section.contents);
+
+  return Result("Unknown node type ....");
+}
+
+Result Parser::ProcessShaderBlock(const SectionParser::Section& section) {
+  ShaderCompiler sc;
+
+  assert(SectionParser::HasShader(section.section_type));
+
+  Result r;
+  std::vector<uint32_t> shader;
+  std::tie(r, shader) =
+      sc.Compile(section.shader_type, section.format, section.contents);
+  if (!r.IsSuccess())
+    return r;
+
+  script_.AddShader(section.shader_type, std::move(shader));
+
+  return {};
+}
+
+Result Parser::ProcessRequireBlock(const std::string& data) {
+  auto node = MakeUnique<RequireNode>();
+
+  Tokenizer tokenizer(data);
+  for (auto token = tokenizer.NextToken(); !token->IsEOS();
+       token = tokenizer.NextToken()) {
+    if (token->IsEOL())
+      continue;
+
+    if (!token->IsString())
+      return Result("Failed to parse requirements block.");
+
+    std::string str = token->AsString();
+    Feature feature = NameToFeature(str);
+    if (feature == Feature::kUnknown) {
+      auto it = std::find_if(str.begin(), str.end(),
+                             [](char c) { return !(isalnum(c) || c == '_'); });
+      if (it != str.end())
+        return Result("Unknown feature or extension: " + str);
+
+      node->AddExtension(str);
+    } else if (feature == Feature::kFramebuffer) {
+      token = tokenizer.NextToken();
+      if (!token->IsString())
+        return Result("Missing framebuffer format");
+
+      FormatParser fmt_parser;
+      auto fmt = fmt_parser.Parse(token->AsString());
+      if (fmt == nullptr)
+        return Result("Failed to parse framebuffer format");
+
+      node->AddRequirement(feature, std::move(fmt));
+    } else if (feature == Feature::kDepthStencil) {
+      token = tokenizer.NextToken();
+      if (!token->IsString())
+        return Result("Missing depthStencil format");
+
+      FormatParser fmt_parser;
+      auto fmt = fmt_parser.Parse(token->AsString());
+      if (fmt == nullptr)
+        return Result("Failed to parse depthstencil format");
+
+      node->AddRequirement(feature, std::move(fmt));
+    } else {
+      node->AddRequirement(feature);
+    }
+
+    token = tokenizer.NextToken();
+    if (!token->IsEOS() && !token->IsEOL())
+      return Result("Failed to parser requirements block: invalid token");
+  }
+
+  if (!node->Requirements().empty() || !node->Extensions().empty())
+    script_.AddRequireNode(std::move(node));
+
+  return {};
+}
+
+Result Parser::ProcessIndicesBlock(const std::string& data) {
+  std::vector<uint16_t> indices;
+
+  Tokenizer tokenizer(data);
+  for (auto token = tokenizer.NextToken(); !token->IsEOS();
+       token = tokenizer.NextToken()) {
+    if (token->IsEOL())
+      continue;
+
+    if (!token->IsInteger())
+      return Result("Invalid value in indices block");
+    if (token->AsUint64() >
+        static_cast<uint64_t>(std::numeric_limits<uint16_t>::max())) {
+      return Result("Value too large in indices block");
+    }
+
+    indices.push_back(token->AsUint16());
+  }
+
+  if (!indices.empty())
+    script_.AddIndices(indices);
+
+  return {};
+}
+
+Result Parser::ProcessVertexDataBlock(const std::string& data) {
+  Tokenizer tokenizer(data);
+
+  // Skip blank and comment lines
+  auto token = tokenizer.NextToken();
+  while (token->IsEOL())
+    token = tokenizer.NextToken();
+
+  // Skip empty vertex data blocks
+  if (token->IsEOS())
+    return {};
+
+  // Process the header line.
+  std::vector<VertexDataNode::Header> headers;
+  while (!token->IsEOL() && !token->IsEOS()) {
+    // Because of the way the tokenizer works we'll see a number then a string
+    // the string will start with a slash which we have to remove.
+    if (!token->IsInteger())
+      return Result("Unable to process vertex data header");
+
+    uint8_t loc = token->AsUint8();
+
+    token = tokenizer.NextToken();
+    if (!token->IsString())
+      return Result("Unable to process vertex data header");
+
+    std::string fmt_name = token->AsString();
+    if (fmt_name.size() < 2)
+      return Result("Vertex data format too short");
+
+    FormatParser parser;
+    auto fmt = parser.Parse(fmt_name.substr(1, fmt_name.length()));
+    if (!fmt)
+      return Result("Invalid format in vertex data header");
+
+    headers.push_back({loc, std::move(fmt)});
+
+    token = tokenizer.NextToken();
+  }
+
+  auto node = MakeUnique<VertexDataNode>();
+
+  // Process data lines
+  for (; !token->IsEOS(); token = tokenizer.NextToken()) {
+    if (token->IsEOL())
+      continue;
+
+    std::vector<VertexDataNode::Cell> row;
+    for (const auto& header : headers) {
+      row.emplace_back();
+
+      if (header.format->GetPackSize() > 0) {
+        if (!token->IsHex())
+          return Result("Invalid packed value in Vertex Data");
+
+        Value v;
+        v.SetIntValue(token->AsHex());
+        row.back().AppendValue(std::move(v));
+      } else {
+        auto& comps = header.format->GetComponents();
+        for (size_t i = 0; i < comps.size();
+             ++i, token = tokenizer.NextToken()) {
+          if (token->IsEOS() || token->IsEOL())
+            return Result("Too few cells in given vertex data row");
+
+          auto& comp = comps[i];
+
+          Value v;
+          if (comp.mode == FormatMode::kUFloat ||
+              comp.mode == FormatMode::kSFloat) {
+            Result r = token->ConvertToDouble();
+            if (!r.IsSuccess())
+              return r;
+
+            v.SetDoubleValue(token->AsDouble());
+          } else if (token->IsInteger()) {
+            v.SetIntValue(token->AsUint64());
+          } else {
+            return Result("Invalid vertex data value");
+          }
+
+          row.back().AppendValue(std::move(v));
+        }
+      }
+    }
+    node->AddRow(std::move(row));
+  }
+
+  node->SetHeaders(std::move(headers));
+  script_.AddVertexData(std::move(node));
+
+  return {};
+}
+
+Result Parser::ProcessTestBlock(const std::string& data) {
+  CommandParser cp;
+  Result r = cp.Parse(data);
+  if (!r.IsSuccess())
+    return r;
+
+  script_.SetTestCommands(cp.TakeCommands());
+
+  return {};
+}
+
+}  // namespace vkscript
+}  // namespace amber
diff --git a/src/vkscript/parser.h b/src/vkscript/parser.h
new file mode 100644
index 0000000..efa56af
--- /dev/null
+++ b/src/vkscript/parser.h
@@ -0,0 +1,64 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VKSCRIPT_PARSER_H_
+#define SRC_VKSCRIPT_PARSER_H_
+
+#include <string>
+
+#include "amber/result.h"
+#include "src/parser.h"
+#include "src/vkscript/script.h"
+#include "src/vkscript/section_parser.h"
+
+namespace amber {
+namespace vkscript {
+
+class Parser : public amber::Parser {
+ public:
+  Parser();
+  ~Parser() override;
+
+  // amber::Parser
+  Result Parse(const std::string& data) override;
+  const amber::Script* GetScript() const override { return &script_; }
+
+  Result ProcessRequireBlockForTesting(const std::string& block) {
+    return ProcessRequireBlock(block);
+  }
+  Result ProcessIndicesBlockForTesting(const std::string& block) {
+    return ProcessIndicesBlock(block);
+  }
+  Result ProcessVertexDataBlockForTesting(const std::string& block) {
+    return ProcessVertexDataBlock(block);
+  }
+  Result ProcessTestBlockForTesting(const std::string& block) {
+    return ProcessTestBlock(block);
+  }
+
+ private:
+  Result ProcessSection(const SectionParser::Section& section);
+  Result ProcessShaderBlock(const SectionParser::Section& section);
+  Result ProcessRequireBlock(const std::string&);
+  Result ProcessIndicesBlock(const std::string&);
+  Result ProcessVertexDataBlock(const std::string&);
+  Result ProcessTestBlock(const std::string&);
+
+  vkscript::Script script_;
+};
+
+}  // namespace vkscript
+}  // namespace amber
+
+#endif  // SRC_VKSCRIPT_PARSER_H_
diff --git a/src/vkscript/parser_test.cc b/src/vkscript/parser_test.cc
new file mode 100644
index 0000000..ffef56e
--- /dev/null
+++ b/src/vkscript/parser_test.cc
@@ -0,0 +1,548 @@
+// Copyright 2018 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 "src/vkscript/parser.h"
+#include "gtest/gtest.h"
+#include "src/feature.h"
+#include "src/format.h"
+#include "src/vkscript/nodes.h"
+
+namespace amber {
+namespace vkscript {
+
+using VkScriptParserTest = testing::Test;
+
+TEST_F(VkScriptParserTest, EmptyRequireBlock) {
+  std::string block = "";
+
+  Parser parser;
+  Result r = parser.ProcessRequireBlockForTesting(block);
+  ASSERT_TRUE(r.IsSuccess());
+
+  auto script = parser.GetScript();
+  ASSERT_TRUE(script->IsVkScript());
+  EXPECT_TRUE(ToVkScript(script)->Nodes().empty());
+}
+
+TEST_F(VkScriptParserTest, RequireBlockNoArgumentFeatures) {
+  struct {
+    const char* name;
+    Feature feature;
+  } features[] = {
+      {"robustBufferAccess", Feature::kRobustBufferAccess},
+      {"fullDrawIndexUint32", Feature::kFullDrawIndexUint32},
+      {"imageCubeArray", Feature::kImageCubeArray},
+      {"independentBlend", Feature::kIndependentBlend},
+      {"geometryShader", Feature::kGeometryShader},
+      {"tessellationShader", Feature::kTessellationShader},
+      {"sampleRateShading", Feature::kSampleRateShading},
+      {"dualSrcBlend", Feature::kDualSrcBlend},
+      {"logicOp", Feature::kLogicOp},
+      {"multiDrawIndirect", Feature::kMultiDrawIndirect},
+      {"drawIndirectFirstInstance", Feature::kDrawIndirectFirstInstance},
+      {"depthClamp", Feature::kDepthClamp},
+      {"depthBiasClamp", Feature::kDepthBiasClamp},
+      {"fillModeNonSolid", Feature::kFillModeNonSolid},
+      {"depthBounds", Feature::kDepthBounds},
+      {"wideLines", Feature::kWideLines},
+      {"largePoints", Feature::kLargePoints},
+      {"alphaToOne", Feature::kAlphaToOne},
+      {"multiViewport", Feature::kMultiViewport},
+      {"samplerAnisotropy", Feature::kSamplerAnisotropy},
+      {"textureCompressionETC2", Feature::kTextureCompressionETC2},
+      {"textureCompressionASTC_LDR", Feature::kTextureCompressionASTC_LDR},
+      {"textureCompressionBC", Feature::kTextureCompressionBC},
+      {"occlusionQueryPrecise", Feature::kOcclusionQueryPrecise},
+      {"pipelineStatisticsQuery", Feature::kPipelineStatisticsQuery},
+      {"vertexPipelineStoresAndAtomics",
+       Feature::kVertexPipelineStoresAndAtomics},
+      {"fragmentStoresAndAtomics", Feature::kFragmentStoresAndAtomics},
+      {"shaderTessellationAndGeometryPointSize",
+       Feature::kShaderTessellationAndGeometryPointSize},
+      {"shaderImageGatherExtended", Feature::kShaderImageGatherExtended},
+      {"shaderStorageImageExtendedFormats",
+       Feature::kShaderStorageImageExtendedFormats},
+      {"shaderStorageImageMultisample",
+       Feature::kShaderStorageImageMultisample},
+      {"shaderStorageImageReadWithoutFormat",
+       Feature::kShaderStorageImageReadWithoutFormat},
+      {"shaderStorageImageWriteWithoutFormat",
+       Feature::kShaderStorageImageWriteWithoutFormat},
+      {"shaderUniformBufferArrayDynamicIndexing",
+       Feature::kShaderUniformBufferArrayDynamicIndexing},
+      {"shaderSampledImageArrayDynamicIndexing",
+       Feature::kShaderSampledImageArrayDynamicIndexing},
+      {"shaderStorageBufferArrayDynamicIndexing",
+       Feature::kShaderStorageBufferArrayDynamicIndexing},
+      {"shaderStorageImageArrayDynamicIndexing",
+       Feature::kShaderStorageImageArrayDynamicIndexing},
+      {"shaderClipDistance", Feature::kShaderClipDistance},
+      {"shaderCullDistance", Feature::kShaderCullDistance},
+      {"shaderFloat64", Feature::kShaderFloat64},
+      {"shaderInt64", Feature::kShaderInt64},
+      {"shaderInt16", Feature::kShaderInt16},
+      {"shaderResourceResidency", Feature::kShaderResourceResidency},
+      {"shaderResourceMinLod", Feature::kShaderResourceMinLod},
+      {"sparseBinding", Feature::kSparseBinding},
+      {"sparseResidencyBuffer", Feature::kSparseResidencyBuffer},
+      {"sparseResidencyImage2D", Feature::kSparseResidencyImage2D},
+      {"sparseResidencyImage3D", Feature::kSparseResidencyImage3D},
+      {"sparseResidency2Samples", Feature::kSparseResidency2Samples},
+      {"sparseResidency4Samples", Feature::kSparseResidency4Samples},
+      {"sparseResidency8Samples", Feature::kSparseResidency8Samples},
+      {"sparseResidency16Samples", Feature::kSparseResidency16Samples},
+      {"sparseResidencyAliased", Feature::kSparseResidencyAliased},
+      {"variableMultisampleRate", Feature::kVariableMultisampleRate},
+      {"inheritedQueries", Feature::kInheritedQueries},
+  };
+
+  for (const auto& feature : features) {
+    Parser parser;
+    Result r = parser.ProcessRequireBlockForTesting(feature.name);
+    ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+    auto& nodes = ToVkScript(parser.GetScript())->Nodes();
+    ASSERT_EQ(1U, nodes.size());
+    ASSERT_TRUE(nodes[0]->IsRequire());
+
+    auto req = nodes[0]->AsRequire();
+    ASSERT_EQ(1U, req->Requirements().size());
+    EXPECT_EQ(feature.feature, req->Requirements()[0].GetFeature());
+  }
+}
+
+TEST_F(VkScriptParserTest, RequireBlockExtensions) {
+  std::string block = R"(VK_KHR_storage_buffer_storage_class
+VK_KHR_variable_pointers)";
+
+  Parser parser;
+  Result r = parser.ProcessRequireBlockForTesting(block);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& nodes = ToVkScript(parser.GetScript())->Nodes();
+  ASSERT_EQ(1U, nodes.size());
+  ASSERT_TRUE(nodes[0]->IsRequire());
+
+  auto req = nodes[0]->AsRequire();
+  ASSERT_EQ(2U, req->Extensions().size());
+
+  const auto& exts = req->Extensions();
+  EXPECT_EQ("VK_KHR_storage_buffer_storage_class", exts[0]);
+  EXPECT_EQ("VK_KHR_variable_pointers", exts[1]);
+}
+
+TEST_F(VkScriptParserTest, RequireBlockFramebuffer) {
+  std::string block = "framebuffer R32G32B32A32_SFLOAT";
+
+  Parser parser;
+  Result r = parser.ProcessRequireBlockForTesting(block);
+  ASSERT_TRUE(r.IsSuccess());
+
+  auto script = ToVkScript(parser.GetScript());
+  EXPECT_EQ(1U, script->Nodes().size());
+
+  auto& nodes = script->Nodes();
+  ASSERT_EQ(1U, nodes.size());
+  ASSERT_TRUE(nodes[0]->IsRequire());
+
+  auto* req = nodes[0]->AsRequire();
+  ASSERT_EQ(1U, req->Requirements().size());
+  EXPECT_EQ(Feature::kFramebuffer, req->Requirements()[0].GetFeature());
+
+  auto format = req->Requirements()[0].GetFormat();
+  EXPECT_EQ(FormatType::kR32G32B32A32_SFLOAT, format->GetFormatType());
+}
+
+TEST_F(VkScriptParserTest, RequireBlockDepthStencil) {
+  std::string block = "depthstencil D24_UNORM_S8_UINT";
+
+  Parser parser;
+  Result r = parser.ProcessRequireBlockForTesting(block);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto script = ToVkScript(parser.GetScript());
+  EXPECT_EQ(1U, script->Nodes().size());
+
+  auto& nodes = script->Nodes();
+  ASSERT_EQ(1U, nodes.size());
+  ASSERT_TRUE(nodes[0]->IsRequire());
+
+  auto* req = nodes[0]->AsRequire();
+  ASSERT_EQ(1U, req->Requirements().size());
+  EXPECT_EQ(Feature::kDepthStencil, req->Requirements()[0].GetFeature());
+
+  auto format = req->Requirements()[0].GetFormat();
+  EXPECT_EQ(FormatType::kD24_UNORM_S8_UINT, format->GetFormatType());
+}
+
+TEST_F(VkScriptParserTest, RequireBlockMultipleLines) {
+  std::string block = R"(
+# Requirements block stuff.
+depthstencil D24_UNORM_S8_UINT
+sparseResidency4Samples
+framebuffer R32G32B32A32_SFLOAT
+# More comments
+inheritedQueries # line comment
+)";
+
+  Parser parser;
+  Result r = parser.ProcessRequireBlockForTesting(block);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto script = ToVkScript(parser.GetScript());
+  EXPECT_EQ(1U, script->Nodes().size());
+
+  auto& nodes = script->Nodes();
+  ASSERT_EQ(1U, nodes.size());
+  ASSERT_TRUE(nodes[0]->IsRequire());
+
+  auto* req = nodes[0]->AsRequire();
+  ASSERT_EQ(4U, req->Requirements().size());
+  EXPECT_EQ(Feature::kDepthStencil, req->Requirements()[0].GetFeature());
+  auto format = req->Requirements()[0].GetFormat();
+  EXPECT_EQ(FormatType::kD24_UNORM_S8_UINT, format->GetFormatType());
+
+  EXPECT_EQ(Feature::kSparseResidency4Samples,
+            req->Requirements()[1].GetFeature());
+
+  EXPECT_EQ(Feature::kFramebuffer, req->Requirements()[2].GetFeature());
+  format = req->Requirements()[2].GetFormat();
+  EXPECT_EQ(FormatType::kR32G32B32A32_SFLOAT, format->GetFormatType());
+
+  EXPECT_EQ(Feature::kInheritedQueries, req->Requirements()[3].GetFeature());
+}
+
+TEST_F(VkScriptParserTest, IndicesBlock) {
+  std::string block = "1 2 3";
+
+  Parser parser;
+  Result r = parser.ProcessIndicesBlockForTesting(block);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& nodes = ToVkScript(parser.GetScript())->Nodes();
+  ASSERT_EQ(1U, nodes.size());
+  ASSERT_TRUE(nodes[0]->IsIndices());
+
+  auto& indices = nodes[0]->AsIndices()->Indices();
+  ASSERT_EQ(3U, indices.size());
+
+  EXPECT_EQ(1, indices[0]);
+  EXPECT_EQ(2, indices[1]);
+  EXPECT_EQ(3, indices[2]);
+}
+
+TEST_F(VkScriptParserTest, IndicesBlockMultipleLines) {
+  std::string block = R"(
+# comment line
+1 2 3   4 5 6
+# another comment
+7 8 9  10 11 12
+)";
+
+  std::vector<uint16_t> results = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
+
+  Parser parser;
+  Result r = parser.ProcessIndicesBlockForTesting(block);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& nodes = ToVkScript(parser.GetScript())->Nodes();
+  ASSERT_EQ(1U, nodes.size());
+  ASSERT_TRUE(nodes[0]->IsIndices());
+
+  auto& indices = nodes[0]->AsIndices()->Indices();
+  ASSERT_EQ(results.size(), indices.size());
+  for (size_t i = 0; i < results.size(); ++i) {
+    EXPECT_EQ(results[i], indices[i]);
+  }
+}
+
+TEST_F(VkScriptParserTest, IndicesBlockBadValue) {
+  std::string block = "1 a 3";
+
+  Parser parser;
+  Result r = parser.ProcessIndicesBlockForTesting(block);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid value in indices block", r.Error());
+}
+
+TEST_F(VkScriptParserTest, IndicesBlockValueTooLarge) {
+  std::string block = "100000000000 3";
+
+  Parser parser;
+  Result r = parser.ProcessIndicesBlockForTesting(block);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Value too large in indices block", r.Error());
+}
+
+TEST_F(VkScriptParserTest, VertexDataEmpty) {
+  std::string block = "\n#comment\n";
+
+  Parser parser;
+  Result r = parser.ProcessVertexDataBlockForTesting(block);
+  ASSERT_TRUE(r.IsSuccess());
+
+  auto& nodes = ToVkScript(parser.GetScript())->Nodes();
+  EXPECT_TRUE(nodes.empty());
+}
+
+TEST_F(VkScriptParserTest, VertexDataHeaderFormatString) {
+  std::string block = "0/R32G32_SFLOAT 1/A8B8G8R8_UNORM_PACK32";
+
+  Parser parser;
+  Result r = parser.ProcessVertexDataBlockForTesting(block);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& nodes = ToVkScript(parser.GetScript())->Nodes();
+  ASSERT_EQ(1U, nodes.size());
+  ASSERT_TRUE(nodes[0]->IsVertexData());
+
+  auto* data = nodes[0]->AsVertexData();
+  EXPECT_TRUE(data->GetRows().empty());
+
+  auto& headers = data->GetHeaders();
+
+  ASSERT_EQ(2U, headers.size());
+  EXPECT_EQ(static_cast<size_t>(0U), headers[0].location);
+  EXPECT_EQ(FormatType::kR32G32_SFLOAT, headers[0].format->GetFormatType());
+
+  EXPECT_EQ(1U, headers[1].location);
+  EXPECT_EQ(FormatType::kA8B8G8R8_UNORM_PACK32,
+            headers[1].format->GetFormatType());
+}
+
+TEST_F(VkScriptParserTest, VertexDataHeaderGlslString) {
+  std::string block = "0/float/vec2 1/int/vec3";
+
+  Parser parser;
+  Result r = parser.ProcessVertexDataBlockForTesting(block);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& nodes = ToVkScript(parser.GetScript())->Nodes();
+  ASSERT_EQ(1U, nodes.size());
+  ASSERT_TRUE(nodes[0]->IsVertexData());
+
+  auto* data = nodes[0]->AsVertexData();
+  EXPECT_TRUE(data->GetRows().empty());
+
+  auto& headers = data->GetHeaders();
+
+  ASSERT_EQ(2U, headers.size());
+  EXPECT_EQ(static_cast<size_t>(0U), headers[0].location);
+  EXPECT_EQ(FormatType::kR32G32_SFLOAT, headers[0].format->GetFormatType());
+
+  auto& comps1 = headers[0].format->GetComponents();
+  ASSERT_EQ(2U, comps1.size());
+  EXPECT_EQ(FormatMode::kSFloat, comps1[0].mode);
+  EXPECT_EQ(FormatMode::kSFloat, comps1[1].mode);
+
+  EXPECT_EQ(1U, headers[1].location);
+  EXPECT_EQ(FormatType::kR32G32B32_SINT, headers[1].format->GetFormatType());
+
+  auto& comps2 = headers[1].format->GetComponents();
+  ASSERT_EQ(3U, comps2.size());
+  EXPECT_EQ(FormatMode::kSInt, comps2[0].mode);
+  EXPECT_EQ(FormatMode::kSInt, comps2[1].mode);
+  EXPECT_EQ(FormatMode::kSInt, comps2[2].mode);
+}
+
+TEST_F(VkScriptParserTest, TestBlock) {
+  std::string block = R"(clear color 255 255 255 0
+clear depth 10
+clear stencil 2
+clear)";
+
+  Parser parser;
+  Result r = parser.ProcessTestBlockForTesting(block);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& nodes = ToVkScript(parser.GetScript())->Nodes();
+  ASSERT_EQ(1U, nodes.size());
+  ASSERT_TRUE(nodes[0]->IsTest());
+
+  const auto& cmds = nodes[0]->AsTest()->GetCommands();
+  ASSERT_EQ(4U, cmds.size());
+
+  ASSERT_TRUE(cmds[0]->IsClearColor());
+  auto* color_cmd = cmds[0]->AsClearColor();
+  EXPECT_FLOAT_EQ(255.f, color_cmd->GetR());
+  EXPECT_FLOAT_EQ(255.f, color_cmd->GetG());
+  EXPECT_FLOAT_EQ(255.f, color_cmd->GetB());
+  EXPECT_FLOAT_EQ(0.0f, color_cmd->GetA());
+
+  ASSERT_TRUE(cmds[1]->IsClearDepth());
+  EXPECT_EQ(10U, cmds[1]->AsClearDepth()->GetValue());
+
+  ASSERT_TRUE(cmds[2]->IsClearStencil());
+  EXPECT_FLOAT_EQ(2, cmds[2]->AsClearStencil()->GetValue());
+
+  EXPECT_TRUE(cmds[3]->IsClear());
+}
+
+TEST_F(VkScriptParserTest, VertexDataRows) {
+  std::string block = R"(
+# Vertex data
+0/R32G32B32_SFLOAT  1/R8G8B8_UNORM
+-1    -1 0.25       255 0 0  # ending comment
+# Another Row
+0.25  -1 0.25       255 0 255
+)";
+
+  Parser parser;
+  Result r = parser.ProcessVertexDataBlockForTesting(block);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& nodes = ToVkScript(parser.GetScript())->Nodes();
+  ASSERT_EQ(1U, nodes.size());
+  ASSERT_TRUE(nodes[0]->IsVertexData());
+
+  auto* data = nodes[0]->AsVertexData();
+  auto& headers = data->GetHeaders();
+  ASSERT_EQ(2U, headers.size());
+
+  // Rows is a vector of vector of cells
+  auto& rows = data->GetRows();
+  ASSERT_EQ(2U, rows.size());
+
+  // A row is a vector of Cells
+  const auto& row1 = rows[0];
+  ASSERT_EQ(2U, row1.size());
+
+  const auto& row1_cell1 = row1[0];
+  ASSERT_EQ(3U, row1_cell1.size());
+
+  ASSERT_TRUE(row1_cell1.GetValue(0).IsFloat());
+  EXPECT_FLOAT_EQ(-1, row1_cell1.GetValue(0).AsFloat());
+  ASSERT_TRUE(row1_cell1.GetValue(1).IsFloat());
+  EXPECT_FLOAT_EQ(-1, row1_cell1.GetValue(1).AsFloat());
+  ASSERT_TRUE(row1_cell1.GetValue(2).IsFloat());
+  EXPECT_FLOAT_EQ(0.25, row1_cell1.GetValue(2).AsFloat());
+
+  const auto& row1_cell2 = row1[1];
+  ASSERT_EQ(3U, row1_cell2.size());
+
+  ASSERT_TRUE(row1_cell2.GetValue(0).IsInteger());
+  EXPECT_FLOAT_EQ(255, row1_cell2.GetValue(0).AsUint8());
+  ASSERT_TRUE(row1_cell2.GetValue(1).IsInteger());
+  EXPECT_FLOAT_EQ(0, row1_cell2.GetValue(1).AsUint8());
+  ASSERT_TRUE(row1_cell2.GetValue(2).IsInteger());
+  EXPECT_FLOAT_EQ(0, row1_cell2.GetValue(2).AsUint8());
+
+  // Second row.
+  const auto& row2 = rows[1];
+  ASSERT_EQ(2U, row2.size());
+
+  const auto& row2_cell1 = row2[0];
+  ASSERT_EQ(3U, row2_cell1.size());
+
+  ASSERT_TRUE(row2_cell1.GetValue(0).IsFloat());
+  EXPECT_FLOAT_EQ(0.25, row2_cell1.GetValue(0).AsFloat());
+  ASSERT_TRUE(row2_cell1.GetValue(1).IsFloat());
+  EXPECT_FLOAT_EQ(-1, row2_cell1.GetValue(1).AsFloat());
+  ASSERT_TRUE(row2_cell1.GetValue(2).IsFloat());
+  EXPECT_FLOAT_EQ(0.25, row2_cell1.GetValue(2).AsFloat());
+
+  const auto& row2_cell2 = row2[1];
+  ASSERT_EQ(3U, row2_cell2.size());
+
+  ASSERT_TRUE(row2_cell2.GetValue(0).IsInteger());
+  EXPECT_FLOAT_EQ(255, row2_cell2.GetValue(0).AsUint8());
+  ASSERT_TRUE(row2_cell2.GetValue(1).IsInteger());
+  EXPECT_FLOAT_EQ(0, row2_cell2.GetValue(1).AsUint8());
+  ASSERT_TRUE(row2_cell2.GetValue(2).IsInteger());
+  EXPECT_FLOAT_EQ(255, row2_cell2.GetValue(2).AsUint8());
+}
+
+TEST_F(VkScriptParserTest, VertexDataShortRow) {
+  std::string block = R"(
+0/R32G32B32_SFLOAT  1/R8G8B8_UNORM
+-1    -1 0.25       255 0 0
+0.25  -1 0.25       255 0
+)";
+
+  Parser parser;
+  Result r = parser.ProcessVertexDataBlockForTesting(block);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Too few cells in given vertex data row", r.Error());
+}
+
+TEST_F(VkScriptParserTest, VertexDataIncorrectValue) {
+  std::string block = R"(
+0/R32G32B32_SFLOAT  1/R8G8B8_UNORM
+-1    -1 0.25       255 StringValue 0
+0.25  -1 0.25       255 0 0
+)";
+
+  Parser parser;
+  Result r = parser.ProcessVertexDataBlockForTesting(block);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid vertex data value", r.Error());
+}
+
+TEST_F(VkScriptParserTest, VertexDataRowsWithHex) {
+  std::string block = R"(
+0/A8B8G8R8_UNORM_PACK32
+0xff0000ff
+0xffff0000
+)";
+
+  Parser parser;
+  Result r = parser.ProcessVertexDataBlockForTesting(block);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto& nodes = ToVkScript(parser.GetScript())->Nodes();
+  ASSERT_EQ(1U, nodes.size());
+  ASSERT_TRUE(nodes[0]->IsVertexData());
+
+  auto* data = nodes[0]->AsVertexData();
+  auto& headers = data->GetHeaders();
+  ASSERT_EQ(1U, headers.size());
+
+  auto& rows = data->GetRows();
+  ASSERT_EQ(2U, rows.size());
+
+  // Each row has 1 cell.
+  auto& row1 = rows[0];
+  ASSERT_EQ(1U, row1.size());
+
+  auto& row1_cell1 = row1[0];
+  ASSERT_EQ(1U, row1_cell1.size());
+
+  ASSERT_TRUE(row1_cell1.GetValue(0).IsInteger());
+  EXPECT_EQ(0xff0000ff, row1_cell1.GetValue(0).AsUint32());
+
+  auto& row2 = rows[1];
+  ASSERT_EQ(1U, row1.size());
+
+  const auto& row2_cell1 = row2[0];
+  ASSERT_EQ(1U, row2_cell1.size());
+
+  ASSERT_TRUE(row2_cell1.GetValue(0).IsInteger());
+  EXPECT_EQ(0xffff0000, row2_cell1.GetValue(0).AsUint32());
+}
+
+TEST_F(VkScriptParserTest, VertexDataRowsWithHexWrongColumn) {
+  std::string block = R"(
+0/R32G32B32_SFLOAT  1/R8G8B8_UNORM
+-1    -1 0.25       0xffff0000
+0.25  -1 0.25       255 0
+)";
+
+  Parser parser;
+  Result r = parser.ProcessVertexDataBlockForTesting(block);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid vertex data value", r.Error());
+}
+
+}  // namespace vkscript
+}  // namespace amber
diff --git a/src/vkscript/script.cc b/src/vkscript/script.cc
new file mode 100644
index 0000000..2c08e5b
--- /dev/null
+++ b/src/vkscript/script.cc
@@ -0,0 +1,55 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vkscript/script.h"
+
+#include "src/make_unique.h"
+#include "src/vkscript/nodes.h"
+
+namespace amber {
+
+const vkscript::Script* ToVkScript(const amber::Script* s) {
+  return static_cast<const vkscript::Script*>(s);
+}
+
+namespace vkscript {
+
+Script::Script() : amber::Script(ScriptType::kVkScript) {}
+
+Script::~Script() = default;
+
+void Script::AddRequireNode(std::unique_ptr<RequireNode> node) {
+  std::unique_ptr<Node> tn(node.release());
+  test_nodes_.push_back(std::move(tn));
+}
+
+void Script::AddShader(ShaderType type, std::vector<uint32_t> shader) {
+  test_nodes_.push_back(MakeUnique<ShaderNode>(type, std::move(shader)));
+}
+
+void Script::AddIndices(const std::vector<uint16_t>& indices) {
+  test_nodes_.push_back(MakeUnique<IndicesNode>(indices));
+}
+
+void Script::AddVertexData(std::unique_ptr<VertexDataNode> node) {
+  std::unique_ptr<Node> tn(node.release());
+  test_nodes_.push_back(std::move(tn));
+}
+
+void Script::SetTestCommands(std::vector<std::unique_ptr<Command>> cmds) {
+  test_nodes_.push_back(MakeUnique<TestNode>(std::move(cmds)));
+}
+
+}  // namespace vkscript
+}  // namespace amber
diff --git a/src/vkscript/script.h b/src/vkscript/script.h
new file mode 100644
index 0000000..966e52a
--- /dev/null
+++ b/src/vkscript/script.h
@@ -0,0 +1,58 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VKSCRIPT_SCRIPT_H_
+#define SRC_VKSCRIPT_SCRIPT_H_
+
+#include <memory>
+#include <vector>
+
+#include "src/command.h"
+#include "src/make_unique.h"
+#include "src/script.h"
+#include "src/vkscript/section_parser.h"
+
+namespace amber {
+namespace vkscript {
+
+class Node;
+class RequireNode;
+class VertexDataNode;
+
+class Script : public amber::Script {
+ public:
+  Script();
+  ~Script() override;
+
+  void AddRequireNode(std::unique_ptr<RequireNode> node);
+  void AddShader(ShaderType, std::vector<uint32_t>);
+  void AddIndices(const std::vector<uint16_t>& indices);
+  void AddVertexData(std::unique_ptr<VertexDataNode> node);
+  void SetTestCommands(std::vector<std::unique_ptr<Command>> commands);
+
+  const std::vector<std::unique_ptr<Node>>& Nodes() const {
+    return test_nodes_;
+  }
+
+ private:
+  std::vector<std::unique_ptr<Node>> test_nodes_;
+};
+
+}  // namespace vkscript
+
+const vkscript::Script* ToVkScript(const amber::Script* s);
+
+}  // namespace amber
+
+#endif  // SRC_VKSCRIPT_SCRIPT_H_
diff --git a/src/vkscript/section_parser.cc b/src/vkscript/section_parser.cc
new file mode 100644
index 0000000..a76db66
--- /dev/null
+++ b/src/vkscript/section_parser.cc
@@ -0,0 +1,205 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vkscript/section_parser.h"
+
+#include <cassert>
+#include <iostream>
+#include <sstream>
+#include <string>
+
+#include "src/make_unique.h"
+
+namespace amber {
+namespace vkscript {
+
+// static
+bool SectionParser::HasShader(const NodeType type) {
+  return type == NodeType::kShader;
+}
+
+SectionParser::SectionParser() = default;
+
+SectionParser::~SectionParser() = default;
+
+Result SectionParser::Parse(const std::string& data) {
+  Result result = SplitSections(data);
+  if (!result.IsSuccess())
+    return result;
+  return {};
+}
+
+Result SectionParser::NameToNodeType(const std::string& data,
+                                     NodeType* section_type,
+                                     ShaderType* shader_type,
+                                     ShaderFormat* fmt) const {
+  assert(section_type);
+  assert(shader_type);
+  assert(fmt);
+
+  *fmt = ShaderFormat::kText;
+
+  std::string name;
+  size_t pos = data.rfind(" spirv hex");
+  if (pos != std::string::npos) {
+    *fmt = ShaderFormat::kSpirvHex;
+    name = data.substr(0, pos);
+  } else {
+    pos = data.rfind(" spirv");
+    if (pos != std::string::npos) {
+      *fmt = ShaderFormat::kSpirvAsm;
+      name = data.substr(0, pos);
+    } else {
+      name = data;
+    }
+  }
+
+  pos = data.rfind(" passthrough");
+  if (pos != std::string::npos) {
+    *fmt = ShaderFormat::kDefault;
+    name = data.substr(0, pos);
+  }
+
+  if (name == "comment") {
+    *section_type = NodeType::kComment;
+  } else if (name == "indices") {
+    *section_type = NodeType::kIndices;
+  } else if (name == "require") {
+    *section_type = NodeType::kRequire;
+  } else if (name == "test") {
+    *section_type = NodeType::kTest;
+  } else if (name == "vertex data") {
+    *section_type = NodeType::kVertexData;
+  } else if (name == "compute shader") {
+    *section_type = NodeType::kShader;
+    *shader_type = ShaderType::kCompute;
+    if (*fmt == ShaderFormat::kText)
+      *fmt = ShaderFormat::kGlsl;
+  } else if (name == "fragment shader") {
+    *section_type = NodeType::kShader;
+    *shader_type = ShaderType::kFragment;
+    if (*fmt == ShaderFormat::kText)
+      *fmt = ShaderFormat::kGlsl;
+  } else if (name == "geometry shader") {
+    *section_type = NodeType::kShader;
+    *shader_type = ShaderType::kGeometry;
+    if (*fmt == ShaderFormat::kText)
+      *fmt = ShaderFormat::kGlsl;
+  } else if (name == "tessellation control shader") {
+    *section_type = NodeType::kShader;
+    *shader_type = ShaderType::kTessellationControl;
+    if (*fmt == ShaderFormat::kText)
+      *fmt = ShaderFormat::kGlsl;
+  } else if (name == "tessellation evaluation shader") {
+    *section_type = NodeType::kShader;
+    *shader_type = ShaderType::kTessellationEvaluation;
+    if (*fmt == ShaderFormat::kText)
+      *fmt = ShaderFormat::kGlsl;
+  } else if (name == "vertex shader") {
+    *section_type = NodeType::kShader;
+    *shader_type = ShaderType::kVertex;
+    if (*fmt == ShaderFormat::kText)
+      *fmt = ShaderFormat::kGlsl;
+  } else {
+    return Result("Invalid name: " + data);
+  }
+
+  if (!SectionParser::HasShader(*section_type) &&
+      (*fmt == ShaderFormat::kGlsl || *fmt == ShaderFormat::kSpirvAsm ||
+       *fmt == ShaderFormat::kSpirvHex)) {
+    return Result("Invalid source format: " + data);
+  }
+
+  return {};
+}
+
+void SectionParser::AddSection(NodeType section_type,
+                               ShaderType shader_type,
+                               ShaderFormat fmt,
+                               const std::string& contents) {
+  if (section_type == NodeType::kComment)
+    return;
+
+  if (fmt == ShaderFormat::kDefault) {
+    sections_.push_back({section_type, shader_type, ShaderFormat::kSpirvAsm,
+                         kPassThroughShader});
+    return;
+  }
+
+  size_t size = contents.size();
+  while (size > 0) {
+    if (contents[size - 1] == '\n' || contents[size - 1] == '\r') {
+      --size;
+      continue;
+    }
+    break;
+  }
+
+  sections_.push_back(
+      {section_type, shader_type, fmt, contents.substr(0, size)});
+}
+
+Result SectionParser::SplitSections(const std::string& data) {
+  std::stringstream ss(data);
+  size_t line_count = 0;
+  bool in_section = false;
+
+  NodeType current_type = NodeType::kComment;
+  ShaderType current_shader = ShaderType::kVertex;
+  ShaderFormat current_fmt = ShaderFormat::kText;
+  std::string section_contents;
+
+  for (std::string line; std::getline(ss, line);) {
+    ++line_count;
+
+    if (!in_section) {
+      if (line.empty() || line[0] == '#' || line == "\r")
+        continue;
+
+      if (line[0] != '[')
+        return Result(std::to_string(line_count) + ": Invalid character");
+
+      in_section = true;
+    }
+
+    if (line.empty()) {
+      section_contents += "\n";
+      continue;
+    }
+
+    if (line[0] == '[') {
+      AddSection(current_type, current_shader, current_fmt, section_contents);
+      section_contents = "";
+
+      size_t name_end = line.rfind("]");
+      if (name_end == std::string::npos)
+        return Result(std::to_string(line_count) + ": Missing section close");
+
+      std::string name = line.substr(1, name_end - 1);
+
+      Result r =
+          NameToNodeType(name, &current_type, &current_shader, &current_fmt);
+      if (!r.IsSuccess())
+        return Result(std::to_string(line_count) + ": " + r.Error());
+    } else {
+      section_contents += line + "\n";
+    }
+  }
+  AddSection(current_type, current_shader, current_fmt, section_contents);
+
+  return {};
+}
+
+}  // namespace vkscript
+}  // namespace amber
diff --git a/src/vkscript/section_parser.h b/src/vkscript/section_parser.h
new file mode 100644
index 0000000..5eb4ad5
--- /dev/null
+++ b/src/vkscript/section_parser.h
@@ -0,0 +1,82 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VKSCRIPT_SECTION_PARSER_H_
+#define SRC_VKSCRIPT_SECTION_PARSER_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "amber/result.h"
+#include "src/shader_data.h"
+
+namespace amber {
+namespace vkscript {
+
+enum class NodeType : uint8_t {
+  kComment = 0,
+  kShader,
+  kIndices,
+  kVertexData,
+  kRequire,
+  kTest,
+};
+
+class SectionParser {
+ public:
+  struct Section {
+    NodeType section_type;
+    ShaderType shader_type;  // Only valid when section_type == kShader
+    ShaderFormat format;
+    std::string contents;
+  };
+
+  static bool HasShader(const NodeType type);
+
+  SectionParser();
+  ~SectionParser();
+
+  Result Parse(const std::string& data);
+  const std::vector<Section>& Sections() { return sections_; }
+
+  Result SplitSectionsForTesting(const std::string& data) {
+    return SplitSections(data);
+  }
+
+  Result NameToNodeTypeForTesting(const std::string& name,
+                                  NodeType* section_type,
+                                  ShaderType* shader_type,
+                                  ShaderFormat* fmt) const {
+    return NameToNodeType(name, section_type, shader_type, fmt);
+  }
+
+ private:
+  Result SplitSections(const std::string& data);
+  void AddSection(NodeType section_type,
+                  ShaderType shader_type,
+                  ShaderFormat fmt,
+                  const std::string& contents);
+  Result NameToNodeType(const std::string& name,
+                        NodeType* section_type,
+                        ShaderType* shader_type,
+                        ShaderFormat* fmt) const;
+
+  std::vector<Section> sections_;
+};
+
+}  // namespace vkscript
+}  // namespace amber
+
+#endif  // SRC_VKSCRIPT_SECTION_PARSER_H_
diff --git a/src/vkscript/section_parser_test.cc b/src/vkscript/section_parser_test.cc
new file mode 100644
index 0000000..664e181
--- /dev/null
+++ b/src/vkscript/section_parser_test.cc
@@ -0,0 +1,299 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vkscript/section_parser.h"
+#include "gtest/gtest.h"
+
+namespace amber {
+namespace vkscript {
+
+using SectionParserTest = testing::Test;
+
+TEST_F(SectionParserTest, SectionParserCommentSection) {
+  std::string input = "[comment]\nThis is the comment body\n.Lots of Text.";
+
+  SectionParser p;
+  Result r = p.SplitSectionsForTesting(input);
+  ASSERT_TRUE(r.IsSuccess());
+
+  auto sections = p.Sections();
+  EXPECT_TRUE(sections.empty());
+}
+
+TEST_F(SectionParserTest, ParseShaderGlslVertex) {
+  std::string shader = R"(#version 430
+void main() {
+})";
+  std::string input = "[vertex shader]\n" + shader;
+
+  SectionParser p;
+  Result r = p.SplitSectionsForTesting(input);
+  ASSERT_TRUE(r.IsSuccess());
+
+  auto sections = p.Sections();
+  ASSERT_EQ(1U, sections.size());
+  EXPECT_EQ(NodeType::kShader, sections[0].section_type);
+  EXPECT_EQ(ShaderType::kVertex, sections[0].shader_type);
+  EXPECT_EQ(ShaderFormat::kGlsl, sections[0].format);
+  EXPECT_EQ(shader, sections[0].contents);
+}
+
+TEST_F(SectionParserTest, ParseShaderGlslVertexPassthrough) {
+  std::string input = "[vertex shader passthrough]";
+
+  SectionParser p;
+  Result r = p.SplitSectionsForTesting(input);
+  ASSERT_TRUE(r.IsSuccess());
+
+  auto sections = p.Sections();
+  ASSERT_EQ(1U, sections.size());
+  EXPECT_EQ(NodeType::kShader, sections[0].section_type);
+  EXPECT_EQ(ShaderType::kVertex, sections[0].shader_type);
+  EXPECT_EQ(ShaderFormat::kSpirvAsm, sections[0].format);
+  EXPECT_EQ(kPassThroughShader, sections[0].contents);
+}
+
+TEST_F(SectionParserTest, SectionParserMultipleSections) {
+  std::string input = R"(
+[comment]
+This is a test.
+
+[vertex shader passthrough]
+[fragment shader]
+#version 430
+void main() {}
+
+[geometry shader]
+float4 main() {}
+
+[comment]
+Another comment section.
+Multi line.
+
+[indices]
+1 2 3 4
+5 6 7 8
+[test]
+test body.)";
+
+  SectionParser p;
+  Result r = p.SplitSectionsForTesting(input);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto sections = p.Sections();
+  ASSERT_EQ(5U, sections.size());
+
+  // Passthrough vertext shader
+  EXPECT_EQ(NodeType::kShader, sections[0].section_type);
+  EXPECT_EQ(ShaderType::kVertex, sections[0].shader_type);
+  EXPECT_EQ(ShaderFormat::kSpirvAsm, sections[0].format);
+  EXPECT_EQ(kPassThroughShader, sections[0].contents);
+
+  // fragment shader
+  EXPECT_EQ(NodeType::kShader, sections[1].section_type);
+  EXPECT_EQ(ShaderType::kFragment, sections[1].shader_type);
+  EXPECT_EQ(ShaderFormat::kGlsl, sections[1].format);
+  EXPECT_EQ("#version 430\nvoid main() {}", sections[1].contents);
+
+  // geometry shader
+  EXPECT_EQ(NodeType::kShader, sections[2].section_type);
+  EXPECT_EQ(ShaderType::kGeometry, sections[2].shader_type);
+  EXPECT_EQ(ShaderFormat::kGlsl, sections[2].format);
+  EXPECT_EQ("float4 main() {}", sections[2].contents);
+
+  // indices
+  EXPECT_EQ(NodeType::kIndices, sections[3].section_type);
+  EXPECT_EQ(ShaderFormat::kText, sections[3].format);
+  EXPECT_EQ("1 2 3 4\n5 6 7 8", sections[3].contents);
+
+  // test
+  EXPECT_EQ(NodeType::kTest, sections[4].section_type);
+  EXPECT_EQ(ShaderFormat::kText, sections[4].format);
+  EXPECT_EQ("test body.", sections[4].contents);
+}
+
+TEST_F(SectionParserTest, SkipCommentLinesOutsideSections) {
+  std::string input = "# comment 1\n#comment 2\r\n[vertex shader]";
+
+  SectionParser p;
+  Result r = p.SplitSectionsForTesting(input);
+  ASSERT_TRUE(r.IsSuccess());
+
+  auto sections = p.Sections();
+  ASSERT_EQ(1U, sections.size());
+  EXPECT_EQ(NodeType::kShader, sections[0].section_type);
+  EXPECT_EQ(ShaderType::kVertex, sections[0].shader_type);
+  EXPECT_EQ(ShaderFormat::kGlsl, sections[0].format);
+  EXPECT_EQ("", sections[0].contents);
+}
+
+TEST_F(SectionParserTest, SkipBlankLinesOutsideSections) {
+  std::string input = "\n\r\n[vertex shader]";
+
+  SectionParser p;
+  Result r = p.SplitSectionsForTesting(input);
+  ASSERT_TRUE(r.IsSuccess()) << r.Error();
+
+  auto sections = p.Sections();
+  ASSERT_EQ(1U, sections.size());
+  EXPECT_EQ(NodeType::kShader, sections[0].section_type);
+  EXPECT_EQ(ShaderType::kVertex, sections[0].shader_type);
+  EXPECT_EQ(ShaderFormat::kGlsl, sections[0].format);
+  EXPECT_EQ("", sections[0].contents);
+}
+
+TEST_F(SectionParserTest, UnknownTextOutsideSection) {
+  std::string input = "Invalid Text";
+
+  SectionParser p;
+  Result r = p.SplitSectionsForTesting(input);
+  EXPECT_FALSE(r.IsSuccess());
+  EXPECT_EQ("1: Invalid character", r.Error());
+}
+
+TEST_F(SectionParserTest, UnknownSectionName) {
+  std::string input = "[Invalid Section]";
+
+  SectionParser p;
+  Result r = p.SplitSectionsForTesting(input);
+  EXPECT_FALSE(r.IsSuccess());
+  EXPECT_EQ("1: Invalid name: Invalid Section", r.Error());
+}
+
+TEST_F(SectionParserTest, MissingSectionClose) {
+  std::string input = "[vertex shader\nMore Content";
+
+  SectionParser p;
+  Result r = p.SplitSectionsForTesting(input);
+  EXPECT_FALSE(r.IsSuccess());
+  EXPECT_EQ("1: Missing section close", r.Error());
+}
+
+TEST_F(SectionParserTest, NameToNodeType) {
+  struct {
+    const char* name;
+    NodeType section_type;
+    ShaderType shader_type;
+    ShaderFormat fmt;
+  } name_cases[] = {
+      {"comment", NodeType::kComment, ShaderType::kVertex, ShaderFormat::kText},
+      {"indices", NodeType::kIndices, ShaderType::kVertex, ShaderFormat::kText},
+      {"require", NodeType::kRequire, ShaderType::kVertex, ShaderFormat::kText},
+      {"test", NodeType::kTest, ShaderType::kVertex, ShaderFormat::kText},
+      {"vertex data", NodeType::kVertexData, ShaderType::kVertex,
+       ShaderFormat::kText},
+
+      {"compute shader", NodeType::kShader, ShaderType::kCompute,
+       ShaderFormat::kGlsl},
+      {"fragment shader", NodeType::kShader, ShaderType::kFragment,
+       ShaderFormat::kGlsl},
+      {"geometry shader", NodeType::kShader, ShaderType::kGeometry,
+       ShaderFormat::kGlsl},
+      {"tessellation control shader", NodeType::kShader,
+       ShaderType::kTessellationControl, ShaderFormat::kGlsl},
+      {"tessellation evaluation shader", NodeType::kShader,
+       ShaderType::kTessellationEvaluation, ShaderFormat::kGlsl},
+      {"vertex shader", NodeType::kShader, ShaderType::kVertex,
+       ShaderFormat::kGlsl},
+      {"compute shader spirv", NodeType::kShader, ShaderType::kCompute,
+       ShaderFormat::kSpirvAsm},
+      {"fragment shader spirv", NodeType::kShader, ShaderType::kFragment,
+       ShaderFormat::kSpirvAsm},
+      {"geometry shader spirv", NodeType::kShader, ShaderType::kGeometry,
+       ShaderFormat::kSpirvAsm},
+      {"tessellation control shader spirv", NodeType::kShader,
+       ShaderType::kTessellationControl, ShaderFormat::kSpirvAsm},
+      {"tessellation evaluation shader spirv", NodeType::kShader,
+       ShaderType::kTessellationEvaluation, ShaderFormat::kSpirvAsm},
+      {"vertex shader spirv", NodeType::kShader, ShaderType::kVertex,
+       ShaderFormat::kSpirvAsm},
+      {"compute shader spirv hex", NodeType::kShader, ShaderType::kCompute,
+       ShaderFormat::kSpirvHex},
+      {"fragment shader spirv hex", NodeType::kShader, ShaderType::kFragment,
+       ShaderFormat::kSpirvHex},
+      {"geometry shader spirv hex", NodeType::kShader, ShaderType::kGeometry,
+       ShaderFormat::kSpirvHex},
+      {"tessellation control shader spirv hex", NodeType::kShader,
+       ShaderType::kTessellationControl, ShaderFormat::kSpirvHex},
+      {"tessellation evaluation shader spirv hex", NodeType::kShader,
+       ShaderType::kTessellationEvaluation, ShaderFormat::kSpirvHex},
+      {"vertex shader spirv hex", NodeType::kShader, ShaderType::kVertex,
+       ShaderFormat::kSpirvHex},
+      {"vertex shader passthrough", NodeType::kShader, ShaderType::kVertex,
+       ShaderFormat::kDefault}};
+
+  for (auto name_case : name_cases) {
+    NodeType section_type = NodeType::kTest;
+    ShaderType shader_type = ShaderType::kVertex;
+    ShaderFormat fmt = ShaderFormat::kText;
+    SectionParser p;
+    Result r = p.NameToNodeTypeForTesting(name_case.name, &section_type,
+                                          &shader_type, &fmt);
+
+    ASSERT_TRUE(r.IsSuccess()) << r.Error();
+    EXPECT_EQ(name_case.section_type, section_type) << name_case.name;
+    EXPECT_EQ(name_case.shader_type, shader_type) << name_case.name;
+    EXPECT_EQ(name_case.fmt, fmt) << name_case.name;
+  }
+}
+
+TEST_F(SectionParserTest, NameToNodeTypeInvalidName) {
+  NodeType section_type = NodeType::kTest;
+  ShaderType shader_type = ShaderType::kVertex;
+  ShaderFormat fmt = ShaderFormat::kText;
+  SectionParser p;
+  Result r = p.NameToNodeTypeForTesting("InvalidName", &section_type,
+                                        &shader_type, &fmt);
+  ASSERT_FALSE(r.IsSuccess());
+  EXPECT_EQ("Invalid name: InvalidName", r.Error());
+}
+
+TEST_F(SectionParserTest, NameToSectionInvalidSuffix) {
+  struct {
+    const char* name;
+  } cases[] = {{"comment spirv"},     {"indices spirv"},
+               {"require spirv"},     {"test spirv"},
+               {"vertex data spirv"}, {"comment spirv hex"},
+               {"indices spirv hex"}, {"require spirv hex"},
+               {"test spirv hex"},    {"vertex data spirv hex"}};
+
+  for (auto name_case : cases) {
+    NodeType section_type = NodeType::kTest;
+    ShaderType shader_type = ShaderType::kVertex;
+    ShaderFormat fmt = ShaderFormat::kText;
+    SectionParser p;
+
+    Result r = p.NameToNodeTypeForTesting(name_case.name, &section_type,
+                                          &shader_type, &fmt);
+    ASSERT_FALSE(r.IsSuccess()) << name_case.name;
+    EXPECT_EQ("Invalid source format: " + std::string(name_case.name),
+              r.Error());
+  }
+}
+
+TEST_F(SectionParserTest, HasShader) {
+  EXPECT_TRUE(SectionParser::HasShader(NodeType::kShader));
+}
+
+TEST_F(SectionParserTest, HasNoShader) {
+  const NodeType false_types[] = {NodeType::kComment, NodeType::kTest,
+                                  NodeType::kIndices, NodeType::kVertexData,
+                                  NodeType::kRequire};
+  for (auto type : false_types) {
+    EXPECT_FALSE(SectionParser::HasShader(type));
+  }
+}
+
+}  // namespace vkscript
+}  // namespace amber
diff --git a/src/vulkan/CMakeLists.txt b/src/vulkan/CMakeLists.txt
new file mode 100644
index 0000000..8df5759
--- /dev/null
+++ b/src/vulkan/CMakeLists.txt
@@ -0,0 +1,43 @@
+# Copyright 2018 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 implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set(VULKAN_ENGINE_SOURCES
+    bit_copy.cc
+    buffer.cc
+    command.cc
+    descriptor.cc
+    device.cc
+    engine_vulkan.cc
+    format_data.cc
+    frame_buffer.cc
+    resource.cc
+    image.cc
+    pipeline.cc
+    graphics_pipeline.cc
+    vertex_buffer.cc
+)
+
+add_library(libamberenginevulkan ${VULKAN_ENGINE_SOURCES})
+amber_default_compile_options(libamberenginevulkan)
+set_target_properties(libamberenginevulkan PROPERTIES
+    OUTPUT_NAME "amberenginevulkan"
+)
+target_link_libraries(libamberenginevulkan ${VULKAN_LIB})
+
+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
+  # with XCode 10.
+  target_compile_options(libamberenginevulkan PRIVATE -Wno-zero-as-null-pointer-constant)
+endif()
diff --git a/src/vulkan/bit_copy.cc b/src/vulkan/bit_copy.cc
new file mode 100644
index 0000000..3be826f
--- /dev/null
+++ b/src/vulkan/bit_copy.cc
@@ -0,0 +1,166 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vulkan/bit_copy.h"
+
+#include <cassert>
+#include <cstring>
+
+namespace amber {
+namespace vulkan {
+
+// static
+void BitCopy::ShiftBufferBits(uint8_t* buffer,
+                              uint8_t length_bytes,
+                              uint8_t shift_bits) {
+  if (shift_bits == 0)
+    return;
+
+  assert(shift_bits < 8);
+
+  uint8_t carry = 0;
+  for (uint32_t i = 0; i < length_bytes; ++i) {
+    uint8_t new_value = static_cast<uint8_t>(buffer[i] << shift_bits) | carry;
+    carry = buffer[i] >> (8 - shift_bits);
+    buffer[i] = new_value;
+  }
+}
+
+// static
+void BitCopy::CopyValueToBuffer(uint8_t* dst,
+                                const Value& src,
+                                uint8_t bit_offset,
+                                uint8_t bits) {
+  uint8_t data[9] = {};
+  if (src.IsInteger()) {
+    if (bits <= 8) {
+      uint8_t data_uint8 = src.AsUint8();
+      data[0] = data_uint8;
+    } else if (bits <= 16) {
+      uint16_t data_uint16 = src.AsUint16();
+      std::memcpy(data, &data_uint16, sizeof(uint16_t));
+    } else if (bits <= 32) {
+      uint32_t data_uint32 = src.AsUint32();
+      std::memcpy(data, &data_uint32, sizeof(uint32_t));
+    } else if (bits <= 64) {
+      uint64_t data_uint64 = src.AsUint64();
+      std::memcpy(data, &data_uint64, sizeof(uint64_t));
+    } else {
+      assert(false && "Invalid int bits for CopyBits");
+    }
+  } else {
+    if (bits == 64) {
+      double data_double = src.AsDouble();
+      std::memcpy(data, &data_double, sizeof(double));
+    } else {
+      float data_float = src.AsFloat();
+      uint16_t hex_float = 0;
+      switch (bits) {
+        case 32:
+          std::memcpy(data, &data_float, sizeof(float));
+          break;
+        case 16:
+        case 11:
+        case 10:
+          hex_float = FloatToHexFloat(data_float, bits);
+          std::memcpy(data, &hex_float, sizeof(uint16_t));
+          break;
+        default:
+          assert(false && "Invalid float bits for CopyBits");
+          break;
+      }
+    }
+  }
+
+  while (bit_offset > 7) {
+    ++dst;
+    bit_offset -= 8;
+  }
+
+  ShiftBufferBits(data, ((bit_offset + bits - 1) / 8) + 1, bit_offset);
+  CopyBits(dst, data, bit_offset, bits);
+}
+
+// static
+void BitCopy::CopyBits(uint8_t* dst,
+                       const uint8_t* src,
+                       uint8_t bit_offset,
+                       uint8_t bits) {
+  while (bit_offset + bits > 0) {
+    uint8_t target_bits = bits;
+    if (bit_offset + target_bits > 8)
+      target_bits = 8 - bit_offset;
+
+    uint8_t bit_mask =
+        static_cast<uint8_t>(((1U << target_bits) - 1U) << bit_offset);
+    *dst = (*src & bit_mask) | (*dst & ~bit_mask);
+
+    bit_offset -= bit_offset;
+    bits -= target_bits;
+    ++dst;
+    ++src;
+  }
+}
+
+// static
+uint16_t BitCopy::FloatExponent(const uint32_t hex_float) {
+  uint32_t exponent = ((hex_float >> 23U) & ((1U << 8U) - 1U)) - 127U + 15U;
+  const uint32_t half_exponent_mask = (1U << 5U) - 1U;
+  assert((exponent & ~half_exponent_mask) == 0U);
+  return static_cast<uint16_t>(exponent & half_exponent_mask);
+}
+
+// static
+uint16_t BitCopy::FloatToHexFloat(float value, uint8_t bits) {
+  switch (bits) {
+    case 10:
+      return FloatToHexFloat10(value);
+    case 11:
+      return FloatToHexFloat11(value);
+    case 16:
+      return FloatToHexFloat16(value);
+  }
+
+  assert(false && "Invalid bits");
+  return 0;
+}
+
+// static
+uint16_t BitCopy::FloatToHexFloat16(const float value) {
+  uint32_t hex;
+  memcpy(&hex, &value, sizeof(float));
+  return static_cast<uint16_t>(FloatSign(hex) << 15U) |
+         static_cast<uint16_t>(FloatExponent(hex) << 10U) | FloatMantissa(hex);
+}
+
+// static
+uint16_t BitCopy::FloatToHexFloat11(const float value) {
+  uint32_t hex;
+  memcpy(&hex, &value, sizeof(float));
+  assert(FloatSign(hex) == 0);
+  return static_cast<uint16_t>(FloatExponent(hex) << 6U) |
+         static_cast<uint16_t>(FloatMantissa(hex) >> 4U);
+}
+
+// static
+uint16_t BitCopy::FloatToHexFloat10(const float value) {
+  uint32_t hex;
+  memcpy(&hex, &value, sizeof(float));
+  assert(FloatSign(hex) == 0);
+  return static_cast<uint16_t>(FloatExponent(hex) << 5U) |
+         static_cast<uint16_t>(FloatMantissa(hex) >> 5U);
+}
+
+}  // namespace vulkan
+}  // namespace amber
diff --git a/src/vulkan/bit_copy.h b/src/vulkan/bit_copy.h
new file mode 100644
index 0000000..31b19bc
--- /dev/null
+++ b/src/vulkan/bit_copy.h
@@ -0,0 +1,82 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VULKAN_BIT_COPY_H_
+#define SRC_VULKAN_BIT_COPY_H_
+
+#include "src/value.h"
+
+namespace amber {
+namespace vulkan {
+
+class BitCopy {
+ public:
+  // Copy [0, bits) bits of src to [bit_offset, bit_offset + bits) of dst.
+  static void CopyValueToBuffer(uint8_t* dst,
+                                const Value& src,
+                                uint8_t bit_offset,
+                                uint8_t bits);
+
+ private:
+  BitCopy() = delete;
+
+  ~BitCopy() = delete;
+
+  static void ShiftBufferBits(uint8_t* buffer,
+                              uint8_t length_bytes,
+                              uint8_t shift_bits);
+
+  static void CopyBits(uint8_t* dst,
+                       const uint8_t* src,
+                       uint8_t bit_offset,
+                       uint8_t bits);
+
+  // Convert float to small float format.
+  // See https://www.khronos.org/opengl/wiki/Small_Float_Formats
+  // and https://en.wikipedia.org/wiki/IEEE_754.
+  //
+  //    Sign Exponent Mantissa Exponent-Bias
+  // 16    1        5       10            15
+  // 11    0        5        6            15
+  // 10    0        5        5            15
+  // 32    1        8       23           127
+  // 64    1       11       52          1023
+  //
+  // 11 and 10 bits floats are always positive.
+  // 14 bits float is used only RGB9_E5 format but it does not exist in Vulkan.
+  //
+  // For example, 1234 in 32 bits float = 1.0011010010 * 2^10 with base 2.
+  //
+  // 1.0011010010 * 2^10 --> 0 (sign) / 10 + 127 (exp) / 0011010010 (Mantissa)
+  //                     --> 0x449a4000
+  static uint16_t FloatToHexFloat(float value, uint8_t bits);
+  static uint16_t FloatToHexFloat16(const float value);
+  static uint16_t FloatToHexFloat11(const float value);
+  static uint16_t FloatToHexFloat10(const float value);
+
+  static uint16_t FloatSign(const uint32_t hex_float) {
+    return static_cast<uint16_t>(hex_float >> 31U);
+  }
+
+  static uint16_t FloatExponent(const uint32_t hex_float);
+
+  static uint16_t FloatMantissa(const uint32_t hex_float) {
+    return static_cast<uint16_t>((hex_float & ((1U << 23U) - 1U)) >> 13U);
+  }
+};
+
+}  // namespace vulkan
+}  // namespace amber
+
+#endif  // SRC_VULKAN_BIT_COPY_H_
diff --git a/src/vulkan/bit_copy_test.cc b/src/vulkan/bit_copy_test.cc
new file mode 100644
index 0000000..b9367c5
--- /dev/null
+++ b/src/vulkan/bit_copy_test.cc
@@ -0,0 +1,382 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vulkan/bit_copy.h"
+
+#include <cstring>
+#include <type_traits>
+
+#include "gtest/gtest.h"
+#include "src/value.h"
+
+namespace amber {
+namespace vulkan {
+namespace {
+
+template <typename T>
+void ExpectBitsEQ(const uint8_t* actual, T expected) {
+  const T* ptr = reinterpret_cast<const T*>(actual);
+  EXPECT_EQ(*ptr, expected);
+}
+
+}  // namespace
+
+using BitCopyTest = testing::Test;
+
+TEST_F(BitCopyTest, CopyInt8) {
+  Value value;
+  uint8_t data = 0;
+
+  // 7      0          0      7
+  // 00000000      --> 11011100 (220)
+  // 110111   (55)
+  value.SetIntValue(55);
+  BitCopy::CopyValueToBuffer(&data, value, 2, 6);
+  EXPECT_EQ(data, 220);
+
+  // 7      0          0      7
+  // 11011100      --> 11011111 (223)
+  //       11 (3)
+  value.SetIntValue(3);
+  BitCopy::CopyValueToBuffer(&data, value, 0, 2);
+  EXPECT_EQ(data, 223);
+
+  // 7      0          0      7
+  // 11011111      --> 10110111 (183)
+  //  011011  (27)
+  value.SetIntValue(27);
+  BitCopy::CopyValueToBuffer(&data, value, 1, 6);
+  EXPECT_EQ(data, 183);
+
+  // 7      0          0      7
+  // 10110111      --> 11010111 (215)
+  //  1010    (10)
+  value.SetIntValue(10);
+  BitCopy::CopyValueToBuffer(&data, value, 3, 4);
+  EXPECT_EQ(data, 215);
+}
+
+TEST_F(BitCopyTest, CopyInt16) {
+  Value value;
+  uint8_t data[2] = {};
+
+  // 15              0          15              0
+  //  0000000000000000      -->  1100000011100100 (49380)
+  //  11000000111001   (12345)
+  value.SetIntValue(12345);
+  BitCopy::CopyValueToBuffer(data, value, 2, 14);
+  ExpectBitsEQ<uint16_t>(data, 49380);
+
+  // 15              0          15              0
+  //  1100000011100100      -->  1110100000100100 (59428)
+  //    101000001      (321)
+  value.SetIntValue(321);
+  BitCopy::CopyValueToBuffer(data, value, 5, 9);
+  ExpectBitsEQ<uint16_t>(data, 59428);
+
+  // 15              0          15              0
+  //  1110100000100100      -->  1111000111010111 (61911)
+  //     1000111010111 (4567)
+  value.SetIntValue(4567);
+  BitCopy::CopyValueToBuffer(data, value, 0, 13);
+  ExpectBitsEQ<uint16_t>(data, 61911);
+
+  // 15              0          15              0
+  //  1111000111010111      -->  1001101111011111 (39903)
+  //   001101111011    (891)
+  value.SetIntValue(891);
+  BitCopy::CopyValueToBuffer(data, value, 3, 12);
+  ExpectBitsEQ<uint16_t>(data, 39903);
+}
+
+TEST_F(BitCopyTest, CopyInt32) {
+  Value value;
+  uint8_t data[4] = {};
+
+  // 31                         31
+  //  0000000000000000      -->  0001011110001100
+  //  0000000000000000           0010100111000000 (395061696)
+  //                 0                          0
+  //
+  //     1011110001100
+  //  00101001110      (12345678)
+  value.SetIntValue(12345678);
+  BitCopy::CopyValueToBuffer(data, value, 5, 24);
+  ExpectBitsEQ<uint32_t>(data, 395061696);
+
+  // 31                         31
+  //  0001011110001100      -->  0001011110000001
+  //  0010100111000000           1100110111000000 (394382784)
+  //                 0                          0
+  //
+  //         110000001
+  //  11001101         (98765)
+  value.SetIntValue(98765);
+  BitCopy::CopyValueToBuffer(data, value, 8, 17);
+  ExpectBitsEQ<uint32_t>(data, 394382784);
+
+  // 31                         31
+  //  0001011110000001      -->  0001011110000001
+  //  1100110111000000           1100111010001110 (394382990)
+  //                 0                          0
+  //
+  //        1010001110 (654)
+  value.SetIntValue(654);
+  BitCopy::CopyValueToBuffer(data, value, 0, 10);
+  ExpectBitsEQ<uint32_t>(data, 394382990);
+
+  // 31                         31
+  //  0001011110000001      -->  1101001011111100
+  //  1100111010001110           0101011010001110 (3539752590)
+  //                 0                          0
+  //
+  //  1101001011111100
+  //  010101           (654)
+  value.SetIntValue(3456789);
+  BitCopy::CopyValueToBuffer(data, value, 10, 22);
+  ExpectBitsEQ<uint32_t>(data, 3539752590);
+}
+
+TEST_F(BitCopyTest, CopyInt64) {
+  Value value;
+  uint8_t data[8] = {};
+
+  // 63                         63
+  //  0000000000000000      -->  0010001111101110
+  //  0000000000000000           0011111101100110
+  //  0000000000000000           0001011110101100
+  //  0000000000000000           0000000000000000 (2589076543500976128)
+  //                 0                          0
+  //
+  //    10001111101110
+  //  0011111101100110
+  //  00010111101011
+  //                   (9876543210987)
+  value.SetIntValue(9876543210987UL);
+  BitCopy::CopyValueToBuffer(data, value, 18, 44);
+  ExpectBitsEQ<uint64_t>(data, 2589076543500976128UL);
+
+  // 63                         63
+  //  0010001111101110      -->  0011110001001110
+  //  0011111101100110           1111110000011110
+  //  0001011110101100           1111010011010001
+  //  0000000000000000           0101111101011000 (4345687900345687896)
+  //                 0                          0
+  //
+  //    11110001001110
+  //  1111110000011110
+  //  1111010011010001
+  //  0101111101011    (543210987543210987)
+  value.SetIntValue(543210987543210987UL);
+  BitCopy::CopyValueToBuffer(data, value, 3, 59);
+  ExpectBitsEQ<uint64_t>(data, 4345687900345687896UL);
+
+  // 63                         63
+  //  0011110001001110      -->  0011110001001110
+  //  1111110000011110           1001011111100010
+  //  1111010011010001           1011010011010001
+  //  0101111101011000           0101111101011000 (4345577690411130712)
+  //                 0                          0
+  //
+  //               110
+  //  1001011111100010
+  //  101
+  //                   (3456789)
+  value.SetIntValue(3456789UL);
+  BitCopy::CopyValueToBuffer(data, value, 29, 22);
+  ExpectBitsEQ<uint64_t>(data, 4345577690411130712UL);
+}
+
+TEST_F(BitCopyTest, CopyIntMultiple) {
+  uint8_t data[32] = {};
+  Value value;
+
+  // Fill [0, 32) bits of data with
+  // 11(3) / 0010001111(143) / 0001000011(67) / 1000010001(529)
+  value.SetIntValue(529);
+  BitCopy::CopyValueToBuffer(data, value, 0, 10);
+  value.SetIntValue(67);
+  BitCopy::CopyValueToBuffer(data, value, 10, 10);
+  value.SetIntValue(143);
+  BitCopy::CopyValueToBuffer(data, value, 20, 10);
+  value.SetIntValue(3);
+  BitCopy::CopyValueToBuffer(data, value, 30, 2);
+
+  // Fill [32, 96) bits of data with
+  // 00000111010110111100110100010101(123456789) /
+  // 00000000100101101011010000111111(9876543)
+  value.SetIntValue(9876543);
+  BitCopy::CopyValueToBuffer(data, value, 32, 32);
+  value.SetIntValue(123456789);
+  BitCopy::CopyValueToBuffer(data, value, 64, 32);
+
+  // Fill [96, 120) bits of data with
+  // 00011111(31) / 00001001(9) / 01001101(77)
+  value.SetIntValue(77);
+  BitCopy::CopyValueToBuffer(data, value, 96, 8);
+  value.SetIntValue(9);
+  BitCopy::CopyValueToBuffer(data, value, 104, 8);
+  value.SetIntValue(31);
+  BitCopy::CopyValueToBuffer(data, value, 112, 8);
+
+  // Fill [120, 184) bits of data with
+  // 00000001101101101001101101001011
+  // 10100110001100001111001101001110(123456789012345678)
+  value.SetIntValue(123456789012345678UL);
+  BitCopy::CopyValueToBuffer(data, value, 120, 64);
+
+  // Fill [184, 216) bits of data with
+  // 10000011110111011011010010000000(34567890)
+  value.SetIntValue(34567890);
+  BitCopy::CopyValueToBuffer(data, value, 184, 32);
+
+  // Fill [216, 256) bits of data with
+  // 01100011(99) / 1000001000110101(33333) / 11011110(222) / 01101111(111)
+  value.SetIntValue(111);
+  BitCopy::CopyValueToBuffer(data, value, 216, 8);
+  value.SetIntValue(222);
+  BitCopy::CopyValueToBuffer(data, value, 224, 8);
+  value.SetIntValue(33333);
+  BitCopy::CopyValueToBuffer(data, value, 232, 16);
+  value.SetIntValue(99);
+  BitCopy::CopyValueToBuffer(data, value, 248, 8);
+
+  // [0, 32) bits of data
+  ExpectBitsEQ<uint32_t>(data, 3371240977);
+
+  // [32, 96) bits of data
+  ExpectBitsEQ<uint64_t>(&data[4], 530242871234049087UL);
+
+  // [96, 120) bits of data
+  ExpectBitsEQ<uint16_t>(&data[12], 2381);
+  EXPECT_EQ(data[14], 31);
+
+  // [120, 184) bits of data
+  ExpectBitsEQ<uint64_t>(&data[15], 123456789012345678UL);
+
+  // [184, 216) bits of data
+  ExpectBitsEQ<uint32_t>(&data[23], 34567890);
+
+  // [216, 256) bits of data
+  ExpectBitsEQ<uint32_t>(&data[27], 2184568431);
+  EXPECT_EQ(data[31], 99);
+}
+
+TEST_F(BitCopyTest, CopyFloat16) {
+  uint8_t data[2] = {};
+  Value value;
+
+  // 16 bits
+  //         Sig / Exp / Mantissa     Sig / Exp / Mantissa
+  // 12.34 =   0 / 130 /  4550820 -->   0 /  18 /      555
+  value.SetDoubleValue(12.34);
+  data[0] = 0;
+  data[1] = 0;
+  BitCopy::CopyValueToBuffer(data, value, 0, 16);
+  ExpectBitsEQ<uint16_t>(data, 18987);
+
+  // 11 bits
+  //         Sig / Exp / Mantissa     Sig / Exp / Mantissa
+  // 5.67 =    0 / 129 /  3502244 -->        17 /       26
+  value.SetDoubleValue(5.67);
+  data[0] = 0;
+  data[1] = 0;
+  BitCopy::CopyValueToBuffer(data, value, 3, 11);
+  ExpectBitsEQ<uint16_t>(data, 8912);
+
+  // 10 bits
+  //         Sig / Exp / Mantissa     Sig / Exp / Mantissa
+  // 0.89 =    0 / 126 /  6543114 -->        14 /       24
+  value.SetDoubleValue(0.89);
+  data[0] = 0;
+  data[1] = 0;
+  BitCopy::CopyValueToBuffer(data, value, 2, 10);
+  ExpectBitsEQ<uint16_t>(data, 1888);
+}
+
+TEST_F(BitCopyTest, CopyFloat) {
+  uint8_t data[4] = {};
+  Value value;
+
+  // 16 bits
+  //         Sig / Exp / Mantissa
+  // 12.34 =   0 / 130 /  4550820
+  value.SetDoubleValue(12.34);
+  BitCopy::CopyValueToBuffer(data, value, 0, 32);
+  ExpectBitsEQ<uint32_t>(data, 1095069860);
+
+  // 11 bits
+  //         Sig / Exp / Mantissa
+  // 5.67 =    0 / 129 /  3502244
+  value.SetDoubleValue(5.67);
+  data[0] = 0;
+  data[1] = 0;
+  data[2] = 0;
+  data[3] = 0;
+  BitCopy::CopyValueToBuffer(data, value, 0, 32);
+  ExpectBitsEQ<uint32_t>(data, 1085632676);
+
+  // 10 bits
+  //         Sig / Exp / Mantissa
+  // 0.89 =    0 / 126 /  6543114
+  value.SetDoubleValue(0.89);
+  data[0] = 0;
+  data[1] = 0;
+  data[2] = 0;
+  data[3] = 0;
+  BitCopy::CopyValueToBuffer(data, value, 0, 32);
+  ExpectBitsEQ<uint32_t>(data, 1063507722);
+}
+
+TEST_F(BitCopyTest, CopyDouble) {
+  uint8_t data[8] = {};
+  Value value;
+
+  //         Sig /  Exp /         Mantissa
+  // 12.34 =   0 / 1026 / 2443202797848494
+  value.SetDoubleValue(12.34);
+  BitCopy::CopyValueToBuffer(data, value, 0, 64);
+  ExpectBitsEQ<uint64_t>(data, 4623136420479977390);
+
+  //         Sig /  Exp /         Mantissa
+  // 5.67 =    0 / 1025 / 1880252844427182
+  value.SetDoubleValue(5.67);
+  data[0] = 0;
+  data[1] = 0;
+  data[2] = 0;
+  data[3] = 0;
+  data[4] = 0;
+  data[5] = 0;
+  data[6] = 0;
+  data[7] = 0;
+  BitCopy::CopyValueToBuffer(data, value, 0, 64);
+  ExpectBitsEQ<uint64_t>(data, 4618069870899185582);
+
+  //         Sig /  Exp /         Mantissa
+  // 0.89 =    0 / 1022 / 3512807709348987
+  value.SetDoubleValue(0.89);
+  data[0] = 0;
+  data[1] = 0;
+  data[2] = 0;
+  data[3] = 0;
+  data[4] = 0;
+  data[5] = 0;
+  data[6] = 0;
+  data[7] = 0;
+  BitCopy::CopyValueToBuffer(data, value, 0, 64);
+  ExpectBitsEQ<uint64_t>(data, 4606191626881995899);
+}
+
+}  // namespace vulkan
+}  // namespace amber
diff --git a/src/vulkan/buffer.cc b/src/vulkan/buffer.cc
new file mode 100644
index 0000000..4c7d2fe
--- /dev/null
+++ b/src/vulkan/buffer.cc
@@ -0,0 +1,93 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vulkan/buffer.h"
+
+namespace amber {
+namespace vulkan {
+
+Buffer::Buffer(VkDevice device,
+               size_t size,
+               const VkPhysicalDeviceMemoryProperties& properties)
+    : Resource(device, size, properties) {}
+
+Buffer::~Buffer() = default;
+
+Result Buffer::Initialize(const VkBufferUsageFlags usage) {
+  Result r = CreateVkBuffer(&buffer_, usage);
+  if (!r.IsSuccess())
+    return r;
+
+  AllocateResult allocate_result = AllocateAndBindMemoryToVkBuffer(
+      buffer_, &memory_, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, false);
+  if (!allocate_result.r.IsSuccess())
+    return allocate_result.r;
+
+  if (CheckMemoryHostAccessible(allocate_result.memory_type_index)) {
+    is_buffer_host_accessible_ = true;
+    return MapMemory(memory_);
+  }
+
+  is_buffer_host_accessible_ = false;
+  return Resource::Initialize();
+}
+
+Result Buffer::CreateVkBufferView(VkFormat format) {
+  VkBufferViewCreateInfo buffer_view_info = {};
+  buffer_view_info.sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO;
+  buffer_view_info.buffer = buffer_;
+  buffer_view_info.format = format;
+  buffer_view_info.offset = 0;
+  buffer_view_info.range = VK_WHOLE_SIZE;
+  if (vkCreateBufferView(GetDevice(), &buffer_view_info, nullptr, &view_) !=
+      VK_SUCCESS) {
+    return Result("Vulkan::Calling vkCreateBufferView Fail");
+  }
+
+  return {};
+}
+
+void Buffer::CopyToDevice(VkCommandBuffer command) {
+  if (is_buffer_host_accessible_)
+    return;
+
+  VkBufferCopy region = {};
+  region.srcOffset = 0;
+  region.dstOffset = 0;
+  region.size = GetSize();
+
+  vkCmdCopyBuffer(command, GetHostAccessibleBuffer(), buffer_, 1, &region);
+}
+
+void Buffer::Shutdown() {
+  // TODO(jaebaek): Doublecheck what happens if |view_| is VK_NULL_HANDLE on
+  //                Android and Windows.
+  if (view_ != VK_NULL_HANDLE) {
+    vkDestroyBufferView(GetDevice(), view_, nullptr);
+    view_ = VK_NULL_HANDLE;
+  }
+
+  if (buffer_ != VK_NULL_HANDLE) {
+    vkDestroyBuffer(GetDevice(), buffer_, nullptr);
+    buffer_ = VK_NULL_HANDLE;
+  }
+
+  if (memory_ != VK_NULL_HANDLE) {
+    vkFreeMemory(GetDevice(), memory_, nullptr);
+    memory_ = VK_NULL_HANDLE;
+  }
+}
+
+}  // namespace vulkan
+}  // namespace amber
diff --git a/src/vulkan/buffer.h b/src/vulkan/buffer.h
new file mode 100644
index 0000000..9ec1dd2
--- /dev/null
+++ b/src/vulkan/buffer.h
@@ -0,0 +1,60 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VULKAN_BUFFER_H_
+#define SRC_VULKAN_BUFFER_H_
+
+#include "amber/result.h"
+#include "src/vulkan/resource.h"
+#include "vulkan/vulkan.h"
+
+namespace amber {
+namespace vulkan {
+
+class Buffer : public Resource {
+ public:
+  Buffer(VkDevice device,
+         size_t size,
+         const VkPhysicalDeviceMemoryProperties& properties);
+  ~Buffer() override;
+
+  Result Initialize(const VkBufferUsageFlags usage);
+  VkBuffer GetVkBuffer() const { return buffer_; }
+  Result CreateVkBufferView(VkFormat format);
+  VkBufferView GetVkBufferView() const { return view_; }
+
+  // TODO(jaebaek): Determine copy all or partial data
+  void CopyToDevice(VkCommandBuffer command);
+
+  // Resource
+  VkDeviceMemory GetHostAccessMemory() const override {
+    if (is_buffer_host_accessible_)
+      return memory_;
+
+    return Resource::GetHostAccessMemory();
+  }
+
+  void Shutdown() override;
+
+ private:
+  VkBuffer buffer_ = VK_NULL_HANDLE;
+  VkBufferView view_ = VK_NULL_HANDLE;
+  VkDeviceMemory memory_ = VK_NULL_HANDLE;
+  bool is_buffer_host_accessible_ = false;
+};
+
+}  // namespace vulkan
+}  // namespace amber
+
+#endif  // SRC_VULKAN_BUFFER_H_
diff --git a/src/vulkan/command.cc b/src/vulkan/command.cc
new file mode 100644
index 0000000..9ff205f
--- /dev/null
+++ b/src/vulkan/command.cc
@@ -0,0 +1,131 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vulkan/command.h"
+
+#include <memory>
+
+#include "src/make_unique.h"
+
+namespace amber {
+
+namespace vulkan {
+
+CommandPool::CommandPool(VkDevice device) : device_(device) {}
+
+CommandPool::~CommandPool() = default;
+
+Result CommandPool::Initialize(uint32_t queue_family_index) {
+  VkCommandPoolCreateInfo pool_info = {};
+  pool_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
+  pool_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
+  pool_info.queueFamilyIndex = queue_family_index;
+
+  if (vkCreateCommandPool(device_, &pool_info, nullptr, &pool_) != VK_SUCCESS)
+    return Result("Vulkan::Calling vkCreateCommandPool Fail");
+
+  return {};
+}
+
+void CommandPool::Shutdown() {
+  vkDestroyCommandPool(device_, pool_, nullptr);
+}
+
+CommandBuffer::CommandBuffer(VkDevice device, VkCommandPool pool, VkQueue queue)
+    : device_(device), pool_(pool), queue_(queue) {}
+
+CommandBuffer::~CommandBuffer() = default;
+
+Result CommandBuffer::Initialize() {
+  VkCommandBufferAllocateInfo command_info = {};
+  command_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
+  command_info.commandPool = pool_;
+  command_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
+  command_info.commandBufferCount = 1;
+
+  if (vkAllocateCommandBuffers(device_, &command_info, &command_) != VK_SUCCESS)
+    return Result("Vulkan::Calling vkAllocateCommandBuffers Fail");
+
+  VkFenceCreateInfo fence_info = {};
+  fence_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
+  if (vkCreateFence(device_, &fence_info, nullptr, &fence_) != VK_SUCCESS)
+    return Result("Vulkan::Calling vkCreateFence Fail");
+
+  return {};
+}
+
+Result CommandBuffer::BeginIfNotInRecording() {
+  if (state_ == CommandBufferState::kRecording)
+    return {};
+
+  if (state_ != CommandBufferState::kInitial)
+    return Result("Vulkan::Begin CommandBuffer from Not Valid State");
+
+  VkCommandBufferBeginInfo command_begin_info = {};
+  command_begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
+  command_begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
+  if (vkBeginCommandBuffer(command_, &command_begin_info) != VK_SUCCESS)
+    return Result("Vulkan::Calling vkBeginCommandBuffer Fail");
+
+  state_ = CommandBufferState::kRecording;
+  return {};
+}
+
+Result CommandBuffer::End() {
+  if (state_ != CommandBufferState::kRecording)
+    return Result("Vulkan::End CommandBuffer from Not Valid State");
+
+  if (vkEndCommandBuffer(command_) != VK_SUCCESS)
+    return Result("Vulkan::Calling vkEndCommandBuffer Fail");
+
+  state_ = CommandBufferState::kExecutable;
+  return {};
+}
+
+Result CommandBuffer::SubmitAndReset() {
+  if (state_ != CommandBufferState::kExecutable)
+    return Result("Vulkan::Submit CommandBuffer from Not Valid State");
+
+  if (vkResetFences(device_, 1, &fence_) != VK_SUCCESS)
+    return Result("Vulkan::Calling vkResetFences Fail");
+
+  VkSubmitInfo submit_info = {};
+  submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
+  submit_info.commandBufferCount = 1;
+  submit_info.pCommandBuffers = &command_;
+  if (vkQueueSubmit(queue_, 1, &submit_info, fence_) != VK_SUCCESS)
+    return Result("Vulkan::Calling vkQueueSubmit Fail");
+
+  VkResult r =
+      vkWaitForFences(device_, 1, &fence_, VK_TRUE, 100000000 /* nanosecond */);
+  if (r == VK_TIMEOUT)
+    return Result("Vulkan::Calling vkWaitForFences Timeout");
+  if (r != VK_SUCCESS)
+    return Result("Vulkan::Calling vkWaitForFences Fail");
+
+  if (vkResetCommandBuffer(command_, 0) != VK_SUCCESS)
+    return Result("Vulkan::Calling vkResetCommandBuffer Fail");
+
+  state_ = CommandBufferState::kInitial;
+  return {};
+}
+
+void CommandBuffer::Shutdown() {
+  vkDestroyFence(device_, fence_, nullptr);
+  vkFreeCommandBuffers(device_, pool_, 1, &command_);
+}
+
+}  // namespace vulkan
+
+}  // namespace amber
diff --git a/src/vulkan/command.h b/src/vulkan/command.h
new file mode 100644
index 0000000..97bd521
--- /dev/null
+++ b/src/vulkan/command.h
@@ -0,0 +1,79 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VULKAN_COMMAND_H_
+#define SRC_VULKAN_COMMAND_H_
+
+#include "amber/result.h"
+#include "vulkan/vulkan.h"
+
+namespace amber {
+
+namespace vulkan {
+
+// Command buffer states based on "5.1. Command Buffer Lifecycle" of Vulkan
+// spec
+enum class CommandBufferState : uint8_t {
+  kInitial = 0,
+  kRecording,
+  kExecutable,
+  kPending,
+  kInvalid,
+};
+
+class CommandPool {
+ public:
+  explicit CommandPool(VkDevice device);
+  ~CommandPool();
+
+  Result Initialize(uint32_t queue_family_index);
+  VkCommandPool GetCommandPool() const { return pool_; }
+  void Shutdown();
+
+ private:
+  VkDevice device_ = VK_NULL_HANDLE;
+  VkCommandPool pool_ = VK_NULL_HANDLE;
+};
+
+class CommandBuffer {
+ public:
+  CommandBuffer(VkDevice device, VkCommandPool pool, VkQueue queue);
+  ~CommandBuffer();
+
+  Result Initialize();
+  VkCommandBuffer GetCommandBuffer() const { return command_; }
+  void Shutdown();
+
+  // Do nothing and return if it is already ready to record. If it is in
+  // initial state, call command begin API and make it ready to record.
+  // Otherwise, report error.
+  Result BeginIfNotInRecording();
+
+  Result End();
+  Result SubmitAndReset();
+
+ private:
+  VkDevice device_ = VK_NULL_HANDLE;
+  VkCommandPool pool_ = VK_NULL_HANDLE;
+  VkQueue queue_ = VK_NULL_HANDLE;
+  VkCommandBuffer command_ = VK_NULL_HANDLE;
+  VkFence fence_ = VK_NULL_HANDLE;
+  CommandBufferState state_ = CommandBufferState::kInitial;
+};
+
+}  // namespace vulkan
+
+}  // namespace amber
+
+#endif  // SRC_VULKAN_COMMAND_H_
diff --git a/src/vulkan/descriptor.cc b/src/vulkan/descriptor.cc
new file mode 100644
index 0000000..294e3b6
--- /dev/null
+++ b/src/vulkan/descriptor.cc
@@ -0,0 +1,58 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vulkan/descriptor.h"
+
+#include <cassert>
+
+namespace amber {
+namespace vulkan {
+
+VkDescriptorType ToVkDescriptorType(DescriptorType type) {
+  switch (type) {
+    case DescriptorType::kStorageImage:
+      return VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
+    case DescriptorType::kSampler:
+      return VK_DESCRIPTOR_TYPE_SAMPLER;
+    case DescriptorType::kSampledImage:
+      return VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE;
+    case DescriptorType::kCombinedImageSampler:
+      return VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
+    case DescriptorType::kUniformTexelBuffer:
+      return VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER;
+    case DescriptorType::kStorageTexelBuffer:
+      return VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER;
+    case DescriptorType::kStorageBuffer:
+      return VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
+    case DescriptorType::kUniformBuffer:
+      return VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
+    case DescriptorType::kDynamicUniformBuffer:
+      return VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
+    case DescriptorType::kDynamicStorageBuffer:
+      return VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC;
+    case DescriptorType::kInputAttachment:
+      return VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT;
+  }
+
+  assert(false && "Unknown resource type");
+  return VK_DESCRIPTOR_TYPE_SAMPLER;
+}
+
+Descriptor::Descriptor(DescriptorType type, uint32_t desc_set, uint32_t binding)
+    : type_(type), descriptor_set_(desc_set), binding_(binding) {}
+
+Descriptor::~Descriptor() = default;
+
+}  // namespace vulkan
+}  // namespace amber
diff --git a/src/vulkan/descriptor.h b/src/vulkan/descriptor.h
new file mode 100644
index 0000000..ea24733
--- /dev/null
+++ b/src/vulkan/descriptor.h
@@ -0,0 +1,92 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VULKAN_DESCRIPTOR_H_
+#define SRC_VULKAN_DESCRIPTOR_H_
+
+#include <memory>
+
+#include "amber/result.h"
+#include "vulkan/vulkan.h"
+
+namespace amber {
+namespace vulkan {
+
+enum class DescriptorType : uint8_t {
+  kStorageImage = 0,
+  kSampler,
+  kSampledImage,
+  kCombinedImageSampler,
+  kUniformTexelBuffer,
+  kStorageTexelBuffer,
+  kStorageBuffer,
+  kUniformBuffer,
+  kDynamicUniformBuffer,
+  kDynamicStorageBuffer,
+  kInputAttachment,
+};
+
+VkDescriptorType ToVkDescriptorType(DescriptorType type);
+
+class Descriptor {
+ public:
+  Descriptor(DescriptorType type, uint32_t desc_set, uint32_t binding);
+
+  ~Descriptor();
+
+  uint32_t GetDescriptorSet() const { return descriptor_set_; }
+  uint32_t GetBinding() const { return binding_; }
+  bool operator<(const Descriptor& r) {
+    if (descriptor_set_ == r.descriptor_set_)
+      return binding_ < r.binding_;
+    return descriptor_set_ < r.descriptor_set_;
+  }
+
+  DescriptorType GetType() const { return type_; }
+
+  bool IsStorageImage() const { return type_ == DescriptorType::kStorageImage; }
+  bool IsSampler() const { return type_ == DescriptorType::kSampler; }
+  bool IsSampledImage() const { return type_ == DescriptorType::kSampledImage; }
+  bool IsCombinedImageSampler() const {
+    return type_ == DescriptorType::kCombinedImageSampler;
+  }
+  bool IsUniformTexelBuffer() const {
+    return type_ == DescriptorType::kUniformTexelBuffer;
+  }
+  bool IsStorageTexelBuffer() const {
+    return type_ == DescriptorType::kStorageTexelBuffer;
+  }
+  bool IsStorageBuffer() const {
+    return type_ == DescriptorType::kStorageBuffer;
+  }
+  bool IsUniformBuffer() const {
+    return type_ == DescriptorType::kUniformBuffer;
+  }
+  bool IsDynamicUniformBuffer() const {
+    return type_ == DescriptorType::kDynamicUniformBuffer;
+  }
+  bool IsDynamicStorageBuffer() const {
+    return type_ == DescriptorType::kDynamicStorageBuffer;
+  }
+
+ private:
+  DescriptorType type_ = DescriptorType::kSampledImage;
+  uint32_t descriptor_set_ = 0;
+  uint32_t binding_ = 0;
+};
+
+}  // namespace vulkan
+}  // namespace amber
+
+#endif  // SRC_VULKAN_DESCRIPTOR_H_
diff --git a/src/vulkan/device.cc b/src/vulkan/device.cc
new file mode 100644
index 0000000..c5dc430
--- /dev/null
+++ b/src/vulkan/device.cc
@@ -0,0 +1,155 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vulkan/device.h"
+
+#include <memory>
+#include <vector>
+
+#include "src/make_unique.h"
+
+namespace amber {
+
+namespace vulkan {
+
+Device::Device() = default;
+Device::Device(VkDevice device) : device_(device), destroy_device_(false) {}
+Device::~Device() = default;
+
+void Device::Shutdown() {
+  if (destroy_device_) {
+    vkDestroyDevice(device_, nullptr);
+    vkDestroyInstance(instance_, nullptr);
+  }
+}
+
+Result Device::Initialize() {
+  if (device_ == VK_NULL_HANDLE) {
+    Result r = CreateInstance();
+    if (!r.IsSuccess())
+      return r;
+
+    r = ChoosePhysicalDevice();
+    if (!r.IsSuccess())
+      return r;
+
+    r = CreateDevice();
+    if (!r.IsSuccess())
+      return r;
+  }
+
+  if (queue_ == VK_NULL_HANDLE) {
+    vkGetDeviceQueue(device_, queue_family_index_, 0, &queue_);
+    if (queue_ == VK_NULL_HANDLE)
+      return Result("Vulkan::Calling vkGetDeviceQueue Fail");
+  }
+  return {};
+}
+
+bool Device::ChooseQueueFamilyIndex(const VkPhysicalDevice& physical_device) {
+  uint32_t count;
+  std::vector<VkQueueFamilyProperties> properties;
+
+  vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &count, nullptr);
+  properties.resize(count);
+  vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &count,
+                                           properties.data());
+
+  for (uint32_t i = 0; i < count; ++i) {
+    if (properties[i].queueFlags &
+        (VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT)) {
+      queue_family_flags_ = properties[i].queueFlags;
+      queue_family_index_ = i;
+      return true;
+    }
+  }
+
+  for (uint32_t i = 0; i < count; ++i) {
+    if (properties[i].queueFlags & VK_QUEUE_COMPUTE_BIT) {
+      queue_family_flags_ = properties[i].queueFlags;
+      queue_family_index_ = i;
+      return true;
+    }
+  }
+
+  return false;
+}
+
+Result Device::CreateInstance() {
+  VkApplicationInfo appInfo = {};
+  appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
+  appInfo.apiVersion = VK_MAKE_VERSION(1, 0, 0);
+
+  VkInstanceCreateInfo instInfo = {};
+  instInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
+  instInfo.pApplicationInfo = &appInfo;
+  // TODO(jaebaek): Enable layers, extensions
+
+  if (vkCreateInstance(&instInfo, nullptr, &instance_) != VK_SUCCESS)
+    return Result("Vulkan::Calling vkCreateInstance Fail");
+
+  return {};
+}
+
+Result Device::ChoosePhysicalDevice() {
+  uint32_t count;
+  std::vector<VkPhysicalDevice> physical_devices;
+
+  if (vkEnumeratePhysicalDevices(instance_, &count, nullptr) != VK_SUCCESS)
+    return Result("Vulkan::Calling vkEnumeratePhysicalDevices Fail");
+  physical_devices.resize(count);
+  if (vkEnumeratePhysicalDevices(instance_, &count, physical_devices.data()) !=
+      VK_SUCCESS)
+    return Result("Vulkan::Calling vkEnumeratePhysicalDevices Fail");
+
+  for (uint32_t i = 0; i < count; ++i) {
+    VkPhysicalDeviceProperties property;
+    vkGetPhysicalDeviceProperties(physical_devices[i], &property);
+    // TODO(jaebaek): Check physical device property
+
+    if (ChooseQueueFamilyIndex(physical_devices[i])) {
+      physical_device_ = physical_devices[i];
+      vkGetPhysicalDeviceMemoryProperties(physical_device_,
+                                          &physical_memory_properties_);
+      return {};
+    }
+  }
+
+  return Result("Vulkan::No physical device supports Vulkan");
+}
+
+Result Device::CreateDevice() {
+  VkDeviceQueueCreateInfo queue_info;
+  const float priorities[] = {1.0f};
+
+  queue_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
+  queue_info.queueFamilyIndex = queue_family_index_;
+  queue_info.queueCount = 1;
+  queue_info.pQueuePriorities = priorities;
+
+  VkDeviceCreateInfo info = {};
+  info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
+  info.pQueueCreateInfos = &queue_info;
+  info.queueCreateInfoCount = 1;
+  // TODO(jaebaek): Enable layers, extensions, features
+
+  if (vkCreateDevice(physical_device_, &info, nullptr, &device_) != VK_SUCCESS)
+    return Result("Vulkan::Calling vkCreateDevice Fail");
+
+  return {};
+}
+
+}  // namespace vulkan
+
+}  // namespace amber
diff --git a/src/vulkan/device.h b/src/vulkan/device.h
new file mode 100644
index 0000000..634e07d
--- /dev/null
+++ b/src/vulkan/device.h
@@ -0,0 +1,76 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VULKAN_DEVICE_H_
+#define SRC_VULKAN_DEVICE_H_
+
+#include <memory>
+
+#include "amber/result.h"
+#include "vulkan/vulkan.h"
+
+namespace amber {
+
+namespace vulkan {
+
+class Device {
+ public:
+  Device();
+  explicit Device(VkDevice device);
+  ~Device();
+
+  Result Initialize();
+  void Shutdown();
+
+  VkDevice GetDevice() const { return device_; }
+  uint32_t GetQueueFamilyIndex() const { return queue_family_index_; }
+  VkQueue GetQueue() const { return queue_; }
+  const VkPhysicalDeviceMemoryProperties& GetPhysicalMemoryProperties() const {
+    return physical_memory_properties_;
+  }
+
+ private:
+  Result CreateInstance();
+
+  // Get a physical device by checking if the physical device has a proper
+  // queue family.
+  Result ChoosePhysicalDevice();
+
+  // Return true if |physical_device| has a queue family that supports both
+  // graphics and compute or only a compute pipeline. If the proper queue
+  // family exists, |queue_family_index_| and |queue_family_flags_| will have
+  // the queue family index and flags, respectively. Return false if the proper
+  // queue family does not exist.
+  bool ChooseQueueFamilyIndex(const VkPhysicalDevice& physical_device);
+
+  // Create a logical device.
+  Result CreateDevice();
+
+  VkInstance instance_ = VK_NULL_HANDLE;
+  VkPhysicalDevice physical_device_ = VK_NULL_HANDLE;
+  VkPhysicalDeviceMemoryProperties physical_memory_properties_ = {};
+  VkDevice device_ = VK_NULL_HANDLE;
+  VkQueueFlags queue_family_flags_ = 0;
+  uint32_t queue_family_index_ = 0;
+
+  VkQueue queue_ = VK_NULL_HANDLE;
+
+  bool destroy_device_ = true;
+};
+
+}  // namespace vulkan
+
+}  // namespace amber
+
+#endif  // SRC_VULKAN_DEVICE_H_
diff --git a/src/vulkan/engine_vulkan.cc b/src/vulkan/engine_vulkan.cc
new file mode 100644
index 0000000..4618296
--- /dev/null
+++ b/src/vulkan/engine_vulkan.cc
@@ -0,0 +1,278 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vulkan/engine_vulkan.h"
+
+#include <algorithm>
+
+#include "src/make_unique.h"
+#include "src/vulkan/format_data.h"
+#include "src/vulkan/graphics_pipeline.h"
+
+namespace amber {
+namespace vulkan {
+namespace {
+
+const uint32_t kFramebufferWidth = 250;
+const uint32_t kFramebufferHeight = 250;
+const VkFormat kDefaultColorFormat = VK_FORMAT_R8G8B8A8_UNORM;
+
+VkShaderStageFlagBits ToVkShaderStage(ShaderType type) {
+  switch (type) {
+    case ShaderType::kGeometry:
+      return VK_SHADER_STAGE_GEOMETRY_BIT;
+    case ShaderType::kFragment:
+      return VK_SHADER_STAGE_FRAGMENT_BIT;
+    case ShaderType::kVertex:
+      return VK_SHADER_STAGE_VERTEX_BIT;
+    case ShaderType::kTessellationControl:
+      return VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT;
+    case ShaderType::kTessellationEvaluation:
+      return VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT;
+    case ShaderType::kCompute:
+      return VK_SHADER_STAGE_COMPUTE_BIT;
+  }
+
+  // Unreachable
+  return VK_SHADER_STAGE_FRAGMENT_BIT;
+}
+
+}  // namespace
+
+EngineVulkan::EngineVulkan() : Engine() {}
+
+EngineVulkan::~EngineVulkan() = default;
+
+Result EngineVulkan::InitDeviceAndCreateCommand() {
+  Result r = device_->Initialize();
+  if (!r.IsSuccess())
+    return r;
+
+  if (!pool_) {
+    pool_ = MakeUnique<CommandPool>(device_->GetDevice());
+    r = pool_->Initialize(device_->GetQueueFamilyIndex());
+    if (!r.IsSuccess())
+      return r;
+  }
+
+  return {};
+}
+
+Result EngineVulkan::Initialize() {
+  if (device_)
+    return Result("Vulkan::Set device_ already exists");
+
+  device_ = MakeUnique<Device>();
+  return InitDeviceAndCreateCommand();
+}
+
+Result EngineVulkan::InitializeWithDevice(void* default_device) {
+  if (device_)
+    return Result("Vulkan::Set device_ already exists");
+
+  VkDevice device = static_cast<VkDevice>(default_device);
+  if (device == VK_NULL_HANDLE)
+    return Result("Vulkan::Set VK_NULL_HANDLE is given");
+
+  device_ = MakeUnique<Device>(device);
+  return InitDeviceAndCreateCommand();
+}
+
+Result EngineVulkan::Shutdown() {
+  for (auto it = modules_.begin(); it != modules_.end(); ++it)
+    vkDestroyShaderModule(device_->GetDevice(), it->second, nullptr);
+
+  pipeline_->Shutdown();
+  pool_->Shutdown();
+  device_->Shutdown();
+  return {};
+}
+
+Result EngineVulkan::AddRequirement(Feature feature, const Format* fmt) {
+  auto it = std::find_if(requirements_.begin(), requirements_.end(),
+                         [&feature](const EngineVulkan::Requirement& req) {
+                           return req.feature == feature;
+                         });
+  if (it != requirements_.end())
+    return Result("Vulkan::Feature Already Exists");
+
+  requirements_.push_back({feature, fmt});
+  return {};
+}
+
+Result EngineVulkan::CreatePipeline(PipelineType type) {
+  if (type == PipelineType::kCompute)
+    return Result("Vulkan::Compute Pipeline Not Implemented");
+
+  VkFormat frame_buffer_format = kDefaultColorFormat;
+  auto it_frame_buffer =
+      std::find_if(requirements_.begin(), requirements_.end(),
+                   [](const EngineVulkan::Requirement& req) {
+                     return req.feature == Feature::kFramebuffer;
+                   });
+  if (it_frame_buffer != requirements_.end()) {
+    frame_buffer_format = ToVkFormat(it_frame_buffer->format->GetFormatType());
+  }
+
+  VkFormat depth_stencil_format = VK_FORMAT_UNDEFINED;
+  auto it_depth_stencil =
+      std::find_if(requirements_.begin(), requirements_.end(),
+                   [](const EngineVulkan::Requirement& req) {
+                     return req.feature == Feature::kDepthStencil;
+                   });
+  if (it_depth_stencil != requirements_.end()) {
+    depth_stencil_format =
+        ToVkFormat(it_depth_stencil->format->GetFormatType());
+  }
+
+  pipeline_ = MakeUnique<GraphicsPipeline>(
+      type, device_->GetDevice(), device_->GetPhysicalMemoryProperties(),
+      frame_buffer_format, depth_stencil_format, GetShaderStageInfo());
+
+  return pipeline_->AsGraphics()->Initialize(
+      kFramebufferWidth, kFramebufferHeight, pool_->GetCommandPool(),
+      device_->GetQueue());
+}
+
+Result EngineVulkan::SetShader(ShaderType type,
+                               const std::vector<uint32_t>& data) {
+  if (type == ShaderType::kCompute)
+    return Result("Vulkan::Compute Pipeline Not Implemented");
+
+  VkShaderModuleCreateInfo info = {};
+  info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
+  info.codeSize = data.size() * sizeof(uint32_t);
+  info.pCode = data.data();
+
+  auto it = modules_.find(type);
+  if (it != modules_.end())
+    return Result("Vulkan::Setting Duplicated Shader Types Fail");
+
+  VkShaderModule shader;
+  if (vkCreateShaderModule(device_->GetDevice(), &info, nullptr, &shader) !=
+      VK_SUCCESS) {
+    return Result("Vulkan::Calling vkCreateShaderModule Fail");
+  }
+
+  modules_[type] = shader;
+  return {};
+}
+
+std::vector<VkPipelineShaderStageCreateInfo>
+EngineVulkan::GetShaderStageInfo() {
+  std::vector<VkPipelineShaderStageCreateInfo> stage_info(modules_.size());
+  uint32_t stage_count = 0;
+  for (auto it : modules_) {
+    stage_info[stage_count] = {};
+    stage_info[stage_count].sType =
+        VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
+    stage_info[stage_count].stage = ToVkShaderStage(it.first);
+    stage_info[stage_count].module = it.second;
+    // TODO(jaebaek): Handle entry point command
+    stage_info[stage_count].pName = "main";
+    ++stage_count;
+  }
+  return stage_info;
+}
+
+Result EngineVulkan::SetBuffer(BufferType type,
+                               uint8_t location,
+                               const Format& format,
+                               const std::vector<Value>& values) {
+  if (!pipeline_)
+    return Result("Vulkan::SetBuffer no Pipeline exists");
+
+  // TODO(jaebaek): Doublecheck those buffers are only for the graphics
+  //                pipeline.
+  if (!pipeline_->IsGraphics())
+    return Result("Vulkan::SetBuffer for Non-Graphics Pipeline");
+
+  pipeline_->AsGraphics()->SetBuffer(type, location, format, values);
+  return {};
+}
+
+Result EngineVulkan::ExecuteClearColor(const ClearColorCommand* command) {
+  if (!pipeline_->IsGraphics())
+    return Result("Vulkan::Clear Color Command for Non-Graphics Pipeline");
+
+  return pipeline_->AsGraphics()->SetClearColor(
+      command->GetR(), command->GetG(), command->GetB(), command->GetA());
+}
+
+Result EngineVulkan::ExecuteClearStencil(const ClearStencilCommand* command) {
+  if (!pipeline_->IsGraphics())
+    return Result("Vulkan::Clear Stencil Command for Non-Graphics Pipeline");
+
+  return pipeline_->AsGraphics()->SetClearStencil(command->GetValue());
+}
+
+Result EngineVulkan::ExecuteClearDepth(const ClearDepthCommand* command) {
+  if (!pipeline_->IsGraphics())
+    return Result("Vulkan::Clear Depth Command for Non-Graphics Pipeline");
+
+  return pipeline_->AsGraphics()->SetClearDepth(command->GetValue());
+}
+
+Result EngineVulkan::ExecuteClear(const ClearCommand*) {
+  if (!pipeline_->IsGraphics())
+    return Result("Vulkan::Clear Command for Non-Graphics Pipeline");
+
+  return pipeline_->AsGraphics()->Clear();
+}
+
+Result EngineVulkan::ExecuteDrawRect(const DrawRectCommand*) {
+  return Result("Vulkan::ExecuteDrawRect Not Implemented");
+}
+
+Result EngineVulkan::ExecuteDrawArrays(const DrawArraysCommand*) {
+  if (!pipeline_->IsGraphics())
+    return Result("Vulkan::DrawArrays for Non-Graphics Pipeline");
+
+  return pipeline_->AsGraphics()->Draw();
+}
+
+Result EngineVulkan::ExecuteCompute(const ComputeCommand*) {
+  return Result("Vulkan::ExecuteCompute Not Implemented");
+}
+
+Result EngineVulkan::ExecuteEntryPoint(const EntryPointCommand*) {
+  return Result("Vulkan::ExecuteEntryPoint Not Implemented");
+}
+
+Result EngineVulkan::ExecutePatchParameterVertices(
+    const PatchParameterVerticesCommand*) {
+  return Result("Vulkan::ExecutePatch Not Implemented");
+}
+
+Result EngineVulkan::ExecuteProbe(const ProbeCommand* command) {
+  if (!pipeline_->IsGraphics())
+    return Result("Vulkan::Probe FrameBuffer for Non-Graphics Pipeline");
+
+  return pipeline_->AsGraphics()->Probe(command);
+}
+
+Result EngineVulkan::ExecuteProbeSSBO(const ProbeSSBOCommand*) {
+  return Result("Vulkan::ExecuteProbeSSBO Not Implemented");
+}
+
+Result EngineVulkan::ExecuteBuffer(const BufferCommand*) {
+  return Result("Vulkan::ExecuteBuffer Not Implemented");
+}
+
+Result EngineVulkan::ExecuteTolerance(const ToleranceCommand*) {
+  return Result("Vulkan::ExecuteTolerance Not Implemented");
+}
+
+}  // namespace vulkan
+}  // namespace amber
diff --git a/src/vulkan/engine_vulkan.h b/src/vulkan/engine_vulkan.h
new file mode 100644
index 0000000..c8c2a00
--- /dev/null
+++ b/src/vulkan/engine_vulkan.h
@@ -0,0 +1,85 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VULKAN_ENGINE_VULKAN_H_
+#define SRC_VULKAN_ENGINE_VULKAN_H_
+
+#include <memory>
+#include <unordered_map>
+
+#include "src/cast_hash.h"
+#include "src/engine.h"
+#include "src/vulkan/command.h"
+#include "src/vulkan/device.h"
+#include "src/vulkan/pipeline.h"
+#include "vulkan/vulkan.h"
+
+namespace amber {
+namespace vulkan {
+
+class EngineVulkan : public Engine {
+ public:
+  EngineVulkan();
+  ~EngineVulkan() override;
+
+  // Engine
+  Result Initialize() override;
+  Result InitializeWithDevice(void* default_device) override;
+  Result Shutdown() override;
+  Result AddRequirement(Feature feature, const Format*) override;
+  Result CreatePipeline(PipelineType type) override;
+  Result SetShader(ShaderType type, const std::vector<uint32_t>& data) override;
+  Result SetBuffer(BufferType type,
+                   uint8_t location,
+                   const Format& format,
+                   const std::vector<Value>& data) override;
+  Result ExecuteClearColor(const ClearColorCommand* cmd) override;
+  Result ExecuteClearStencil(const ClearStencilCommand* cmd) override;
+  Result ExecuteClearDepth(const ClearDepthCommand* cmd) override;
+  Result ExecuteClear(const ClearCommand* cmd) override;
+  Result ExecuteDrawRect(const DrawRectCommand* cmd) override;
+  Result ExecuteDrawArrays(const DrawArraysCommand* cmd) override;
+  Result ExecuteCompute(const ComputeCommand* cmd) override;
+  Result ExecuteEntryPoint(const EntryPointCommand* cmd) override;
+  Result ExecutePatchParameterVertices(
+      const PatchParameterVerticesCommand* cmd) override;
+  Result ExecuteProbe(const ProbeCommand* cmd) override;
+  Result ExecuteProbeSSBO(const ProbeSSBOCommand* cmd) override;
+  Result ExecuteBuffer(const BufferCommand* cmd) override;
+  Result ExecuteTolerance(const ToleranceCommand* cmd) override;
+
+ private:
+  Result InitDeviceAndCreateCommand();
+
+  std::vector<VkPipelineShaderStageCreateInfo> GetShaderStageInfo();
+
+  std::unique_ptr<Device> device_;
+  std::unique_ptr<CommandPool> pool_;
+  std::unique_ptr<Pipeline> pipeline_;
+
+  std::unordered_map<ShaderType, VkShaderModule, CastHash<ShaderType>> modules_;
+
+  struct Requirement {
+    Feature feature;
+    const Format* format;
+  };
+
+  std::vector<Requirement> requirements_;
+  std::vector<Requirement>::iterator FindFeature(Feature feature);
+};
+
+}  // namespace vulkan
+}  // namespace amber
+
+#endif  // SRC_VULKAN_ENGINE_VULKAN_H_
diff --git a/src/vulkan/find_vulkan.cmake b/src/vulkan/find_vulkan.cmake
new file mode 100644
index 0000000..630346a
--- /dev/null
+++ b/src/vulkan/find_vulkan.cmake
@@ -0,0 +1,77 @@
+# Copyright 2018 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 implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+# Include this file to find Vulkan and and set up compilation and linking.
+
+
+# Export these settings to the includer.
+set(Vulkan_FOUND FALSE)
+set(VULKAN_LIB "")
+
+
+# Our first choice is to pick up the Vulkan headers from an enclosing project.
+# And if that's the case, then use Vulkan libraries as specified by
+# Vulkan_LIBRARIES, with a default library of "vulkan".
+set(X "${Vulkan-Headers_SOURCE_DIR}/include")
+if (IS_DIRECTORY "${X}")
+  message(STATUS "Amber: Using Vulkan header dir ${X}")
+  list(APPEND CMAKE_REQUIRED_INCLUDES "${X}")
+  # Add the directory to the list of include paths, before any others.
+  include_directories(BEFORE "${X}")
+  CHECK_INCLUDE_FILE(vulkan/vulkan.h HAVE_VULKAN_HEADER)
+
+  if (${HAVE_VULKAN_HEADER})
+    if ("${Vulkan_LIBRARIES}" STREQUAL "")
+      message(STATUS "Amber: Defaulting to Vulkan library: vulkan")
+      set(VULKAN_LIB vulkan)
+    else()
+      message(STATUS "Amber: Using specified Vulkan libraries: ${Vulkan_LIBRARIES}")
+      set(VULKAN_LIB "${Vulkan_LIBRARIES}")
+    endif()
+    # For now assume we have Vulkan.  We have its header, but we haven't checked
+    # for the library.
+    # TODO(dneto): Actually check for the libraries.
+    set(Vulkan_FOUND TRUE)
+  endif()
+endif()
+unset(X)
+
+if (NOT ${Vulkan_FOUND})
+  # If we aren't already building a Vulkan library, then use CMake to find it.
+  if(NOT ${CMAKE_VERSION} VERSION_LESS "3.7")
+    # LunarG added FindVulkan support to CMake 3.7.  If you have the Vulkan SDK
+    # published by LunarG, then set environment variables:
+    #  VULKAN_SDK should point to the platform-specific SDK directory containing
+    #    the include and lib directories.
+    #  VK_ICD_FILENAMES should point to ICD JSON file.
+
+    # Example, with the LunarG SDK macOS edition with MoltenVK:
+    #  export VULKAN_SDK="$HOME/vulkan-macos-1.1.85.0/macOS"
+    #  export VK_ICD_FILENAMES="$VULKAN_SDK/etc/vulkan/icd/MoltenVK_icd.json"
+    # See https://cmake.org/cmake/help/v3.7/module/FindVulkan.html
+    find_package(Vulkan)
+    if(${Vulkan_FOUND})
+      message(STATUS "Amber: Using Vulkan from Vulkan SDK at $ENV{VULKAN_SDK}")
+      # Use the imported library target set up by find_package.
+      set(VULKAN_LIB Vulkan::Vulkan)
+      # Add the Vulkan include directory to the list of include paths.
+      include_directories("${Vulkan_INCLUDE_DIRS}")
+    endif()
+  endif()
+endif()
+
+if (NOT ${Vulkan_FOUND})
+  message(STATUS "Amber: Did not find Vulkan")
+endif()
diff --git a/src/vulkan/format_data.cc b/src/vulkan/format_data.cc
new file mode 100644
index 0000000..a78aad5
--- /dev/null
+++ b/src/vulkan/format_data.cc
@@ -0,0 +1,299 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vulkan/format_data.h"
+
+namespace amber {
+namespace vulkan {
+
+VkFormat ToVkFormat(FormatType type) {
+  switch (type) {
+    case FormatType::kUnknown:
+      return VK_FORMAT_UNDEFINED;
+    case FormatType::kA1R5G5B5_UNORM_PACK16:
+      return VK_FORMAT_A1R5G5B5_UNORM_PACK16;
+    case FormatType::kA2B10G10R10_SINT_PACK32:
+      return VK_FORMAT_A2B10G10R10_SINT_PACK32;
+    case FormatType::kA2B10G10R10_SNORM_PACK32:
+      return VK_FORMAT_A2B10G10R10_SNORM_PACK32;
+    case FormatType::kA2B10G10R10_SSCALED_PACK32:
+      return VK_FORMAT_A2B10G10R10_SSCALED_PACK32;
+    case FormatType::kA2B10G10R10_UINT_PACK32:
+      return VK_FORMAT_A2B10G10R10_UINT_PACK32;
+    case FormatType::kA2B10G10R10_UNORM_PACK32:
+      return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
+    case FormatType::kA2B10G10R10_USCALED_PACK32:
+      return VK_FORMAT_A2B10G10R10_USCALED_PACK32;
+    case FormatType::kA2R10G10B10_SINT_PACK32:
+      return VK_FORMAT_A2R10G10B10_SINT_PACK32;
+    case FormatType::kA2R10G10B10_SNORM_PACK32:
+      return VK_FORMAT_A2R10G10B10_SNORM_PACK32;
+    case FormatType::kA2R10G10B10_SSCALED_PACK32:
+      return VK_FORMAT_A2R10G10B10_SSCALED_PACK32;
+    case FormatType::kA2R10G10B10_UINT_PACK32:
+      return VK_FORMAT_A2R10G10B10_UINT_PACK32;
+    case FormatType::kA2R10G10B10_UNORM_PACK32:
+      return VK_FORMAT_A2R10G10B10_UNORM_PACK32;
+    case FormatType::kA2R10G10B10_USCALED_PACK32:
+      return VK_FORMAT_A2R10G10B10_USCALED_PACK32;
+    case FormatType::kA8B8G8R8_SINT_PACK32:
+      return VK_FORMAT_A8B8G8R8_SINT_PACK32;
+    case FormatType::kA8B8G8R8_SNORM_PACK32:
+      return VK_FORMAT_A8B8G8R8_SNORM_PACK32;
+    case FormatType::kA8B8G8R8_SRGB_PACK32:
+      return VK_FORMAT_A8B8G8R8_SRGB_PACK32;
+    case FormatType::kA8B8G8R8_SSCALED_PACK32:
+      return VK_FORMAT_A8B8G8R8_SSCALED_PACK32;
+    case FormatType::kA8B8G8R8_UINT_PACK32:
+      return VK_FORMAT_A8B8G8R8_UINT_PACK32;
+    case FormatType::kA8B8G8R8_UNORM_PACK32:
+      return VK_FORMAT_A8B8G8R8_UNORM_PACK32;
+    case FormatType::kA8B8G8R8_USCALED_PACK32:
+      return VK_FORMAT_A8B8G8R8_USCALED_PACK32;
+    case FormatType::kB10G11R11_UFLOAT_PACK32:
+      return VK_FORMAT_B10G11R11_UFLOAT_PACK32;
+    case FormatType::kB4G4R4A4_UNORM_PACK16:
+      return VK_FORMAT_B4G4R4A4_UNORM_PACK16;
+    case FormatType::kB5G5R5A1_UNORM_PACK16:
+      return VK_FORMAT_B5G5R5A1_UNORM_PACK16;
+    case FormatType::kB5G6R5_UNORM_PACK16:
+      return VK_FORMAT_B5G6R5_UNORM_PACK16;
+    case FormatType::kB8G8R8A8_SINT:
+      return VK_FORMAT_B8G8R8A8_SINT;
+    case FormatType::kB8G8R8A8_SNORM:
+      return VK_FORMAT_B8G8R8A8_SNORM;
+    case FormatType::kB8G8R8A8_SRGB:
+      return VK_FORMAT_B8G8R8A8_SRGB;
+    case FormatType::kB8G8R8A8_SSCALED:
+      return VK_FORMAT_B8G8R8A8_SSCALED;
+    case FormatType::kB8G8R8A8_UINT:
+      return VK_FORMAT_B8G8R8A8_UINT;
+    case FormatType::kB8G8R8A8_UNORM:
+      return VK_FORMAT_B8G8R8A8_UNORM;
+    case FormatType::kB8G8R8A8_USCALED:
+      return VK_FORMAT_B8G8R8A8_USCALED;
+    case FormatType::kB8G8R8_SINT:
+      return VK_FORMAT_B8G8R8_SINT;
+    case FormatType::kB8G8R8_SNORM:
+      return VK_FORMAT_B8G8R8_SNORM;
+    case FormatType::kB8G8R8_SRGB:
+      return VK_FORMAT_B8G8R8_SRGB;
+    case FormatType::kB8G8R8_SSCALED:
+      return VK_FORMAT_B8G8R8_SSCALED;
+    case FormatType::kB8G8R8_UINT:
+      return VK_FORMAT_B8G8R8_UINT;
+    case FormatType::kB8G8R8_UNORM:
+      return VK_FORMAT_B8G8R8_UNORM;
+    case FormatType::kB8G8R8_USCALED:
+      return VK_FORMAT_B8G8R8_USCALED;
+    case FormatType::kD16_UNORM:
+      return VK_FORMAT_D16_UNORM;
+    case FormatType::kD16_UNORM_S8_UINT:
+      return VK_FORMAT_D16_UNORM_S8_UINT;
+    case FormatType::kD24_UNORM_S8_UINT:
+      return VK_FORMAT_D24_UNORM_S8_UINT;
+    case FormatType::kD32_SFLOAT:
+      return VK_FORMAT_D32_SFLOAT;
+    case FormatType::kD32_SFLOAT_S8_UINT:
+      return VK_FORMAT_D32_SFLOAT_S8_UINT;
+    case FormatType::kR16G16B16A16_SFLOAT:
+      return VK_FORMAT_R16G16B16A16_SFLOAT;
+    case FormatType::kR16G16B16A16_SINT:
+      return VK_FORMAT_R16G16B16A16_SINT;
+    case FormatType::kR16G16B16A16_SNORM:
+      return VK_FORMAT_R16G16B16A16_SNORM;
+    case FormatType::kR16G16B16A16_SSCALED:
+      return VK_FORMAT_R16G16B16A16_SSCALED;
+    case FormatType::kR16G16B16A16_UINT:
+      return VK_FORMAT_R16G16B16A16_UINT;
+    case FormatType::kR16G16B16A16_UNORM:
+      return VK_FORMAT_R16G16B16A16_UNORM;
+    case FormatType::kR16G16B16A16_USCALED:
+      return VK_FORMAT_R16G16B16A16_USCALED;
+    case FormatType::kR16G16B16_SFLOAT:
+      return VK_FORMAT_R16G16B16_SFLOAT;
+    case FormatType::kR16G16B16_SINT:
+      return VK_FORMAT_R16G16B16_SINT;
+    case FormatType::kR16G16B16_SNORM:
+      return VK_FORMAT_R16G16B16_SNORM;
+    case FormatType::kR16G16B16_SSCALED:
+      return VK_FORMAT_R16G16B16_SSCALED;
+    case FormatType::kR16G16B16_UINT:
+      return VK_FORMAT_R16G16B16_UINT;
+    case FormatType::kR16G16B16_UNORM:
+      return VK_FORMAT_R16G16B16_UNORM;
+    case FormatType::kR16G16B16_USCALED:
+      return VK_FORMAT_R16G16B16_USCALED;
+    case FormatType::kR16G16_SFLOAT:
+      return VK_FORMAT_R16G16_SFLOAT;
+    case FormatType::kR16G16_SINT:
+      return VK_FORMAT_R16G16_SINT;
+    case FormatType::kR16G16_SNORM:
+      return VK_FORMAT_R16G16_SNORM;
+    case FormatType::kR16G16_SSCALED:
+      return VK_FORMAT_R16G16_SSCALED;
+    case FormatType::kR16G16_UINT:
+      return VK_FORMAT_R16G16_UINT;
+    case FormatType::kR16G16_UNORM:
+      return VK_FORMAT_R16G16_UNORM;
+    case FormatType::kR16G16_USCALED:
+      return VK_FORMAT_R16G16_USCALED;
+    case FormatType::kR16_SFLOAT:
+      return VK_FORMAT_R16_SFLOAT;
+    case FormatType::kR16_SINT:
+      return VK_FORMAT_R16_SINT;
+    case FormatType::kR16_SNORM:
+      return VK_FORMAT_R16_SNORM;
+    case FormatType::kR16_SSCALED:
+      return VK_FORMAT_R16_SSCALED;
+    case FormatType::kR16_UINT:
+      return VK_FORMAT_R16_UINT;
+    case FormatType::kR16_UNORM:
+      return VK_FORMAT_R16_UNORM;
+    case FormatType::kR16_USCALED:
+      return VK_FORMAT_R16_USCALED;
+    case FormatType::kR32G32B32A32_SFLOAT:
+      return VK_FORMAT_R32G32B32A32_SFLOAT;
+    case FormatType::kR32G32B32A32_SINT:
+      return VK_FORMAT_R32G32B32A32_SINT;
+    case FormatType::kR32G32B32A32_UINT:
+      return VK_FORMAT_R32G32B32A32_UINT;
+    case FormatType::kR32G32B32_SFLOAT:
+      return VK_FORMAT_R32G32B32_SFLOAT;
+    case FormatType::kR32G32B32_SINT:
+      return VK_FORMAT_R32G32B32_SINT;
+    case FormatType::kR32G32B32_UINT:
+      return VK_FORMAT_R32G32B32_UINT;
+    case FormatType::kR32G32_SFLOAT:
+      return VK_FORMAT_R32G32_SFLOAT;
+    case FormatType::kR32G32_SINT:
+      return VK_FORMAT_R32G32_SINT;
+    case FormatType::kR32G32_UINT:
+      return VK_FORMAT_R32G32_UINT;
+    case FormatType::kR32_SFLOAT:
+      return VK_FORMAT_R32_SFLOAT;
+    case FormatType::kR32_SINT:
+      return VK_FORMAT_R32_SINT;
+    case FormatType::kR32_UINT:
+      return VK_FORMAT_R32_UINT;
+    case FormatType::kR4G4B4A4_UNORM_PACK16:
+      return VK_FORMAT_R4G4B4A4_UNORM_PACK16;
+    case FormatType::kR4G4_UNORM_PACK8:
+      return VK_FORMAT_R4G4_UNORM_PACK8;
+    case FormatType::kR5G5B5A1_UNORM_PACK16:
+      return VK_FORMAT_R5G5B5A1_UNORM_PACK16;
+    case FormatType::kR5G6B5_UNORM_PACK16:
+      return VK_FORMAT_R5G6B5_UNORM_PACK16;
+    case FormatType::kR64G64B64A64_SFLOAT:
+      return VK_FORMAT_R64G64B64A64_SFLOAT;
+    case FormatType::kR64G64B64A64_SINT:
+      return VK_FORMAT_R64G64B64A64_SINT;
+    case FormatType::kR64G64B64A64_UINT:
+      return VK_FORMAT_R64G64B64A64_UINT;
+    case FormatType::kR64G64B64_SFLOAT:
+      return VK_FORMAT_R64G64B64_SFLOAT;
+    case FormatType::kR64G64B64_SINT:
+      return VK_FORMAT_R64G64B64_SINT;
+    case FormatType::kR64G64B64_UINT:
+      return VK_FORMAT_R64G64B64_UINT;
+    case FormatType::kR64G64_SFLOAT:
+      return VK_FORMAT_R64G64_SFLOAT;
+    case FormatType::kR64G64_SINT:
+      return VK_FORMAT_R64G64_SINT;
+    case FormatType::kR64G64_UINT:
+      return VK_FORMAT_R64G64_UINT;
+    case FormatType::kR64_SFLOAT:
+      return VK_FORMAT_R64_SFLOAT;
+    case FormatType::kR64_SINT:
+      return VK_FORMAT_R64_SINT;
+    case FormatType::kR64_UINT:
+      return VK_FORMAT_R64_UINT;
+    case FormatType::kR8G8B8A8_SINT:
+      return VK_FORMAT_R8G8B8A8_SINT;
+    case FormatType::kR8G8B8A8_SNORM:
+      return VK_FORMAT_R8G8B8A8_SNORM;
+    case FormatType::kR8G8B8A8_SRGB:
+      return VK_FORMAT_R8G8B8A8_SRGB;
+    case FormatType::kR8G8B8A8_SSCALED:
+      return VK_FORMAT_R8G8B8A8_SSCALED;
+    case FormatType::kR8G8B8A8_UINT:
+      return VK_FORMAT_R8G8B8A8_UINT;
+    case FormatType::kR8G8B8A8_UNORM:
+      return VK_FORMAT_R8G8B8A8_UNORM;
+    case FormatType::kR8G8B8A8_USCALED:
+      return VK_FORMAT_R8G8B8A8_USCALED;
+    case FormatType::kR8G8B8_SINT:
+      return VK_FORMAT_R8G8B8_SINT;
+    case FormatType::kR8G8B8_SNORM:
+      return VK_FORMAT_R8G8B8_SNORM;
+    case FormatType::kR8G8B8_SRGB:
+      return VK_FORMAT_R8G8B8_SRGB;
+    case FormatType::kR8G8B8_SSCALED:
+      return VK_FORMAT_R8G8B8_SSCALED;
+    case FormatType::kR8G8B8_UINT:
+      return VK_FORMAT_R8G8B8_UINT;
+    case FormatType::kR8G8B8_UNORM:
+      return VK_FORMAT_R8G8B8_UNORM;
+    case FormatType::kR8G8B8_USCALED:
+      return VK_FORMAT_R8G8B8_USCALED;
+    case FormatType::kR8G8_SINT:
+      return VK_FORMAT_R8G8_SINT;
+    case FormatType::kR8G8_SNORM:
+      return VK_FORMAT_R8G8_SNORM;
+    case FormatType::kR8G8_SRGB:
+      return VK_FORMAT_R8G8_SRGB;
+    case FormatType::kR8G8_SSCALED:
+      return VK_FORMAT_R8G8_SSCALED;
+    case FormatType::kR8G8_UINT:
+      return VK_FORMAT_R8G8_UINT;
+    case FormatType::kR8G8_UNORM:
+      return VK_FORMAT_R8G8_UNORM;
+    case FormatType::kR8G8_USCALED:
+      return VK_FORMAT_R8G8_USCALED;
+    case FormatType::kR8_SINT:
+      return VK_FORMAT_R8_SINT;
+    case FormatType::kR8_SNORM:
+      return VK_FORMAT_R8_SNORM;
+    case FormatType::kR8_SRGB:
+      return VK_FORMAT_R8_SRGB;
+    case FormatType::kR8_SSCALED:
+      return VK_FORMAT_R8_SSCALED;
+    case FormatType::kR8_UINT:
+      return VK_FORMAT_R8_UINT;
+    case FormatType::kR8_UNORM:
+      return VK_FORMAT_R8_UNORM;
+    case FormatType::kR8_USCALED:
+      return VK_FORMAT_R8_USCALED;
+    case FormatType::kS8_UINT:
+      return VK_FORMAT_S8_UINT;
+    case FormatType::kX8_D24_UNORM_PACK32:
+      return VK_FORMAT_X8_D24_UNORM_PACK32;
+  }
+  return VK_FORMAT_UNDEFINED;
+}
+
+uint32_t VkFormatToByteSize(VkFormat format) {
+  switch (format) {
+    case VK_FORMAT_R8G8B8A8_UNORM:
+      return 4;
+
+    // TODO(jaebaek): Handle all cases.
+    default:
+      break;
+  }
+  return 0;
+}
+
+}  // namespace vulkan
+}  // namespace amber
diff --git a/src/vulkan/format_data.h b/src/vulkan/format_data.h
new file mode 100644
index 0000000..c9dc44a
--- /dev/null
+++ b/src/vulkan/format_data.h
@@ -0,0 +1,30 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VULKAN_FORMAT_DATA_H_
+#define SRC_VULKAN_FORMAT_DATA_H_
+
+#include "src/format_data.h"
+#include "vulkan/vulkan.h"
+
+namespace amber {
+namespace vulkan {
+
+VkFormat ToVkFormat(FormatType type);
+uint32_t VkFormatToByteSize(VkFormat format);
+
+}  // namespace vulkan
+}  // namespace amber
+
+#endif  // SRC_VULKAN_FORMAT_DATA_H_
diff --git a/src/vulkan/frame_buffer.cc b/src/vulkan/frame_buffer.cc
new file mode 100644
index 0000000..ab36bbc
--- /dev/null
+++ b/src/vulkan/frame_buffer.cc
@@ -0,0 +1,83 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vulkan/frame_buffer.h"
+
+#include <limits>
+
+#include "src/make_unique.h"
+
+namespace amber {
+namespace vulkan {
+
+FrameBuffer::FrameBuffer(VkDevice device, uint32_t width, uint32_t height)
+    : device_(device), width_(width), height_(height) {}
+
+FrameBuffer::~FrameBuffer() = default;
+
+Result FrameBuffer::Initialize(
+    VkRenderPass render_pass,
+    VkFormat color_format,
+    VkFormat depth_format,
+    const VkPhysicalDeviceMemoryProperties& properties) {
+  std::vector<VkImageView> attachments;
+
+  if (color_format != VK_FORMAT_UNDEFINED) {
+    color_image_ = MakeUnique<Image>(device_, color_format, width_, height_,
+                                     depth_, properties);
+    Result r = color_image_->Initialize(VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
+                                        VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT);
+    if (!r.IsSuccess())
+      return r;
+    attachments.push_back(color_image_->GetVkImageView());
+  }
+
+  if (depth_format != VK_FORMAT_UNDEFINED) {
+    depth_image_ = MakeUnique<Image>(device_, depth_format, width_, height_,
+                                     depth_, properties);
+    Result r =
+        depth_image_->Initialize(VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
+                                 VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT);
+    if (!r.IsSuccess())
+      return r;
+    attachments.push_back(depth_image_->GetVkImageView());
+  }
+
+  VkFramebufferCreateInfo frame_buffer_info = {};
+  frame_buffer_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
+  frame_buffer_info.renderPass = render_pass;
+  frame_buffer_info.attachmentCount = static_cast<uint32_t>(attachments.size());
+  frame_buffer_info.pAttachments = attachments.data();
+  frame_buffer_info.width = width_;
+  frame_buffer_info.height = height_;
+  frame_buffer_info.layers = 1;
+
+  if (vkCreateFramebuffer(device_, &frame_buffer_info, nullptr, &frame_) !=
+      VK_SUCCESS) {
+    return Result("Vulkan::Calling vkCreateFramebuffer Fail");
+  }
+
+  return {};
+}
+
+void FrameBuffer::Shutdown() {
+  vkDestroyFramebuffer(device_, frame_, nullptr);
+  if (color_image_)
+    color_image_->Shutdown();
+  if (depth_image_)
+    depth_image_->Shutdown();
+}
+
+}  // namespace vulkan
+}  // namespace amber
diff --git a/src/vulkan/frame_buffer.h b/src/vulkan/frame_buffer.h
new file mode 100644
index 0000000..0adf7ca
--- /dev/null
+++ b/src/vulkan/frame_buffer.h
@@ -0,0 +1,59 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VULKAN_FRAME_BUFFER_H_
+#define SRC_VULKAN_FRAME_BUFFER_H_
+
+#include "src/vulkan/image.h"
+
+namespace amber {
+namespace vulkan {
+
+class FrameBuffer {
+ public:
+  FrameBuffer(VkDevice device, uint32_t width, uint32_t height);
+  ~FrameBuffer();
+
+  Result Initialize(VkRenderPass render_pass,
+                    VkFormat color_format,
+                    VkFormat depth_format,
+                    const VkPhysicalDeviceMemoryProperties& properties);
+  void Shutdown();
+
+  VkFramebuffer GetFrameBuffer() const { return frame_; }
+  const void* GetColorBufferPtr() const {
+    return color_image_->HostAccessibleMemoryPtr();
+  }
+  VkImage GetColorImage() const { return color_image_->GetVkImage(); }
+  Result CopyColorImageToHost(VkCommandBuffer command) {
+    return color_image_->CopyToHost(command);
+  }
+
+  uint32_t GetWidth() const { return width_; }
+  uint32_t GetHeight() const { return height_; }
+
+ private:
+  VkDevice device_ = VK_NULL_HANDLE;
+  VkFramebuffer frame_ = VK_NULL_HANDLE;
+  std::unique_ptr<Image> color_image_;
+  std::unique_ptr<Image> depth_image_;
+  uint32_t width_ = 0;
+  uint32_t height_ = 0;
+  uint32_t depth_ = 1;
+};
+
+}  // namespace vulkan
+}  // namespace amber
+
+#endif  // SRC_VULKAN_FRAME_BUFFER_H_
diff --git a/src/vulkan/graphics_pipeline.cc b/src/vulkan/graphics_pipeline.cc
new file mode 100644
index 0000000..500d423
--- /dev/null
+++ b/src/vulkan/graphics_pipeline.cc
@@ -0,0 +1,590 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vulkan/graphics_pipeline.h"
+
+#include <cmath>
+
+#include "src/command.h"
+#include "src/make_unique.h"
+#include "src/vulkan/format_data.h"
+
+namespace amber {
+namespace vulkan {
+namespace {
+
+const VkAttachmentDescription kDefaultAttachmentDesc = {
+    0,                     /* flags */
+    VK_FORMAT_UNDEFINED,   /* format */
+    VK_SAMPLE_COUNT_1_BIT, /* samples */
+    // TODO(jaebaek): Set up proper loadOp, StoreOp.
+    VK_ATTACHMENT_LOAD_OP_LOAD,           /* loadOp */
+    VK_ATTACHMENT_STORE_OP_STORE,         /* storeOp */
+    VK_ATTACHMENT_LOAD_OP_LOAD,           /* stencilLoadOp */
+    VK_ATTACHMENT_STORE_OP_STORE,         /* stencilStoreOp */
+    VK_IMAGE_LAYOUT_UNDEFINED,            /* initialLayout */
+    VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, /* finalLayout */
+};
+
+const VkPipelineRasterizationStateCreateInfo kDefaultRasterizationInfo = {
+    VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, /* sType */
+    nullptr,                                                    /* pNext */
+    0,                                                          /* flags */
+    VK_FALSE,                /* depthClampEnable */
+    VK_FALSE,                /* rasterizerDiscardEnable */
+    VK_POLYGON_MODE_FILL,    /* polygonMode */
+    VK_CULL_MODE_NONE,       /* cullMode */
+    VK_FRONT_FACE_CLOCKWISE, /* frontFace */
+    VK_FALSE,                /* depthBiasEnable */
+    0,                       /* depthBiasConstantFactor */
+    0,                       /* depthBiasClamp */
+    0,                       /* depthBiasSlopeFactor */
+    0,                       /* lineWidth */
+};
+
+const VkSampleMask kSampleMask = ~0U;
+
+const VkPipelineMultisampleStateCreateInfo kMultisampleInfo = {
+    VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, /* sType */
+    nullptr,                                                  /* pNext */
+    0,                                                        /* flags */
+    kDefaultAttachmentDesc.samples, /* rasterizationSamples */
+    VK_FALSE,                       /* sampleShadingEnable */
+    0,                              /* minSampleShading */
+    &kSampleMask,                   /* pSampleMask */
+    VK_FALSE,                       /* alphaToCoverageEnable */
+    VK_FALSE,                       /* alphaToOneEnable */
+};
+
+const float kEpsilon = 0.002f;
+
+bool IsFloatPixelEqualInt(float pixel, uint8_t expected) {
+  // TODO(jaebaek): Change kEpsilon to tolerance.
+  return std::fabs(pixel - static_cast<float>(expected) / 255.0f) < kEpsilon;
+}
+
+}  // namespace
+
+GraphicsPipeline::GraphicsPipeline(
+    PipelineType type,
+    VkDevice device,
+    const VkPhysicalDeviceMemoryProperties& properties,
+    VkFormat color_format,
+    VkFormat depth_stencil_format,
+    std::vector<VkPipelineShaderStageCreateInfo> shader_stage_info)
+    : Pipeline(type, device, properties),
+      color_format_(color_format),
+      depth_stencil_format_(depth_stencil_format),
+      shader_stage_info_(shader_stage_info) {}
+
+GraphicsPipeline::~GraphicsPipeline() = default;
+
+Result GraphicsPipeline::CreateRenderPass() {
+  VkSubpassDescription subpass_desc = {};
+  subpass_desc.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
+
+  std::vector<VkAttachmentDescription> attachment_desc;
+
+  VkAttachmentReference color_refer = {};
+  VkAttachmentReference depth_refer = {};
+
+  if (color_format_ != VK_FORMAT_UNDEFINED) {
+    attachment_desc.push_back(kDefaultAttachmentDesc);
+    attachment_desc.back().format = color_format_;
+
+    color_refer.attachment = static_cast<uint32_t>(attachment_desc.size() - 1);
+    color_refer.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+
+    subpass_desc.colorAttachmentCount = 1;
+    subpass_desc.pColorAttachments = &color_refer;
+  }
+
+  if (depth_stencil_format_ != VK_FORMAT_UNDEFINED) {
+    attachment_desc.push_back(kDefaultAttachmentDesc);
+    attachment_desc.back().format = depth_stencil_format_;
+
+    depth_refer.attachment = static_cast<uint32_t>(attachment_desc.size() - 1);
+    depth_refer.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
+
+    subpass_desc.pDepthStencilAttachment = &depth_refer;
+  }
+
+  VkRenderPassCreateInfo render_pass_info = {};
+  render_pass_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
+  render_pass_info.attachmentCount =
+      static_cast<uint32_t>(attachment_desc.size());
+  render_pass_info.pAttachments = attachment_desc.data();
+  render_pass_info.subpassCount = 1;
+  render_pass_info.pSubpasses = &subpass_desc;
+
+  if (vkCreateRenderPass(device_, &render_pass_info, nullptr, &render_pass_) !=
+      VK_SUCCESS) {
+    return Result("Vulkan::Calling vkCreateRenderPass Fail");
+  }
+
+  return {};
+}
+
+VkPipelineDepthStencilStateCreateInfo
+GraphicsPipeline::GetPipelineDepthStencilInfo() {
+  VkPipelineDepthStencilStateCreateInfo depthstencil_info = {};
+  // TODO(jaebaek): Depth/stencil test setup should be come from the
+  // PipelineData.
+  depthstencil_info.depthTestEnable = VK_TRUE;
+  depthstencil_info.depthWriteEnable = VK_TRUE;
+  depthstencil_info.depthCompareOp = VK_COMPARE_OP_LESS;
+  depthstencil_info.depthBoundsTestEnable = VK_FALSE;
+  depthstencil_info.stencilTestEnable = VK_FALSE;
+  return depthstencil_info;
+}
+
+VkPipelineColorBlendAttachmentState
+GraphicsPipeline::GetPipelineColorBlendAttachmentState() {
+  VkPipelineColorBlendAttachmentState colorblend_attachment = {};
+  // TODO(jaebaek): Update blend state should be come from the PipelineData.
+  colorblend_attachment.blendEnable = VK_FALSE;
+  colorblend_attachment.srcColorBlendFactor = VK_BLEND_FACTOR_ZERO;
+  colorblend_attachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO;
+  colorblend_attachment.colorBlendOp = VK_BLEND_OP_ADD;
+  colorblend_attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
+  colorblend_attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
+  colorblend_attachment.alphaBlendOp = VK_BLEND_OP_ADD;
+  colorblend_attachment.colorWriteMask =
+      VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
+      VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
+  return colorblend_attachment;
+}
+
+Result GraphicsPipeline::CreateVkGraphicsPipeline() {
+  if (pipeline_ != VK_NULL_HANDLE)
+    return Result("Vulkan::Pipeline already created");
+
+  Result r = CreatePipelineLayout();
+  if (!r.IsSuccess())
+    return r;
+
+  VkPipelineVertexInputStateCreateInfo vertex_input_info = {};
+  vertex_input_info.sType =
+      VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
+  vertex_input_info.vertexBindingDescriptionCount = 1;
+
+  VkVertexInputBindingDescription vertex_binding_desc = {};
+  if (vertex_buffer_) {
+    vertex_binding_desc = vertex_buffer_->GetVertexInputBinding();
+    auto vertex_attr_desc = vertex_buffer_->GetVertexInputAttr();
+
+    vertex_input_info.pVertexBindingDescriptions = &vertex_binding_desc;
+    vertex_input_info.vertexAttributeDescriptionCount =
+        static_cast<uint32_t>(vertex_attr_desc.size());
+    vertex_input_info.pVertexAttributeDescriptions = vertex_attr_desc.data();
+  } else {
+    vertex_binding_desc.binding = 0;
+    vertex_binding_desc.stride = 0;
+    vertex_binding_desc.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
+
+    vertex_input_info.pVertexBindingDescriptions = &vertex_binding_desc;
+    vertex_input_info.vertexAttributeDescriptionCount = 0;
+    vertex_input_info.pVertexAttributeDescriptions = nullptr;
+  }
+
+  VkPipelineInputAssemblyStateCreateInfo input_assembly_info = {};
+  input_assembly_info.sType =
+      VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
+  // TODO(jaebaek): Handle the given index if exists.
+  input_assembly_info.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
+  input_assembly_info.primitiveRestartEnable = VK_FALSE;
+
+  VkViewport viewport = {0,
+                         0,
+                         static_cast<float>(frame_->GetWidth()),
+                         static_cast<float>(frame_->GetHeight()),
+                         0,
+                         1};
+
+  VkRect2D scissor = {{0, 0}, {frame_->GetWidth(), frame_->GetHeight()}};
+
+  VkPipelineViewportStateCreateInfo viewport_info = {};
+  viewport_info.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
+  viewport_info.viewportCount = 1;
+  viewport_info.pViewports = &viewport;
+  viewport_info.scissorCount = 1;
+  viewport_info.pScissors = &scissor;
+
+  VkGraphicsPipelineCreateInfo pipeline_info = {};
+  pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
+  pipeline_info.stageCount = static_cast<uint32_t>(shader_stage_info_.size());
+  pipeline_info.pStages = shader_stage_info_.data();
+  pipeline_info.pVertexInputState = &vertex_input_info;
+  pipeline_info.pInputAssemblyState = &input_assembly_info;
+  pipeline_info.pViewportState = &viewport_info;
+  pipeline_info.pRasterizationState = &kDefaultRasterizationInfo;
+  pipeline_info.pMultisampleState = &kMultisampleInfo;
+
+  VkPipelineDepthStencilStateCreateInfo depthstencil_info;
+  if (depth_stencil_format_ != VK_FORMAT_UNDEFINED) {
+    depthstencil_info = GetPipelineDepthStencilInfo();
+    pipeline_info.pDepthStencilState = &depthstencil_info;
+  }
+
+  VkPipelineColorBlendStateCreateInfo colorblend_info = {};
+  VkPipelineColorBlendAttachmentState colorblend_attachment;
+  if (color_format_ != VK_FORMAT_UNDEFINED) {
+    colorblend_attachment = GetPipelineColorBlendAttachmentState();
+
+    colorblend_info.sType =
+        VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
+    colorblend_info.logicOpEnable = VK_FALSE;
+    colorblend_info.logicOp = VK_LOGIC_OP_COPY;
+    colorblend_info.attachmentCount = 1;
+    colorblend_info.pAttachments = &colorblend_attachment;
+    pipeline_info.pColorBlendState = &colorblend_info;
+  }
+
+  pipeline_info.layout = pipeline_layout_;
+  pipeline_info.renderPass = render_pass_;
+  pipeline_info.subpass = 0;
+
+  if (vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1, &pipeline_info,
+                                nullptr, &pipeline_) != VK_SUCCESS) {
+    return Result("Vulkan::Calling vkCreateGraphicsPipelines Fail");
+  }
+
+  return {};
+}
+
+Result GraphicsPipeline::Initialize(uint32_t width,
+                                    uint32_t height,
+                                    VkCommandPool pool,
+                                    VkQueue queue) {
+  Result r = Pipeline::InitializeCommandBuffer(pool, queue);
+  if (!r.IsSuccess())
+    return r;
+
+  r = CreateRenderPass();
+  if (!r.IsSuccess())
+    return r;
+
+  frame_ = MakeUnique<FrameBuffer>(device_, width, height);
+  r = frame_->Initialize(render_pass_, color_format_, depth_stencil_format_,
+                         memory_properties_);
+  if (!r.IsSuccess())
+    return r;
+
+  frame_width_ = width;
+  frame_height_ = height;
+
+  return {};
+}
+
+void GraphicsPipeline::SetBuffer(BufferType type,
+                                 uint8_t location,
+                                 const Format& format,
+                                 const std::vector<Value>& values) {
+  // TODO(jaebaek): Handle indices data.
+  if (type != BufferType::kVertexData)
+    return;
+
+  if (!vertex_buffer_)
+    vertex_buffer_ = MakeUnique<VertexBuffer>(device_);
+
+  vertex_buffer_->SetData(location, format, values);
+}
+
+Result GraphicsPipeline::SendBufferDataIfNeeded() {
+  if (!vertex_buffer_)
+    return {};
+
+  if (vertex_buffer_->VertexDataSent())
+    return {};
+
+  Result r = command_->BeginIfNotInRecording();
+  if (!r.IsSuccess())
+    return r;
+
+  DeactivateRenderPassIfNeeded();
+
+  // TODO(jaebaek): Send indices data too.
+  return vertex_buffer_->SendVertexData(command_->GetCommandBuffer(),
+                                        memory_properties_);
+}
+
+void GraphicsPipeline::ActivateRenderPassIfNeeded() {
+  if (render_pass_state_ == RenderPassState::kActive)
+    return;
+
+  VkRenderPassBeginInfo render_begin_info = {};
+  render_begin_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
+  render_begin_info.renderPass = render_pass_;
+  render_begin_info.framebuffer = frame_->GetFrameBuffer();
+  render_begin_info.renderArea = {{0, 0}, {frame_width_, frame_height_}};
+  vkCmdBeginRenderPass(command_->GetCommandBuffer(), &render_begin_info,
+                       VK_SUBPASS_CONTENTS_INLINE);
+  render_pass_state_ = RenderPassState::kActive;
+}
+
+void GraphicsPipeline::DeactivateRenderPassIfNeeded() {
+  if (render_pass_state_ == RenderPassState::kInactive)
+    return;
+
+  vkCmdEndRenderPass(command_->GetCommandBuffer());
+  render_pass_state_ = RenderPassState::kInactive;
+}
+
+Result GraphicsPipeline::SetClearColor(float r, float g, float b, float a) {
+  if (color_format_ == VK_FORMAT_UNDEFINED) {
+    return Result(
+        "Vulkan::ClearColorCommand No Color Buffer for FrameBuffer Exists");
+  }
+
+  clear_color_r_ = r;
+  clear_color_g_ = g;
+  clear_color_b_ = b;
+  clear_color_a_ = a;
+  return {};
+}
+
+Result GraphicsPipeline::SetClearStencil(uint32_t stencil) {
+  if (depth_stencil_format_ == VK_FORMAT_UNDEFINED) {
+    return Result(
+        "Vulkan::ClearStencilCommand No DepthStencil Buffer for FrameBuffer "
+        "Exists");
+  }
+
+  clear_stencil_ = stencil;
+  return {};
+}
+
+Result GraphicsPipeline::SetClearDepth(float depth) {
+  if (depth_stencil_format_ == VK_FORMAT_UNDEFINED) {
+    return Result(
+        "Vulkan::ClearStencilCommand No DepthStencil Buffer for FrameBuffer "
+        "Exists");
+  }
+
+  clear_depth_ = depth;
+  return {};
+}
+
+Result GraphicsPipeline::Clear() {
+  if (color_format_ == VK_FORMAT_UNDEFINED &&
+      depth_stencil_format_ == VK_FORMAT_UNDEFINED) {
+    return Result(
+        "Vulkan::ClearColorCommand No Color nor DepthStencil Buffer for "
+        "FrameBuffer Exists");
+  }
+
+  if (color_format_ != VK_FORMAT_UNDEFINED) {
+    VkClearValue clear_value;
+    clear_value.color = {
+        {clear_color_r_, clear_color_g_, clear_color_b_, clear_color_a_}};
+    Result r = ClearBuffer(clear_value, VK_IMAGE_ASPECT_COLOR_BIT);
+    if (!r.IsSuccess())
+      return r;
+  }
+
+  if (depth_stencil_format_ == VK_FORMAT_UNDEFINED)
+    return {};
+
+  VkClearValue clear_value;
+  clear_value.depthStencil = {clear_depth_, clear_stencil_};
+  return ClearBuffer(clear_value,
+                     VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT);
+}
+
+Result GraphicsPipeline::ClearBuffer(const VkClearValue& clear_value,
+                                     VkImageAspectFlags aspect) {
+  Result r = command_->BeginIfNotInRecording();
+  if (!r.IsSuccess())
+    return r;
+
+  // TODO(jaebaek): When multiple clear and draw commands exist, handle
+  //                begin/end render pass properly.
+  ActivateRenderPassIfNeeded();
+
+  VkClearAttachment clear_attachment = {};
+  clear_attachment.aspectMask = aspect;
+  clear_attachment.colorAttachment = 0;
+  clear_attachment.clearValue = clear_value;
+
+  VkClearRect clear_rect;
+  clear_rect.rect = {{0, 0}, {frame_width_, frame_height_}};
+  clear_rect.baseArrayLayer = 0;
+  clear_rect.layerCount = 1;
+
+  vkCmdClearAttachments(command_->GetCommandBuffer(), 1, &clear_attachment, 1,
+                        &clear_rect);
+
+  return {};
+}
+
+Result GraphicsPipeline::Draw() {
+  // TODO(jaebaek): Handle primitive topology.
+  if (pipeline_ == VK_NULL_HANDLE) {
+    Result r = CreateVkGraphicsPipeline();
+    if (!r.IsSuccess())
+      return r;
+  }
+
+  Result r = SendBufferDataIfNeeded();
+  if (!r.IsSuccess())
+    return r;
+
+  r = command_->BeginIfNotInRecording();
+  if (!r.IsSuccess())
+    return r;
+
+  ActivateRenderPassIfNeeded();
+
+  vkCmdBindPipeline(command_->GetCommandBuffer(),
+                    VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_);
+
+  uint32_t vertex_count = 0;
+  uint32_t instance_count = 0;
+  if (vertex_buffer_) {
+    vertex_buffer_->BindToCommandBuffer(command_->GetCommandBuffer());
+    vertex_count = static_cast<uint32_t>(vertex_buffer_->GetVertexCount());
+    instance_count = 1;
+  }
+
+  vkCmdDraw(command_->GetCommandBuffer(), vertex_count, instance_count, 0, 0);
+
+  return {};
+}
+
+Result GraphicsPipeline::SubmitProbeCommand() {
+  Result r = command_->BeginIfNotInRecording();
+  if (!r.IsSuccess())
+    return r;
+
+  ActivateRenderPassIfNeeded();
+  DeactivateRenderPassIfNeeded();
+
+  r = frame_->CopyColorImageToHost(command_->GetCommandBuffer());
+  if (!r.IsSuccess())
+    return r;
+
+  r = command_->End();
+  if (!r.IsSuccess())
+    return r;
+
+  return command_->SubmitAndReset();
+}
+
+Result GraphicsPipeline::VerifyPixels(const uint32_t x,
+                                      const uint32_t y,
+                                      const uint32_t width,
+                                      const uint32_t height,
+                                      const ProbeCommand* command) {
+  const uint32_t stride = VkFormatToByteSize(color_format_);
+
+  // TODO(jaebaek): Support all VkFormat
+  const uint8_t* ptr = static_cast<const uint8_t*>(frame_->GetColorBufferPtr());
+  uint32_t count_of_invalid_pixels = 0;
+  uint32_t first_invalid_i = 0;
+  uint32_t first_invalid_j = 0;
+  for (uint32_t j = 0; j < height; ++j) {
+    const uint8_t* p = ptr + stride * frame_->GetWidth() * (j + y) + stride * x;
+    for (uint32_t i = 0; i < width; ++i) {
+      // TODO(jaebaek): Get actual pixel values based on frame buffer formats.
+      if (!IsFloatPixelEqualInt(command->GetR(), p[stride * i]) ||
+          !IsFloatPixelEqualInt(command->GetG(), p[stride * i + 1]) ||
+          !IsFloatPixelEqualInt(command->GetB(), p[stride * i + 2]) ||
+          (command->IsRGBA() &&
+           !IsFloatPixelEqualInt(command->GetA(), p[stride * i + 3]))) {
+        if (!count_of_invalid_pixels) {
+          first_invalid_i = i;
+          first_invalid_j = j;
+        }
+        ++count_of_invalid_pixels;
+      }
+    }
+  }
+
+  if (count_of_invalid_pixels) {
+    const uint8_t* p =
+        ptr + stride * frame_->GetWidth() * (first_invalid_j + y) + stride * x;
+    return Result(
+        "Probe failed at: " + std::to_string(first_invalid_i + x) + ", " +
+        std::to_string(first_invalid_j + y) + "\n" +
+        "  Expected RGBA: " + std::to_string(command->GetR() * 255) + ", " +
+        std::to_string(command->GetG() * 255) + ", " +
+        std::to_string(command->GetB() * 255) +
+        (command->IsRGBA() ? ", " + std::to_string(command->GetA() * 255) +
+                                 "\n  Actual RGBA: "
+                           : "\n  Actual RGB: ") +
+        std::to_string(static_cast<int>(p[stride * first_invalid_i])) + ", " +
+        std::to_string(static_cast<int>(p[stride * first_invalid_i + 1])) +
+        ", " +
+        std::to_string(static_cast<int>(p[stride * first_invalid_i + 2])) +
+        (command->IsRGBA() ? ", " + std::to_string(static_cast<int>(
+                                        p[stride * first_invalid_i + 3]))
+                           : "") +
+        "\n" + "Probe failed in " + std::to_string(count_of_invalid_pixels) +
+        " pixels");
+  }
+
+  return {};
+}
+
+Result GraphicsPipeline::Probe(const ProbeCommand* command) {
+  uint32_t x = 0;
+  uint32_t y = 0;
+  uint32_t width = 0;
+  uint32_t height = 0;
+  const uint32_t frame_width = frame_->GetWidth();
+  const uint32_t frame_height = frame_->GetHeight();
+
+  if (command->IsWholeWindow()) {
+    width = frame_width;
+    height = frame_height;
+  } else if (command->IsRelative()) {
+    x = static_cast<uint32_t>(frame_width * command->GetX());
+    y = static_cast<uint32_t>(frame_height * command->GetY());
+    width = static_cast<uint32_t>(frame_width * command->GetWidth());
+    height = static_cast<uint32_t>(frame_height * command->GetHeight());
+  } else {
+    x = static_cast<uint32_t>(command->GetX());
+    y = static_cast<uint32_t>(command->GetY());
+    width = static_cast<uint32_t>(command->GetWidth());
+    height = static_cast<uint32_t>(command->GetHeight());
+  }
+
+  if (x + width > frame_width || y + height > frame_height) {
+    return Result(
+        "Vulkan::Probe Position(" + std::to_string(x + width - 1) + ", " +
+        std::to_string(y + height - 1) + ") is out of framebuffer scope (" +
+        std::to_string(frame_width) + "," + std::to_string(frame_height) + ")");
+  }
+
+  Result r = SubmitProbeCommand();
+  if (!r.IsSuccess())
+    return r;
+
+  return VerifyPixels(x, y, width, height, command);
+}
+
+void GraphicsPipeline::Shutdown() {
+  DeactivateRenderPassIfNeeded();
+
+  Result r = command_->End();
+  if (r.IsSuccess())
+    command_->SubmitAndReset();
+
+  Pipeline::Shutdown();
+  frame_->Shutdown();
+  vkDestroyRenderPass(device_, render_pass_, nullptr);
+}
+
+}  // namespace vulkan
+}  // namespace amber
diff --git a/src/vulkan/graphics_pipeline.h b/src/vulkan/graphics_pipeline.h
new file mode 100644
index 0000000..9608dc9
--- /dev/null
+++ b/src/vulkan/graphics_pipeline.h
@@ -0,0 +1,121 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VULKAN_GRAPHICS_PIPELINE_H_
+#define SRC_VULKAN_GRAPHICS_PIPELINE_H_
+
+#include <memory>
+#include <vector>
+
+#include "amber/result.h"
+#include "src/buffer_data.h"
+#include "src/format.h"
+#include "src/value.h"
+#include "src/vulkan/frame_buffer.h"
+#include "src/vulkan/pipeline.h"
+#include "src/vulkan/vertex_buffer.h"
+#include "vulkan/vulkan.h"
+
+namespace amber {
+
+class ProbeCommand;
+
+namespace vulkan {
+
+class GraphicsPipeline : public Pipeline {
+ public:
+  GraphicsPipeline(PipelineType type,
+                   VkDevice device,
+                   const VkPhysicalDeviceMemoryProperties& properties,
+                   VkFormat color_format,
+                   VkFormat depth_stencil_format,
+                   std::vector<VkPipelineShaderStageCreateInfo>);
+  ~GraphicsPipeline() override;
+
+  Result Initialize(uint32_t width,
+                    uint32_t height,
+                    VkCommandPool pool,
+                    VkQueue queue);
+  void Shutdown() override;
+
+  void SetBuffer(BufferType type,
+                 uint8_t location,
+                 const Format& format,
+                 const std::vector<Value>& values);
+
+  Result Clear();
+  Result ClearBuffer(const VkClearValue& clear_value,
+                     VkImageAspectFlags aspect);
+  Result Probe(const ProbeCommand*);
+
+  VkFormat GetColorFormat() const { return color_format_; }
+  VkFormat GetDepthStencilFormat() const { return depth_stencil_format_; }
+
+  Result SetClearColor(float r, float g, float b, float a);
+  Result SetClearStencil(uint32_t stencil);
+  Result SetClearDepth(float depth);
+
+  Result Draw();
+
+ private:
+  enum class RenderPassState : uint8_t {
+    kActive = 0,
+    kInactive,
+  };
+
+  Result CreateVkGraphicsPipeline();
+
+  Result CreateRenderPass();
+  void ActivateRenderPassIfNeeded();
+  void DeactivateRenderPassIfNeeded();
+
+  Result SendBufferDataIfNeeded();
+
+  // TODO(jaebaek): Implement image/ssbo probe.
+  Result SubmitProbeCommand();
+  Result VerifyPixels(const uint32_t x,
+                      const uint32_t y,
+                      const uint32_t width,
+                      const uint32_t height,
+                      const ProbeCommand* command);
+
+  VkPipelineDepthStencilStateCreateInfo GetPipelineDepthStencilInfo();
+  VkPipelineColorBlendAttachmentState GetPipelineColorBlendAttachmentState();
+
+  VkRenderPass render_pass_ = VK_NULL_HANDLE;
+  RenderPassState render_pass_state_ = RenderPassState::kInactive;
+
+  std::unique_ptr<FrameBuffer> frame_;
+  VkFormat color_format_;
+  VkFormat depth_stencil_format_;
+
+  std::vector<VkPipelineShaderStageCreateInfo> shader_stage_info_;
+
+  uint32_t frame_width_ = 0;
+  uint32_t frame_height_ = 0;
+
+  float clear_color_r_ = 0;
+  float clear_color_g_ = 0;
+  float clear_color_b_ = 0;
+  float clear_color_a_ = 0;
+  uint32_t clear_stencil_ = 0;
+  float clear_depth_ = 1.0f;
+
+  std::unique_ptr<VertexBuffer> vertex_buffer_;
+};
+
+}  // namespace vulkan
+}  // namespace amber
+
+#endif  // SRC_VULKAN_GRAPHICS_PIPELINE_H_
diff --git a/src/vulkan/image.cc b/src/vulkan/image.cc
new file mode 100644
index 0000000..4722187
--- /dev/null
+++ b/src/vulkan/image.cc
@@ -0,0 +1,151 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vulkan/image.h"
+
+#include <limits>
+
+#include "src/vulkan/format_data.h"
+
+namespace amber {
+namespace vulkan {
+namespace {
+
+const VkImageCreateInfo kDefaultImageInfo = {
+    VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, /* sType */
+    nullptr,                             /* pNext */
+    0,                                   /* flags */
+    VK_IMAGE_TYPE_2D,                    /* imageType */
+    VK_FORMAT_R8G8B8A8_UNORM,            /* format */
+    {250, 250, 1},                       /* extent */
+    1,                                   /* mipLevels */
+    1,                                   /* arrayLayers */
+    VK_SAMPLE_COUNT_1_BIT,               /* samples */
+    VK_IMAGE_TILING_OPTIMAL,             /* tiling */
+    VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
+        VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, /* usage */
+    VK_SHARING_MODE_EXCLUSIVE,               /* sharingMode */
+    0,                                       /* queueFamilyIndexCount */
+    nullptr,                                 /* pQueueFamilyIndices */
+    VK_IMAGE_LAYOUT_UNDEFINED,               /* initialLayout */
+};
+
+}  // namespace
+
+Image::Image(VkDevice device,
+             VkFormat format,
+             uint32_t x,
+             uint32_t y,
+             uint32_t z,
+             const VkPhysicalDeviceMemoryProperties& properties)
+    : Resource(device, x * y * z * VkFormatToByteSize(format), properties),
+      image_info_(kDefaultImageInfo) {
+  image_info_.format = format;
+  image_info_.extent = {x, y, z};
+}
+
+Image::~Image() = default;
+
+Result Image::Initialize(VkImageUsageFlags usage) {
+  if (image_ != VK_NULL_HANDLE)
+    return Result("Vulkan::Image was already initalized");
+
+  image_info_.usage = usage;
+
+  if (vkCreateImage(GetDevice(), &image_info_, nullptr, &image_) != VK_SUCCESS)
+    return Result("Vulkan::Calling vkCreateImage Fail");
+
+  AllocateResult allocate_result = AllocateAndBindMemoryToVkImage(
+      image_, &memory_, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, false);
+  if (!allocate_result.r.IsSuccess())
+    return allocate_result.r;
+
+  Result r = CreateVkImageView();
+  if (!r.IsSuccess())
+    return r;
+
+  if (CheckMemoryHostAccessible(allocate_result.memory_type_index)) {
+    is_image_host_accessible_ = true;
+    return MapMemory(memory_);
+  }
+
+  is_image_host_accessible_ = false;
+  return Resource::Initialize();
+}
+
+Result Image::CreateVkImageView() {
+  VkImageViewCreateInfo image_view_info = {};
+  image_view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
+  image_view_info.image = image_;
+  // TODO(jaebaek): Set .viewType correctly
+  image_view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
+  image_view_info.format = image_info_.format;
+  image_view_info.components = {
+      VK_COMPONENT_SWIZZLE_R,
+      VK_COMPONENT_SWIZZLE_G,
+      VK_COMPONENT_SWIZZLE_B,
+      VK_COMPONENT_SWIZZLE_A,
+  };
+  image_view_info.subresourceRange = {
+      VK_IMAGE_ASPECT_COLOR_BIT, /* aspectMask */
+      0,                         /* baseMipLevel */
+      1,                         /* levelCount */
+      0,                         /* baseArrayLayer */
+      1,                         /* layerCount */
+  };
+
+  if (vkCreateImageView(GetDevice(), &image_view_info, nullptr, &view_) !=
+      VK_SUCCESS) {
+    return Result("Vulkan::Calling vkCreateImageView Fail");
+  }
+
+  return {};
+}
+
+void Image::Shutdown() {
+  vkDestroyImageView(GetDevice(), view_, nullptr);
+  vkDestroyImage(GetDevice(), image_, nullptr);
+  vkFreeMemory(GetDevice(), memory_, nullptr);
+
+  view_ = VK_NULL_HANDLE;
+  image_ = VK_NULL_HANDLE;
+  memory_ = VK_NULL_HANDLE;
+}
+
+Result Image::CopyToHost(VkCommandBuffer command) {
+  if (is_image_host_accessible_)
+    return {};
+
+  VkBufferImageCopy copy_region = {};
+  copy_region.bufferOffset = 0;
+  copy_region.bufferRowLength = 0;
+  copy_region.bufferImageHeight = 0;
+  copy_region.imageSubresource = {
+      VK_IMAGE_ASPECT_COLOR_BIT, /* aspectMask */
+      0,                         /* mipLevel */
+      0,                         /* baseArrayLayer */
+      1,                         /* layerCount */
+  };
+  copy_region.imageOffset = {0, 0, 0};
+  copy_region.imageExtent = {image_info_.extent.width,
+                             image_info_.extent.height, 1};
+
+  vkCmdCopyImageToBuffer(command, image_, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+                         GetHostAccessibleBuffer(), 1, &copy_region);
+
+  return {};
+}
+
+}  // namespace vulkan
+}  // namespace amber
diff --git a/src/vulkan/image.h b/src/vulkan/image.h
new file mode 100644
index 0000000..c946af4
--- /dev/null
+++ b/src/vulkan/image.h
@@ -0,0 +1,68 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VULKAN_IMAGE_H_
+#define SRC_VULKAN_IMAGE_H_
+
+#include "amber/result.h"
+#include "src/vulkan/resource.h"
+#include "vulkan/vulkan.h"
+
+namespace amber {
+namespace vulkan {
+
+class Image : public Resource {
+ public:
+  Image(VkDevice device,
+        VkFormat format,
+        uint32_t x,
+        uint32_t y,
+        uint32_t z,
+        const VkPhysicalDeviceMemoryProperties& properties);
+  ~Image() override;
+
+  Result Initialize(VkImageUsageFlags usage);
+  VkImage GetVkImage() const { return image_; }
+  VkImageView GetVkImageView() const { return view_; }
+
+  // TODO(jaebaek): Determine copy all or partial data
+  Result CopyToHost(VkCommandBuffer command);
+
+  // TODO(jaebaek): Implement CopyToDevice
+
+  // Resource
+  VkDeviceMemory GetHostAccessMemory() const override {
+    if (is_image_host_accessible_)
+      return memory_;
+
+    return Resource::GetHostAccessMemory();
+  }
+
+  void Shutdown() override;
+
+ private:
+  Result CreateVkImageView();
+
+  VkImageCreateInfo image_info_;
+
+  VkImage image_ = VK_NULL_HANDLE;
+  VkImageView view_ = VK_NULL_HANDLE;
+  VkDeviceMemory memory_ = VK_NULL_HANDLE;
+  bool is_image_host_accessible_ = false;
+};
+
+}  // namespace vulkan
+}  // namespace amber
+
+#endif  // SRC_VULKAN_IMAGE_H_
diff --git a/src/vulkan/pipeline.cc b/src/vulkan/pipeline.cc
new file mode 100644
index 0000000..e16c277
--- /dev/null
+++ b/src/vulkan/pipeline.cc
@@ -0,0 +1,68 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vulkan/pipeline.h"
+
+#include <limits>
+
+#include "src/command.h"
+#include "src/make_unique.h"
+#include "src/vulkan/graphics_pipeline.h"
+
+namespace amber {
+namespace vulkan {
+
+Pipeline::Pipeline(PipelineType type,
+                   VkDevice device,
+                   const VkPhysicalDeviceMemoryProperties& properties)
+    : device_(device), memory_properties_(properties), pipeline_type_(type) {}
+
+Pipeline::~Pipeline() = default;
+
+GraphicsPipeline* Pipeline::AsGraphics() {
+  return static_cast<GraphicsPipeline*>(this);
+}
+
+Result Pipeline::InitializeCommandBuffer(VkCommandPool pool, VkQueue queue) {
+  command_ = MakeUnique<CommandBuffer>(device_, pool, queue);
+  Result r = command_->Initialize();
+  if (!r.IsSuccess())
+    return r;
+
+  return {};
+}
+
+void Pipeline::Shutdown() {
+  // TODO(jaebaek): destroy pipeline_cache_ and pipeline_
+  command_->Shutdown();
+  vkDestroyPipelineLayout(device_, pipeline_layout_, nullptr);
+}
+
+Result Pipeline::CreatePipelineLayout() {
+  VkPipelineLayoutCreateInfo pipeline_layout_info = {};
+  pipeline_layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
+  pipeline_layout_info.setLayoutCount = 0;
+  pipeline_layout_info.pSetLayouts = nullptr;
+  // TODO(jaebaek): Push constant for pipeline_layout_info.
+
+  if (vkCreatePipelineLayout(device_, &pipeline_layout_info, nullptr,
+                             &pipeline_layout_) != VK_SUCCESS) {
+    return Result("Vulkan::Calling vkCreatePipelineLayout Fail");
+  }
+
+  return {};
+}
+
+}  // namespace vulkan
+}  // namespace amber
diff --git a/src/vulkan/pipeline.h b/src/vulkan/pipeline.h
new file mode 100644
index 0000000..38a234a
--- /dev/null
+++ b/src/vulkan/pipeline.h
@@ -0,0 +1,67 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VULKAN_PIPELINE_H_
+#define SRC_VULKAN_PIPELINE_H_
+
+#include <memory>
+#include <vector>
+
+#include "amber/result.h"
+#include "src/engine.h"
+#include "src/vulkan/command.h"
+#include "vulkan/vulkan.h"
+
+namespace amber {
+namespace vulkan {
+
+class GraphicsPipeline;
+
+class Pipeline {
+ public:
+  virtual ~Pipeline();
+
+  bool IsGraphics() const { return pipeline_type_ == PipelineType::kGraphics; }
+  bool IsCompute() const { return pipeline_type_ == PipelineType::kCompute; }
+
+  GraphicsPipeline* AsGraphics();
+
+  virtual void Shutdown();
+
+ protected:
+  Pipeline(PipelineType type,
+           VkDevice device,
+           const VkPhysicalDeviceMemoryProperties& properties);
+  Result InitializeCommandBuffer(VkCommandPool pool, VkQueue queue);
+
+  Result CreatePipelineLayout();
+
+  VkPipelineCache pipeline_cache_ = VK_NULL_HANDLE;
+  VkPipeline pipeline_ = VK_NULL_HANDLE;
+  VkPipelineLayout pipeline_layout_ = VK_NULL_HANDLE;
+
+  std::vector<VkDescriptorSetLayout> descriptor_set_layout_;
+
+  VkDevice device_ = VK_NULL_HANDLE;
+  VkPhysicalDeviceMemoryProperties memory_properties_;
+  std::unique_ptr<CommandBuffer> command_;
+
+ private:
+  PipelineType pipeline_type_;
+};
+
+}  // namespace vulkan
+}  // namespace amber
+
+#endif  // SRC_VULKAN_PIPELINE_H_
diff --git a/src/vulkan/resource.cc b/src/vulkan/resource.cc
new file mode 100644
index 0000000..69a21a5
--- /dev/null
+++ b/src/vulkan/resource.cc
@@ -0,0 +1,209 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vulkan/resource.h"
+
+#include <limits>
+
+#include "src/make_unique.h"
+#include "src/vulkan/format_data.h"
+
+namespace amber {
+namespace vulkan {
+
+Resource::Resource(VkDevice device,
+                   size_t size,
+                   const VkPhysicalDeviceMemoryProperties& properties)
+    : device_(device), size_(size), physical_memory_properties_(properties) {}
+
+Resource::~Resource() = default;
+
+void Resource::Shutdown() {
+  UnMapMemory(host_accessible_memory_);
+  vkDestroyBuffer(device_, host_accessible_buffer_, nullptr);
+  vkFreeMemory(device_, host_accessible_memory_, nullptr);
+}
+
+Result Resource::Initialize() {
+  Result r = CreateVkBuffer(
+      &host_accessible_buffer_,
+      VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
+  if (!r.IsSuccess())
+    return r;
+
+  AllocateResult allocate_result = AllocateAndBindMemoryToVkBuffer(
+      host_accessible_buffer_, &host_accessible_memory_,
+      VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
+          VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
+      true);
+  if (!allocate_result.r.IsSuccess())
+    return allocate_result.r;
+
+  return MapMemory(host_accessible_memory_);
+}
+
+Result Resource::CreateVkBuffer(VkBuffer* buffer, VkBufferUsageFlags usage) {
+  if (buffer == nullptr)
+    return Result("Vulkan::Given VkBuffer pointer is nullptr");
+
+  VkBufferCreateInfo buffer_info = {};
+  buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
+  buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
+  buffer_info.size = size_;
+  buffer_info.usage = usage;
+
+  if (vkCreateBuffer(device_, &buffer_info, nullptr, buffer) != VK_SUCCESS)
+    return Result("Vulkan::Calling vkCreateBuffer Fail");
+
+  return {};
+}
+
+uint32_t Resource::ChooseMemory(uint32_t memory_type_bits,
+                                VkMemoryPropertyFlags flags,
+                                bool force_flags) {
+  // Based on Vulkan spec about VkMemoryRequirements, N th bit of
+  // |memory_type_bits| is 1 where N can be the proper memory type index.
+  // This code is looking for the first non-zero bit whose memory type
+  // VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT property. If not exists,
+  // it returns the first non-zero bit.
+  uint32_t first_non_zero = std::numeric_limits<uint32_t>::max();
+  uint32_t memory_type_index = 0;
+  while (memory_type_bits) {
+    if (memory_type_bits % 2) {
+      if (first_non_zero == std::numeric_limits<uint32_t>::max())
+        first_non_zero = memory_type_index;
+
+      if ((physical_memory_properties_.memoryTypes[memory_type_index]
+               .propertyFlags &
+           flags) == flags) {
+        return memory_type_index;
+      }
+    }
+
+    ++memory_type_index;
+    memory_type_bits >>= 1;
+  }
+
+  if (force_flags)
+    return std::numeric_limits<uint32_t>::max();
+
+  return first_non_zero;
+}
+
+const VkMemoryRequirements Resource::GetVkBufferMemoryRequirements(
+    VkBuffer buffer) const {
+  VkMemoryRequirements requirement;
+  vkGetBufferMemoryRequirements(device_, buffer, &requirement);
+  return requirement;
+}
+
+const VkMemoryRequirements Resource::GetVkImageMemoryRequirements(
+    VkImage image) const {
+  VkMemoryRequirements requirement;
+  vkGetImageMemoryRequirements(device_, image, &requirement);
+  return requirement;
+}
+
+Resource::AllocateResult Resource::AllocateAndBindMemoryToVkBuffer(
+    VkBuffer buffer,
+    VkDeviceMemory* memory,
+    VkMemoryPropertyFlags flags,
+    bool force_flags) {
+  if (buffer == VK_NULL_HANDLE)
+    return {Result("Vulkan::Given VkBuffer is VK_NULL_HANDLE"), 0};
+
+  if (memory == nullptr)
+    return {Result("Vulkan::Given VkDeviceMemory pointer is nullptr"), 0};
+
+  auto requirement = GetVkBufferMemoryRequirements(buffer);
+
+  uint32_t memory_type_index =
+      ChooseMemory(requirement.memoryTypeBits, flags, force_flags);
+  if (memory_type_index == std::numeric_limits<uint32_t>::max())
+    return {Result("Vulkan::Find Proper Memory Fail"), 0};
+
+  Result r = AllocateMemory(memory, requirement.size, memory_type_index);
+  if (!r.IsSuccess())
+    return {r, 0};
+
+  return {BindMemoryToVkBuffer(buffer, *memory), memory_type_index};
+}
+
+Result Resource::AllocateMemory(VkDeviceMemory* memory,
+                                VkDeviceSize size,
+                                uint32_t memory_type_index) {
+  VkMemoryAllocateInfo alloc_info = {};
+  alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
+  alloc_info.allocationSize = size;
+  alloc_info.memoryTypeIndex = memory_type_index;
+  if (vkAllocateMemory(device_, &alloc_info, nullptr, memory) != VK_SUCCESS)
+    return Result("Vulkan::Calling vkAllocateMemory Fail");
+
+  return {};
+}
+
+Result Resource::BindMemoryToVkBuffer(VkBuffer buffer, VkDeviceMemory memory) {
+  if (vkBindBufferMemory(device_, buffer, memory, 0) != VK_SUCCESS)
+    return Result("Vulkan::Calling vkBindBufferMemory Fail");
+
+  return {};
+}
+
+Resource::AllocateResult Resource::AllocateAndBindMemoryToVkImage(
+    VkImage image,
+    VkDeviceMemory* memory,
+    VkMemoryPropertyFlags flags,
+    bool force_flags) {
+  if (image == nullptr)
+    return {Result("Vulkan::Given VkImage pointer is nullptr"), 0};
+
+  if (memory == nullptr)
+    return {Result("Vulkan::Given VkDeviceMemory pointer is nullptr"), 0};
+
+  auto requirement = GetVkImageMemoryRequirements(image);
+
+  uint32_t memory_type_index =
+      ChooseMemory(requirement.memoryTypeBits, flags, force_flags);
+  if (memory_type_index == std::numeric_limits<uint32_t>::max())
+    return {Result("Vulkan::Find Proper Memory Fail"), 0};
+
+  Result r = AllocateMemory(memory, requirement.size, memory_type_index);
+  if (!r.IsSuccess())
+    return {r, 0};
+
+  return {BindMemoryToVkImage(image, *memory), memory_type_index};
+}
+
+Result Resource::BindMemoryToVkImage(VkImage image, VkDeviceMemory memory) {
+  if (vkBindImageMemory(device_, image, memory, 0) != VK_SUCCESS)
+    return Result("Vulkan::Calling vkBindImageMemory Fail");
+
+  return {};
+}
+
+Result Resource::MapMemory(VkDeviceMemory memory) {
+  if (vkMapMemory(device_, memory, 0, VK_WHOLE_SIZE, 0, &memory_ptr_) !=
+      VK_SUCCESS) {
+    return Result("Vulkan::Calling vkMapMemory Fail");
+  }
+
+  return {};
+}
+
+void Resource::UnMapMemory(VkDeviceMemory memory) {
+  vkUnmapMemory(device_, memory);
+}
+
+}  // namespace vulkan
+}  // namespace amber
diff --git a/src/vulkan/resource.h b/src/vulkan/resource.h
new file mode 100644
index 0000000..c2530ae
--- /dev/null
+++ b/src/vulkan/resource.h
@@ -0,0 +1,101 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VULKAN_RESOURCE_H_
+#define SRC_VULKAN_RESOURCE_H_
+
+#include <memory>
+
+#include "amber/result.h"
+#include "vulkan/vulkan.h"
+
+namespace amber {
+namespace vulkan {
+
+class Resource {
+ public:
+  virtual ~Resource();
+
+  virtual VkDeviceMemory GetHostAccessMemory() const {
+    return host_accessible_memory_;
+  }
+
+  virtual void Shutdown();
+
+  void* HostAccessibleMemoryPtr() const { return memory_ptr_; }
+
+ protected:
+  Resource(VkDevice device,
+           size_t size,
+           const VkPhysicalDeviceMemoryProperties& properties);
+  Result Initialize();
+  Result CreateVkBuffer(VkBuffer* buffer, VkBufferUsageFlags usage);
+
+  VkDevice GetDevice() const { return device_; }
+  VkBuffer GetHostAccessibleBuffer() const { return host_accessible_buffer_; }
+
+  size_t GetSize() const { return size_; }
+
+  struct AllocateResult {
+    Result r;
+    uint32_t memory_type_index;
+  };
+
+  AllocateResult AllocateAndBindMemoryToVkBuffer(VkBuffer buffer,
+                                                 VkDeviceMemory* memory,
+                                                 VkMemoryPropertyFlags flags,
+                                                 bool force_flags);
+  AllocateResult AllocateAndBindMemoryToVkImage(VkImage image,
+                                                VkDeviceMemory* memory,
+                                                VkMemoryPropertyFlags flags,
+                                                bool force_flags);
+
+  bool CheckMemoryHostAccessible(uint32_t memory_type_index) {
+    return (physical_memory_properties_.memoryTypes[memory_type_index]
+                .propertyFlags &
+            VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) ==
+           VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT;
+  }
+
+  Result MapMemory(VkDeviceMemory memory);
+  void UnMapMemory(VkDeviceMemory memory);
+
+ private:
+  uint32_t ChooseMemory(uint32_t memory_type_bits,
+                        VkMemoryPropertyFlags flags,
+                        bool force_flags);
+  Result AllocateMemory(VkDeviceMemory* memory,
+                        VkDeviceSize size,
+                        uint32_t memory_type_index);
+
+  Result BindMemoryToVkBuffer(VkBuffer buffer, VkDeviceMemory memory);
+  const VkMemoryRequirements GetVkBufferMemoryRequirements(
+      VkBuffer buffer) const;
+
+  Result BindMemoryToVkImage(VkImage image, VkDeviceMemory memory);
+  const VkMemoryRequirements GetVkImageMemoryRequirements(VkImage image) const;
+
+  VkDevice device_ = VK_NULL_HANDLE;
+  size_t size_ = 0;
+  VkPhysicalDeviceMemoryProperties physical_memory_properties_;
+
+  VkBuffer host_accessible_buffer_ = VK_NULL_HANDLE;
+  VkDeviceMemory host_accessible_memory_ = VK_NULL_HANDLE;
+  void* memory_ptr_ = nullptr;
+};
+
+}  // namespace vulkan
+}  // namespace amber
+
+#endif  // SRC_VULKAN_RESOURCE_H_
diff --git a/src/vulkan/vertex_buffer.cc b/src/vulkan/vertex_buffer.cc
new file mode 100644
index 0000000..943cbb1
--- /dev/null
+++ b/src/vulkan/vertex_buffer.cc
@@ -0,0 +1,118 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/vulkan/vertex_buffer.h"
+
+#include <cassert>
+#include <cstring>
+
+#include "src/make_unique.h"
+#include "src/vulkan/bit_copy.h"
+#include "src/vulkan/format_data.h"
+
+namespace amber {
+namespace vulkan {
+
+VertexBuffer::VertexBuffer(VkDevice device) : device_(device) {}
+
+VertexBuffer::~VertexBuffer() = default;
+
+void VertexBuffer::SetData(uint8_t location,
+                           const Format& format,
+                           const std::vector<Value>& values) {
+  vertex_attr_desc_.emplace_back();
+  // TODO(jaebaek): Support multiple binding
+  vertex_attr_desc_.back().binding = 0;
+  vertex_attr_desc_.back().location = location;
+  vertex_attr_desc_.back().format = ToVkFormat(format.GetFormatType());
+  vertex_attr_desc_.back().offset = stride_in_bytes_;
+
+  stride_in_bytes_ += format.GetByteSize();
+
+  formats_.push_back(format);
+  data_.push_back(values);
+}
+
+void VertexBuffer::FillVertexBufferWithData(VkCommandBuffer command) {
+  // Send vertex data from host to device.
+  uint8_t* ptr = static_cast<uint8_t*>(buffer_->HostAccessibleMemoryPtr());
+  for (uint32_t i = 0; i < GetVertexCount(); ++i) {
+    for (uint32_t j = 0; j < formats_.size(); ++j) {
+      const auto pack_size = formats_[j].GetPackSize();
+      if (pack_size) {
+        BitCopy::CopyValueToBuffer(ptr, data_[j][i], 0, pack_size);
+        ptr += pack_size / 8;
+        continue;
+      }
+
+      const auto& components = formats_[j].GetComponents();
+      uint8_t bit_offset = 0;
+
+      for (uint32_t k = 0; k < components.size(); ++k) {
+        uint8_t bits = components[k].num_bits;
+        BitCopy::CopyValueToBuffer(ptr, data_[j][i * components.size() + k],
+                                   bit_offset, bits);
+
+        assert(k == components.size() - 1 ||
+               static_cast<uint32_t>(bit_offset) + static_cast<uint32_t>(bits) <
+                   256);
+        bit_offset += bits;
+      }
+
+      ptr += formats_[j].GetByteSize();
+    }
+  }
+
+  ptr = static_cast<uint8_t*>(buffer_->HostAccessibleMemoryPtr());
+  buffer_->CopyToDevice(command);
+}
+
+void VertexBuffer::BindToCommandBuffer(VkCommandBuffer command) {
+  const VkDeviceSize offset = 0;
+  const VkBuffer buffer = buffer_->GetVkBuffer();
+  // TODO(jaebaek): Support multiple binding
+  vkCmdBindVertexBuffers(command, 0, 1, &buffer, &offset);
+}
+
+Result VertexBuffer::SendVertexData(
+    VkCommandBuffer command,
+    const VkPhysicalDeviceMemoryProperties& properties) {
+  if (!is_vertex_data_pending_)
+    return Result("Vulkan::Vertices data was already sent");
+
+  const size_t n_vertices = GetVertexCount();
+  if (n_vertices == 0)
+    return Result("Vulkan::Data for VertexBuffer is empty");
+
+  size_t bytes = stride_in_bytes_ * n_vertices;
+
+  if (!buffer_) {
+    buffer_ = MakeUnique<Buffer>(device_, bytes, properties);
+    Result r = buffer_->Initialize(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT |
+                                   VK_BUFFER_USAGE_TRANSFER_DST_BIT);
+    if (!r.IsSuccess())
+      return r;
+  }
+
+  if (formats_.empty() || formats_[0].GetComponents().empty())
+    return Result("Vulkan::Formats for VertexBuffer is empty");
+
+  FillVertexBufferWithData(command);
+
+  is_vertex_data_pending_ = false;
+  return {};
+}
+
+}  // namespace vulkan
+}  // namespace amber
diff --git a/src/vulkan/vertex_buffer.h b/src/vulkan/vertex_buffer.h
new file mode 100644
index 0000000..ef0c242
--- /dev/null
+++ b/src/vulkan/vertex_buffer.h
@@ -0,0 +1,84 @@
+// Copyright 2018 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 implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_VULKAN_VERTEX_BUFFER_H_
+#define SRC_VULKAN_VERTEX_BUFFER_H_
+
+#include <memory>
+#include <vector>
+
+#include "amber/result.h"
+#include "src/format.h"
+#include "src/value.h"
+#include "src/vulkan/buffer.h"
+#include "vulkan/vulkan.h"
+
+namespace amber {
+namespace vulkan {
+
+class VertexBuffer {
+ public:
+  explicit VertexBuffer(VkDevice device);
+  ~VertexBuffer();
+
+  Result SendVertexData(VkCommandBuffer command,
+                        const VkPhysicalDeviceMemoryProperties& properties);
+  bool VertexDataSent() { return !is_vertex_data_pending_; }
+
+  void SetData(uint8_t location,
+               const Format& format,
+               const std::vector<Value>& values);
+
+  const std::vector<VkVertexInputAttributeDescription>& GetVertexInputAttr()
+      const {
+    return vertex_attr_desc_;
+  }
+
+  VkVertexInputBindingDescription GetVertexInputBinding() const {
+    VkVertexInputBindingDescription vertex_binding_desc = {};
+    vertex_binding_desc.binding = 0;
+    vertex_binding_desc.stride = stride_in_bytes_;
+    vertex_binding_desc.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
+    return vertex_binding_desc;
+  }
+
+  size_t GetVertexCount() const {
+    if (data_.empty())
+      return 0;
+
+    return data_[0].size() / formats_[0].GetComponents().size();
+  }
+
+  void BindToCommandBuffer(VkCommandBuffer command);
+
+ private:
+  void FillVertexBufferWithData(VkCommandBuffer command);
+
+  VkDevice device_ = VK_NULL_HANDLE;
+
+  bool is_vertex_data_pending_ = true;
+
+  std::unique_ptr<Buffer> buffer_;
+  uint32_t stride_in_bytes_ = 0;
+
+  std::vector<Format> formats_;
+  std::vector<std::vector<Value>> data_;
+
+  std::vector<VkVertexInputAttributeDescription> vertex_attr_desc_;
+};
+
+}  // namespace vulkan
+}  // namespace amber
+
+#endif  // SRC_VULKAN_VERTEX_BUFFER_H_
diff --git a/tests/cases/clear.amber b/tests/cases/clear.amber
new file mode 100644
index 0000000..b178987
--- /dev/null
+++ b/tests/cases/clear.amber
@@ -0,0 +1,31 @@
+# Copyright 2018 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.
+
+[vertex shader passthrough]
+
+[fragment shader]
+#version 430
+
+layout(location = 0) in vec4 color_in;
+layout(location = 0) out vec4 color_out;
+
+void
+main()
+{
+  color_out = color_in;
+}
+
+[test]
+clear
+relative probe rect rgba (0.0, 0.0, 1.0, 1.0) (0, 0, 0, 0)
diff --git a/tests/cases/clear_and_probe_all_wrong_color.expect_fail.amber b/tests/cases/clear_and_probe_all_wrong_color.expect_fail.amber
new file mode 100644
index 0000000..fda1da2
--- /dev/null
+++ b/tests/cases/clear_and_probe_all_wrong_color.expect_fail.amber
@@ -0,0 +1,33 @@
+# Copyright 2018 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.
+
+[vertex shader passthrough]
+
+[fragment shader]
+#version 430
+
+layout(location = 0) in vec4 color_in;
+layout(location = 0) out vec4 color_out;
+
+void
+main()
+{
+  color_out = color_in;
+}
+
+[test]
+clear
+
+# Expected pixel color is (0, 0, 0, 0)
+relative probe rect rgba (0.0, 0.0, 1.0, 1.0) (1.0, 0, 0, 0)
diff --git a/tests/cases/clear_and_probe_small_wrong_color.expect_fail.amber b/tests/cases/clear_and_probe_small_wrong_color.expect_fail.amber
new file mode 100644
index 0000000..54eee4f
--- /dev/null
+++ b/tests/cases/clear_and_probe_small_wrong_color.expect_fail.amber
@@ -0,0 +1,33 @@
+# Copyright 2018 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.
+
+[vertex shader passthrough]
+
+[fragment shader]
+#version 430
+
+layout(location = 0) in vec4 color_in;
+layout(location = 0) out vec4 color_out;
+
+void
+main()
+{
+  color_out = color_in;
+}
+
+[test]
+clear
+
+# Expected pixel color is (0, 0, 0, 0)
+relative probe rect rgba (0.9, 0.9, 0.1, 0.1) (1.0, 0, 0, 0)
diff --git a/tests/cases/clear_and_probe_too_large_rect.expect_fail.amber b/tests/cases/clear_and_probe_too_large_rect.expect_fail.amber
new file mode 100644
index 0000000..ed58f41
--- /dev/null
+++ b/tests/cases/clear_and_probe_too_large_rect.expect_fail.amber
@@ -0,0 +1,33 @@
+# Copyright 2018 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.
+
+[vertex shader passthrough]
+
+[fragment shader]
+#version 430
+
+layout(location = 0) in vec4 color_in;
+layout(location = 0) out vec4 color_out;
+
+void
+main()
+{
+  color_out = color_in;
+}
+
+[test]
+clear
+
+# Probe rect is out of framebuffer i.e., 0.9 + 1.0 = 1.9 > 1.0
+relative probe rect rgba (0.9, 0.9, 1.0, 1.0) (1.0, 0, 0, 0)
diff --git a/tests/cases/clear_color.amber b/tests/cases/clear_color.amber
new file mode 100644
index 0000000..74433de
--- /dev/null
+++ b/tests/cases/clear_color.amber
@@ -0,0 +1,38 @@
+# Copyright 2018 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.
+
+[vertex shader passthrough]
+
+[fragment shader]
+#version 430
+
+layout(location = 0) in vec4 color_in;
+layout(location = 0) out vec4 color_out;
+
+void
+main()
+{
+  color_out = color_in;
+}
+
+[test]
+clear color 1 0.4 0.5 0.2
+clear
+relative probe rect rgba (0.0, 0.0, 1.0, 1.0) (1.0, 0.4, 0.5, 0.2)
+relative probe rect rgba (0.9, 0.9, 0.1, 0.1) (1.0, 0.4, 0.5, 0.2)
+
+clear color 0.4 0.2 1 0.5
+clear
+relative probe rect rgba (0.0, 0.0, 1.0, 1.0) (0.4, 0.2, 1.0, 0.5)
+relative probe rect rgba (0.9, 0.9, 0.1, 0.1) (0.4, 0.2, 1.0, 0.5)
diff --git a/tests/cases/clear_color_without_clear_command.expect_fail.amber b/tests/cases/clear_color_without_clear_command.expect_fail.amber
new file mode 100644
index 0000000..e367c6d
--- /dev/null
+++ b/tests/cases/clear_color_without_clear_command.expect_fail.amber
@@ -0,0 +1,34 @@
+# Copyright 2018 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.
+
+[vertex shader passthrough]
+
+[fragment shader]
+#version 430
+
+layout(location = 0) in vec4 color_in;
+layout(location = 0) out vec4 color_out;
+
+void
+main()
+{
+  color_out = color_in;
+}
+
+[test]
+clear color 0.1 0.2 0.3 0.4
+
+# No clear command before probe. Random pixels are expected.
+
+relative probe rect rgba (0.0, 0.0, 1.0, 1.0) (0.1, 0.2, 0.3, 0.4)
diff --git a/tests/cases/multiple_clear_color.amber b/tests/cases/multiple_clear_color.amber
new file mode 100644
index 0000000..ede2d82
--- /dev/null
+++ b/tests/cases/multiple_clear_color.amber
@@ -0,0 +1,45 @@
+# Copyright 2018 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.
+
+[vertex shader passthrough]
+
+[fragment shader]
+#version 430
+
+layout(location = 0) in vec4 color_in;
+layout(location = 0) out vec4 color_out;
+
+void
+main()
+{
+  color_out = color_in;
+}
+
+[test]
+clear color 0   1.4 0.6 0.1
+clear color 1   0.4 0.5 0.2
+clear color 0.4 0.2 1   0.5
+clear
+relative probe rect rgba (0.0, 0.0, 1.0, 1.0) (0.4, 0.2, 1.0, 0.5)
+relative probe rect rgba (0.9, 0.9, 0.1, 0.1) (0.4, 0.2, 1.0, 0.5)
+
+clear color 0   0   0   0
+clear color 0.4 0.2 1   0.5
+clear color 0   1.4 0.6 0.1
+clear color 1   0.4 0.5 0.2
+clear
+clear
+clear
+relative probe rect rgba (0.0, 0.0, 1.0, 1.0) (1.0, 0.4, 0.5, 0.2)
+relative probe rect rgba (0.9, 0.9, 0.1, 0.1) (1.0, 0.4, 0.5, 0.2)
diff --git a/tests/run_tests.py b/tests/run_tests.py
new file mode 100755
index 0000000..8353d25
--- /dev/null
+++ b/tests/run_tests.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env python
+#
+# Copyright 2018 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 implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+import difflib
+import optparse
+import os
+import re
+import subprocess
+import sys
+import tempfile
+
+class TestCase:
+  def __init__(self, input_path, parse_only):
+    self.input_path = input_path
+    self.parse_only = parse_only
+
+    self.results = {}
+
+  def IsExpectedFail(self):
+    fail_re = re.compile('^.+[.]expect_fail[.]amber')
+    return fail_re.match(self.GetInputPath())
+
+  def IsParseOnly(self):
+    return self.parse_only
+
+  def GetInputPath(self):
+    return self.input_path
+
+  def GetResult(self, fmt):
+    return self.results[fmt]
+
+
+class TestRunner:
+  def RunTest(self, tc):
+    print "Testing %s" % tc.GetInputPath()
+
+    cmd = [self.options.test_prog_path]
+    if tc.IsParseOnly():
+      cmd += ['-p']
+    cmd += [tc.GetInputPath()]
+
+    try:
+      subprocess.check_output(cmd)
+    except Exception as e:
+      if not tc.IsExpectedFail():
+        print e
+      return False
+
+    return True
+
+
+  def RunTests(self):
+    for tc in self.test_cases:
+      result = self.RunTest(tc)
+
+      if not tc.IsExpectedFail() and not result:
+        self.failures.append(tc.GetInputPath())
+      elif tc.IsExpectedFail() and result:
+        print("Expected: " + tc.GetInputPath() + " to fail but passed.")
+        self.failures.append(tc.GetInputPath())
+
+  def SummarizeResults(self):
+    if len(self.failures) > 0:
+      self.failures.sort()
+
+      print '\nSummary of Failures:'
+      for failure in self.failures:
+        print failure
+
+    print
+    print 'Test cases executed: %d' % len(self.test_cases)
+    print '  Successes: %d' % (len(self.test_cases) - len(self.failures))
+    print '  Failures:  %d' % len(self.failures)
+    print
+
+
+  def Run(self):
+    base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
+
+    usage = 'usage: %prog [options] (file)'
+    parser = optparse.OptionParser(usage=usage)
+    parser.add_option('--build-dir',
+                      default=os.path.join(base_path, 'out', 'Debug'),
+                      help='path to build directory')
+    parser.add_option('--test-dir',
+                      default=os.path.join(os.path.dirname(__file__), 'cases'),
+                      help='path to directory containing test files')
+    parser.add_option('--test-prog-path', default=None,
+                      help='path to program to test')
+    parser.add_option('--parse-only',
+                      action="store_true", default=False,
+                      help='only parse test cases; do not execute')
+
+    self.options, self.args = parser.parse_args()
+
+    if self.options.test_prog_path == None:
+      test_prog = os.path.abspath(os.path.join(self.options.build_dir, 'amber'))
+      if not os.path.isfile(test_prog):
+        print "Cannot find test program %s" % test_prog
+        return 1
+
+      self.options.test_prog_path = test_prog
+
+    if not os.path.isfile(self.options.test_prog_path):
+      print "--test-prog-path must point to an executable"
+      return 1
+
+    input_file_re = re.compile('^.+[.]amber')
+    self.test_cases = []
+
+    if self.args:
+      for filename in self.args:
+        input_path = os.path.join(self.options.test_dir, filename)
+        if not os.path.isfile(input_path):
+          print "Cannot find test file '%s'" % filename
+          return 1
+
+        self.test_cases.append(TestCase(input_path, self.options.parse_only))
+
+    else:
+      for file_dir, _, filename_list in os.walk(self.options.test_dir):
+        for input_filename in filename_list:
+          if input_file_re.match(input_filename):
+            input_path = os.path.join(file_dir, input_filename)
+            if os.path.isfile(input_path):
+              self.test_cases.append(
+                  TestCase(input_path, self.options.parse_only))
+
+    self.failures = []
+
+    self.RunTests()
+    self.SummarizeResults()
+
+    return len(self.failures) != 0
+
+def main():
+  runner = TestRunner()
+  return runner.Run()
+
+
+if __name__ == '__main__':
+  sys.exit(main())
diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt
new file mode 100644
index 0000000..2e24bca
--- /dev/null
+++ b/third_party/CMakeLists.txt
@@ -0,0 +1,182 @@
+# Copyright 2018 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 implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+if(WIN32)
+  option(g`_force_shared_crt
+    "Use shared (DLL) run-time lib even when Google Test is built as static lib."
+    ON)
+endif()
+
+if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
+  # Turn off warnings to make gtest happy
+  set(GTEST_BUILD_FIXES
+      -Wno-covered-switch-default
+      -Wno-deprecated
+      -Wno-disabled-macro-expansion
+      -Wno-exit-time-constructors
+      -Wno-exit-time-destructors
+      -Wno-global-constructors
+      -Wno-missing-field-initializers
+      -Wno-format-nonliteral
+      -Wno-missing-noreturn
+      -Wno-missing-prototypes
+      -Wno-missing-variable-declarations
+      -Wno-shift-sign-overflow
+      -Wno-sign-conversion
+      -Wno-undef
+      -Wno-unused-member-function
+      -Wno-used-but-marked-unused
+      -Wno-weak-vtables
+      -Wno-zero-as-null-pointer-constant)
+
+  set(GLSLANG_BUILD_FIXES
+      -Wno-conversion
+      -Wno-covered-switch-default
+      -Wno-date-time
+      -Wno-deprecated
+      -Wno-disabled-macro-expansion
+      -Wno-double-promotion
+      -Wno-error
+      -Wno-exit-time-destructors
+      -Wno-extra-semi
+      -Wno-float-equal
+      -Wno-format-nonliteral
+      -Wno-format-pedantic
+      -Wno-global-constructors
+      -Wno-gnu-redeclared-enum
+      -Wno-implicit-fallthrough
+      -Wno-inconsistent-missing-destructor-override
+      -Wno-missing-field-initializers
+      -Wno-missing-noreturn
+      -Wno-missing-prototypes
+      -Wno-missing-variable-declarations
+      -Wno-newline-eof
+      -Wno-old-style-cast
+      -Wno-reserved-id-macro
+      -Wno-shadow
+      -Wno-shadow-field
+      -Wno-shadow-field-in-constructor
+      -Wno-shift-sign-overflow
+      -Wno-sign-conversion
+      -Wno-signed-enum-bitfield
+      -Wno-undef
+      -Wno-undefined-func-template
+      -Wno-undefined-reinterpret-cast
+      -Wno-unreachable-code
+      -Wno-unreachable-code-break
+      -Wno-unreachable-code-return
+      -Wno-unused-macros
+      -Wno-unused-parameter
+      -Wno-unused-variable
+      -Wno-used-but-marked-unused
+      -Wno-weak-vtables
+      -Wno-zero-as-null-pointer-constant)
+
+  set(SPIRV_TOOLS_BUILD_FIXES
+      -Wno-conditional-uninitialized
+      -Wno-covered-switch-default
+      -Wno-deprecated
+      -Wno-documentation
+      -Wno-documentation-pedantic
+      -Wno-double-promotion
+      -Wno-extra-semi
+      -Wno-float-equal
+      -Wno-format-nonliteral
+      -Wno-implicit-fallthrough
+      -Wno-missing-prototypes
+      -Wno-old-style-cast
+      -Wno-range-loop-analysis
+      -Wno-shift-sign-overflow
+      -Wno-unreachable-code-break
+      -Wno-unreachable-code-return
+      -Wno-unused-member-function
+      -Wno-weak-vtables
+      -Wno-zero-as-null-pointer-constant)
+
+  set(SHADERC_BUILD_FIXES
+      -Wno-comma
+      -Wno-conversion
+      -Wno-covered-switch
+      -Wno-covered-switch-default
+      -Wno-deprecated
+      -Wno-double-promotion
+      -Wno-extra-semi
+      -Wno-float-equal
+      -Wno-inconsistent-missing-destructor-override
+      -Wno-missing-field-initializers
+      -Wno-missing-prototypes
+      -Wno-newline-eof
+      -Wno-old-style-cast
+      -Wno-reserved-id-macro
+      -Wno-shadow-uncaptured-local
+      -Wno-shadow-field-in-constructor
+      -Wno-sign-conversion
+      -Wno-signed-enum-bitfield
+      -Wno-undef
+      -Wno-unreachable-code-return
+      -Wno-weak-vtables
+      -Wno-zero-as-null-pointer-constant
+  )
+endif()
+
+if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
+  set(GTEST_BUILD_FIXES "")
+
+  set(GLSLANG_BUILD_FIXES
+      -Wno-error
+      -Wno-overflow
+      -Wno-missing-field-initializers
+      -Wno-pedantic
+      -Wno-unused-parameter
+      -Wno-unused-variable)
+
+  set(SPIRV_TOOLS_BUILD_FIXES "")
+
+  set(SHADERC_BUILD_FIXES
+      -Wno-missing-field-initializers
+      -Wno-pedantic)
+endif()
+
+set(CXX_BACK ${CMAKE_CXX_FLAGS})
+SET(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} "${GTEST_BUILD_FIXES}")
+STRING(REGEX REPLACE ";" " " CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
+add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/googletest EXCLUDE_FROM_ALL)
+set(CMAKE_CXX_FLAGS ${CXX_BACK})
+
+set(CXX_BACK ${CMAKE_CXX_FLAGS})
+SET(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} "${SPIRV_TOOLS_BUILD_FIXES}")
+STRING(REGEX REPLACE ";" " " CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
+set(SPIRV-Headers_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/spirv-headers)
+set(SPIRV_SKIP_TESTS ON)
+set(SPIRV_SKIP_EXECUTABLES ON)
+add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/spirv-tools)
+set(CMAKE_CXX_FLAGS ${CXX_BACK})
+
+set(CXX_BACK ${CMAKE_CXX_FLAGS})
+SET(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} "${GLSLANG_BUILD_FIXES}")
+STRING(REGEX REPLACE ";" " " CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
+set(ENABLE_HLSL ON)
+set(BUILD_TESTING OFF)
+set(ENABLE_GLSLANG_BINARIES OFF)
+set(ENABLE_SPVREMAPPER OFF)
+add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/glslang)
+set(CMAKE_CXX_FLAGS ${CXX_BACK})
+
+set(CXX_BACK ${CMAKE_CXX_FLAGS})
+SET(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} "${SHADERC_BUILD_FIXES}")
+STRING(REGEX REPLACE ";" " " CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
+set(SHADERC_THIRD_PARTY_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR} CACHE STRING "")
+set(SHADERC_SKIP_TESTS ON)
+add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/shaderc)
+set(CMAKE_CXX_FLAGS ${CXX_BACK})
diff --git a/tools/format b/tools/format
new file mode 100755
index 0000000..867f99d
--- /dev/null
+++ b/tools/format
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+find src -name "*.h"  -exec clang-format -i {} \;
+find src -name "*.cc"  -exec clang-format -i {} \;
+find samples -name "*.h"  -exec clang-format -i {} \;
+find samples -name "*.cc"  -exec clang-format -i {} \;
+find include -name "*.h"  -exec clang-format -i {} \;
+
diff --git a/tools/git-sync-deps b/tools/git-sync-deps
new file mode 100755
index 0000000..74a0952
--- /dev/null
+++ b/tools/git-sync-deps
@@ -0,0 +1,275 @@
+#!/usr/bin/env python
+# Copyright 2014 Google Inc.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#    * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+#    * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""Parse a DEPS file and git checkout all of the dependencies.
+
+Args:
+  An optional list of deps_os values.
+
+Environment Variables:
+  GIT_EXECUTABLE: path to "git" binary; if unset, will look for one of
+  ['git', 'git.exe', 'git.bat'] in your default path.
+
+  GIT_SYNC_DEPS_PATH: file to get the dependency list from; if unset,
+  will use the file ../DEPS relative to this script's directory.
+
+  GIT_SYNC_DEPS_QUIET: if set to non-empty string, suppress messages.
+
+Git Config:
+  To disable syncing of a single repository:
+      cd path/to/repository
+      git config sync-deps.disable true
+
+  To re-enable sync:
+      cd path/to/repository
+      git config --unset sync-deps.disable
+"""
+
+
+import os
+import subprocess
+import sys
+import threading
+
+
+def git_executable():
+  """Find the git executable.
+
+  Returns:
+      A string suitable for passing to subprocess functions, or None.
+  """
+  envgit = os.environ.get('GIT_EXECUTABLE')
+  searchlist = ['git', 'git.exe', 'git.bat']
+  if envgit:
+    searchlist.insert(0, envgit)
+  with open(os.devnull, 'w') as devnull:
+    for git in searchlist:
+      try:
+        subprocess.call([git, '--version'], stdout=devnull)
+      except (OSError,):
+        continue
+      return git
+  return None
+
+
+DEFAULT_DEPS_PATH = os.path.normpath(
+  os.path.join(os.path.dirname(__file__), os.pardir, 'DEPS'))
+
+
+def usage(deps_file_path = None):
+  sys.stderr.write(
+    'Usage: run to grab dependencies, with optional platform support:\n')
+  sys.stderr.write('  %s %s' % (sys.executable, __file__))
+  if deps_file_path:
+    parsed_deps = parse_file_to_dict(deps_file_path)
+    if 'deps_os' in parsed_deps:
+      for deps_os in parsed_deps['deps_os']:
+        sys.stderr.write(' [%s]' % deps_os)
+  sys.stderr.write('\n\n')
+  sys.stderr.write(__doc__)
+
+
+def git_repository_sync_is_disabled(git, directory):
+  try:
+    disable = subprocess.check_output(
+      [git, 'config', 'sync-deps.disable'], cwd=directory)
+    return disable.lower().strip() in ['true', '1', 'yes', 'on']
+  except subprocess.CalledProcessError:
+    return False
+
+
+def is_git_toplevel(git, directory):
+  """Return true iff the directory is the top level of a Git repository.
+
+  Args:
+    git (string) the git executable
+
+    directory (string) the path into which the repository
+              is expected to be checked out.
+  """
+  try:
+    toplevel = subprocess.check_output(
+      [git, 'rev-parse', '--show-toplevel'], cwd=directory).strip()
+    return os.path.realpath(directory) == os.path.realpath(toplevel)
+  except subprocess.CalledProcessError:
+    return False
+
+
+def status(directory, checkoutable):
+  def truncate(s, length):
+    return s if len(s) <= length else s[:(length - 3)] + '...'
+  dlen = 36
+  directory = truncate(directory, dlen)
+  checkoutable = truncate(checkoutable, 40)
+  sys.stdout.write('%-*s @ %s\n' % (dlen, directory, checkoutable))
+
+
+def git_checkout_to_directory(git, repo, checkoutable, directory, verbose):
+  """Checkout (and clone if needed) a Git repository.
+
+  Args:
+    git (string) the git executable
+
+    repo (string) the location of the repository, suitable
+         for passing to `git clone`.
+
+    checkoutable (string) a tag, branch, or commit, suitable for
+                 passing to `git checkout`
+
+    directory (string) the path into which the repository
+              should be checked out.
+
+    verbose (boolean)
+
+  Raises an exception if any calls to git fail.
+  """
+  if not os.path.isdir(directory):
+    subprocess.check_call(
+      [git, 'clone', '--quiet', repo, directory])
+
+  if not is_git_toplevel(git, directory):
+    # if the directory exists, but isn't a git repo, you will modify
+    # the parent repostory, which isn't what you want.
+    sys.stdout.write('%s\n  IS NOT TOP-LEVEL GIT DIRECTORY.\n' % directory)
+    return
+
+  # Check to see if this repo is disabled.  Quick return.
+  if git_repository_sync_is_disabled(git, directory):
+    sys.stdout.write('%s\n  SYNC IS DISABLED.\n' % directory)
+    return
+
+  with open(os.devnull, 'w') as devnull:
+    # If this fails, we will fetch before trying again.  Don't spam user
+    # with error infomation.
+    if 0 == subprocess.call([git, 'checkout', '--quiet', checkoutable],
+                            cwd=directory, stderr=devnull):
+      # if this succeeds, skip slow `git fetch`.
+      if verbose:
+        status(directory, checkoutable)  # Success.
+      return
+
+  # If the repo has changed, always force use of the correct repo.
+  # If origin already points to repo, this is a quick no-op.
+  subprocess.check_call(
+      [git, 'remote', 'set-url', 'origin', repo], cwd=directory)
+
+  subprocess.check_call([git, 'fetch', '--quiet'], cwd=directory)
+
+  subprocess.check_call([git, 'checkout', '--quiet', checkoutable], cwd=directory)
+
+  if verbose:
+    status(directory, checkoutable)  # Success.
+
+
+def parse_file_to_dict(path):
+  dictionary = {}
+  execfile(path, dictionary)
+  return dictionary
+
+
+def git_sync_deps(deps_file_path, command_line_os_requests, verbose):
+  """Grab dependencies, with optional platform support.
+
+  Args:
+    deps_file_path (string) Path to the DEPS file.
+
+    command_line_os_requests (list of strings) Can be empty list.
+        List of strings that should each be a key in the deps_os
+        dictionary in the DEPS file.
+
+  Raises git Exceptions.
+  """
+  git = git_executable()
+  assert git
+
+  deps_file_directory = os.path.dirname(deps_file_path)
+  deps_file = parse_file_to_dict(deps_file_path)
+  dependencies = deps_file['deps'].copy()
+  os_specific_dependencies = deps_file.get('deps_os', dict())
+  if 'all' in command_line_os_requests:
+    for value in os_specific_dependencies.itervalues():
+      dependencies.update(value)
+  else:
+    for os_name in command_line_os_requests:
+      # Add OS-specific dependencies
+      if os_name in os_specific_dependencies:
+        dependencies.update(os_specific_dependencies[os_name])
+  for directory in dependencies:
+    for other_dir in dependencies:
+      if directory.startswith(other_dir + '/'):
+        raise Exception('%r is parent of %r' % (other_dir, directory))
+  list_of_arg_lists = []
+  for directory in sorted(dependencies):
+    if '@' in dependencies[directory]:
+      repo, checkoutable = dependencies[directory].split('@', 1)
+    else:
+      raise Exception("please specify commit or tag")
+
+    relative_directory = os.path.join(deps_file_directory, directory)
+
+    list_of_arg_lists.append(
+      (git, repo, checkoutable, relative_directory, verbose))
+
+  multithread(git_checkout_to_directory, list_of_arg_lists)
+
+  for directory in deps_file.get('recursedeps', []):
+    recursive_path = os.path.join(deps_file_directory, directory, 'DEPS')
+    git_sync_deps(recursive_path, command_line_os_requests, verbose)
+
+
+def multithread(function, list_of_arg_lists):
+  # for args in list_of_arg_lists:
+  #   function(*args)
+  # return
+  threads = []
+  for args in list_of_arg_lists:
+    thread = threading.Thread(None, function, None, args)
+    thread.start()
+    threads.append(thread)
+  for thread in threads:
+    thread.join()
+
+
+def main(argv):
+  deps_file_path = os.environ.get('GIT_SYNC_DEPS_PATH', DEFAULT_DEPS_PATH)
+  verbose = not bool(os.environ.get('GIT_SYNC_DEPS_QUIET', False))
+
+  if '--help' in argv or '-h' in argv:
+    usage(deps_file_path)
+    return 1
+
+  git_sync_deps(deps_file_path, argv, verbose)
+  # subprocess.check_call(
+  #     [sys.executable,
+  #      os.path.join(os.path.dirname(deps_file_path), 'bin', 'fetch-gn')])
+  return 0
+
+
+if __name__ == '__main__':
+  exit(main(sys.argv[1:]))
diff --git a/tools/update_build_version.py b/tools/update_build_version.py
new file mode 100755
index 0000000..b353607
--- /dev/null
+++ b/tools/update_build_version.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python
+
+# Copyright 2018 The Amber Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Generates build-versions.h in the src/ directory.
+#
+# Args:  <output_dir> <amber-dir> <spirv-tools-dir> <spirv-headers> <glslang-dir> <shaderc-dir>
+
+from __future__ import print_function
+
+import datetime
+import os.path
+import re
+import subprocess
+import sys
+import time
+
+OUTFILE = 'src/build-versions.h'
+
+
+def command_output(cmd, directory):
+  p = subprocess.Popen(cmd,
+                       cwd=directory,
+                       stdout=subprocess.PIPE,
+                       stderr=subprocess.PIPE)
+  (stdout, _) = p.communicate()
+  if p.returncode != 0:
+    raise RuntimeError('Failed to run {} in {}'.format(cmd, directory))
+  return stdout
+
+
+def describe(directory):
+  return command_output(
+      ['git', 'log', '-1', '--format=%h'], directory).rstrip().decode()
+
+
+def get_version_string(project, directory):
+  return "#define {}_VERSION \"{}\"".format(project.upper(), describe(directory))
+
+
+def main():
+  if len(sys.argv) != 7:
+    print('usage: {} <outdir> <amber-dir> <spirv-tools-dir> <spirv-headers> <glslang-dir> <shaderc-dir>'.format(
+      sys.argv[0]))
+    sys.exit(1)
+
+  outdir = sys.argv[1]
+
+  projects = ['amber', 'spirv_tools', 'spirv_headers', 'glslang', 'shaderc']
+  new_content = ''.join([
+    '{}\n'.format(get_version_string(p, d))
+    for (p, d) in zip(projects, sys.argv[2:])
+  ])
+
+  file = outdir + "/" + OUTFILE
+  if os.path.isfile(file):
+    with open(file, 'r') as f:
+      if new_content == f.read():
+        return
+  with open(file, 'w') as f:
+    f.write(new_content)
+
+
+if __name__ == '__main__':
+  main()