benchmarks: Add a basic set of benchmarks

Add google benchmark to the DEPs.

Implement a basic set of benchmarks for each of the writers and the WGSL parser.

Add build rules for CMake. GN build rules TODO.

Add a simple go tool (ported from Marl) to diff two benchmarks. Less
noisy than the one provided by google benchmark.

Bug: tint:1378
Change-Id: I73cf92c5d9fd2d3bfac8f264864fd774afbd5d01
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/76840
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ryan Harrison <rharrison@chromium.org>
Commit-Queue: Ben Clayton <bclayton@chromium.org>
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 36a743f..6049b1b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -81,6 +81,7 @@
 option_if_not_defined(TINT_BUILD_SPIRV_TOOLS_FUZZER "Build SPIRV-Tools fuzzer" OFF)
 option_if_not_defined(TINT_BUILD_AST_FUZZER "Build AST fuzzer" OFF)
 option_if_not_defined(TINT_BUILD_REGEX_FUZZER "Build regex fuzzer" OFF)
+option_if_not_defined(TINT_BUILD_BENCHMARKS "Build benchmarks" OFF)
 option_if_not_defined(TINT_BUILD_TESTS "Build tests" ${TINT_BUILD_TESTS_DEFAULT})
 option_if_not_defined(TINT_BUILD_AS_OTHER_OS "Override OS detection to force building of *_other.cc files" OFF)
 option_if_not_defined(TINT_BUILD_REMOTE_COMPILE "Build the remote-compile tool for validating shaders on a remote machine" OFF)
@@ -111,6 +112,7 @@
 message(STATUS "Tint build SPIRV-Tools fuzzer: ${TINT_BUILD_SPIRV_TOOLS_FUZZER}")
 message(STATUS "Tint build AST fuzzer: ${TINT_BUILD_AST_FUZZER}")
 message(STATUS "Tint build regex fuzzer: ${TINT_BUILD_REGEX_FUZZER}")
+message(STATUS "Tint build benchmarks: ${TINT_BUILD_BENCHMARKS}")
 message(STATUS "Tint build tests: ${TINT_BUILD_TESTS}")
 message(STATUS "Tint build with ASAN: ${TINT_ENABLE_ASAN}")
 message(STATUS "Tint build with MSAN: ${TINT_ENABLE_MSAN}")
@@ -254,7 +256,7 @@
   endif(DOXYGEN_FOUND)
 endif()
 
-function(tint_default_compile_options TARGET)
+function(tint_core_compile_options TARGET)
   target_include_directories(${TARGET} PUBLIC "${TINT_ROOT_SOURCE_DIR}")
   target_include_directories(${TARGET} PUBLIC "${TINT_ROOT_SOURCE_DIR}/include")
 
@@ -263,20 +265,48 @@
         "${TINT_ROOT_SOURCE_DIR}/third_party/spirv-headers/include")
   endif()
 
-  target_compile_definitions(${TARGET} PUBLIC
-      -DTINT_BUILD_SPV_READER=$<BOOL:${TINT_BUILD_SPV_READER}>)
-  target_compile_definitions(${TARGET} PUBLIC
-      -DTINT_BUILD_WGSL_READER=$<BOOL:${TINT_BUILD_WGSL_READER}>)
-  target_compile_definitions(${TARGET} PUBLIC
-      -DTINT_BUILD_GLSL_WRITER=$<BOOL:${TINT_BUILD_GLSL_WRITER}>)
-  target_compile_definitions(${TARGET} PUBLIC
-      -DTINT_BUILD_HLSL_WRITER=$<BOOL:${TINT_BUILD_HLSL_WRITER}>)
-  target_compile_definitions(${TARGET} PUBLIC
-      -DTINT_BUILD_MSL_WRITER=$<BOOL:${TINT_BUILD_MSL_WRITER}>)
-  target_compile_definitions(${TARGET} PUBLIC
-      -DTINT_BUILD_SPV_WRITER=$<BOOL:${TINT_BUILD_SPV_WRITER}>)
-  target_compile_definitions(${TARGET} PUBLIC
-      -DTINT_BUILD_WGSL_WRITER=$<BOOL:${TINT_BUILD_WGSL_WRITER}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_SPV_READER=$<BOOL:${TINT_BUILD_SPV_READER}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_WGSL_READER=$<BOOL:${TINT_BUILD_WGSL_READER}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_GLSL_WRITER=$<BOOL:${TINT_BUILD_GLSL_WRITER}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_HLSL_WRITER=$<BOOL:${TINT_BUILD_HLSL_WRITER}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_MSL_WRITER=$<BOOL:${TINT_BUILD_MSL_WRITER}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_SPV_WRITER=$<BOOL:${TINT_BUILD_SPV_WRITER}>)
+  target_compile_definitions(${TARGET} PUBLIC -DTINT_BUILD_WGSL_WRITER=$<BOOL:${TINT_BUILD_WGSL_WRITER}>)
+
+  if (COMPILER_IS_LIKE_GNU)
+    target_compile_options(${TARGET} PRIVATE
+      -std=c++17
+      -fno-exceptions
+      -fno-rtti
+    )
+
+    if (${TINT_ENABLE_MSAN})
+      target_compile_options(${TARGET} PRIVATE -fsanitize=memory)
+      target_link_options(${TARGET} PRIVATE -fsanitize=memory)
+    elseif (${TINT_ENABLE_ASAN})
+      target_compile_options(${TARGET} PRIVATE -fsanitize=address)
+      target_link_options(${TARGET} PRIVATE -fsanitize=address)
+    elseif (${TINT_ENABLE_UBSAN})
+      target_compile_options(${TARGET} PRIVATE -fsanitize=undefined)
+      target_link_options(${TARGET} PRIVATE -fsanitize=undefined)
+    endif()
+  endif(COMPILER_IS_LIKE_GNU)
+
+  if (TINT_EMIT_COVERAGE)
+    if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
+        target_compile_options(${TARGET} PRIVATE "--coverage")
+        target_link_options(${TARGET} PRIVATE "gcov")
+    elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
+        target_compile_options(${TARGET} PRIVATE "-fprofile-instr-generate" "-fcoverage-mapping")
+        target_link_options(${TARGET} PRIVATE "-fprofile-instr-generate" "-fcoverage-mapping")
+    else()
+        message(FATAL_ERROR "Coverage generation not supported for the ${CMAKE_CXX_COMPILER_ID} toolchain")
+    endif()
+  endif(TINT_EMIT_COVERAGE)
+endfunction()
+
+function(tint_default_compile_options TARGET)
+  tint_core_compile_options(${TARGET})
 
   set(COMMON_GNU_OPTIONS
     -Wall
@@ -299,11 +329,8 @@
     -Weverything
   )
 
-  if (${COMPILER_IS_LIKE_GNU})
+  if (COMPILER_IS_LIKE_GNU)
     target_compile_options(${TARGET} PRIVATE
-      -std=c++17
-      -fno-exceptions
-      -fno-rtti
       -pedantic-errors
       ${COMMON_GNU_OPTIONS}
     )
@@ -314,30 +341,7 @@
         ${COMMON_CLANG_OPTIONS}
       )
     endif()
-
-    if (${TINT_ENABLE_MSAN})
-      target_compile_options(${TARGET} PRIVATE -fsanitize=memory)
-      target_link_options(${TARGET} PRIVATE -fsanitize=memory)
-    elseif (${TINT_ENABLE_ASAN})
-      target_compile_options(${TARGET} PRIVATE -fsanitize=address)
-      target_link_options(${TARGET} PRIVATE -fsanitize=address)
-    elseif (${TINT_ENABLE_UBSAN})
-      target_compile_options(${TARGET} PRIVATE -fsanitize=undefined)
-      target_link_options(${TARGET} PRIVATE -fsanitize=undefined)
-    endif()
-  endif()
-
-  if (${TINT_EMIT_COVERAGE})
-    if(CMAKE_CXX_COMPILER_ID MATCHES "GNU")
-        target_compile_options(${TARGET} PRIVATE "--coverage")
-        target_link_options(${TARGET} PRIVATE "gcov")
-    elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
-        target_compile_options(${TARGET} PRIVATE "-fprofile-instr-generate" "-fcoverage-mapping")
-        target_link_options(${TARGET} PRIVATE "-fprofile-instr-generate" "-fcoverage-mapping")
-    else()
-        message(FATAL_ERROR "Coverage generation not supported for the ${CMAKE_CXX_COMPILER_ID} toolchain")
-    endif()
-  endif()
+  endif(COMPILER_IS_LIKE_GNU)
 
   if (MSVC)
     # Specify /EHs for exception handling.
@@ -379,7 +383,6 @@
       )
     endif()
   endif()
-
 endfunction()
 
 add_subdirectory(third_party)
diff --git a/DEPS b/DEPS
index 6bb6635..fbd2af5 100644
--- a/DEPS
+++ b/DEPS
@@ -10,6 +10,7 @@
   'chromium_git':  'https://chromium.googlesource.com',
   'github': '/external/github.com',
 
+  'benchmark_revision': 'e991355c02b93fe17713efe04cbc2e278e00fdbd',
   'build_revision': '555c8b467c21e2c4b22d00e87e3faa0431df9ac2',
   'buildtools_revision': 'f78b4b9f33bd8ef9944d5ce643daff1c31880189',
   'catapult_revision': 'fa35beefb3429605035f98211ddb8750dee6a13d',
@@ -101,6 +102,9 @@
   'third_party/catapult': Var('chromium_git') + '/catapult.git@' +
       Var('catapult_revision'),
 
+  'third_party/benchmark': Var('chromium_git') + Var('github') +
+      '/google/benchmark.git@' + Var('benchmark_revision'),
+
   'third_party/googletest': Var('chromium_git') + Var('github') +
       '/google/googletest.git@' + Var('googletest_revision'),
 
diff --git a/fuzzers/tint_black_box_fuzz_target.cc b/fuzzers/tint_black_box_fuzz_target.cc
index cefaa76..f2f2f68 100644
--- a/fuzzers/tint_black_box_fuzz_target.cc
+++ b/fuzzers/tint_black_box_fuzz_target.cc
@@ -56,13 +56,7 @@
   }
 
   fseek(file, 0, SEEK_END);
-  auto tell_file_size = ftell(file);
-  if (tell_file_size <= 0) {
-    std::cerr << "Input file of incorrect size: " << input_file << std::endl;
-    fclose(file);
-    return {};
-  }
-  const auto file_size = static_cast<size_t>(tell_file_size);
+  const auto file_size = static_cast<size_t>(ftell(file));
   if (0 != (file_size % sizeof(T))) {
     std::cerr << "File " << input_file
               << " does not contain an integral number of objects: "
diff --git a/kokoro/linux/docker.sh b/kokoro/linux/docker.sh
index cef68d1..9818060 100755
--- a/kokoro/linux/docker.sh
+++ b/kokoro/linux/docker.sh
@@ -84,7 +84,8 @@
 
     COMMON_CMAKE_FLAGS=""
     COMMON_CMAKE_FLAGS+=" -DCMAKE_BUILD_TYPE=${BUILD_TYPE}"
-    COMMON_CMAKE_FLAGS+=" -DTINT_DOCS_WARN_AS_ERROR=ON"
+    COMMON_CMAKE_FLAGS+=" -DTINT_DOCS_WARN_AS_ERROR=1"
+    COMMON_CMAKE_FLAGS+=" -DTINT_BUILD_BENCHMARKS=1"
 
     if [ "$BUILD_TOOLCHAIN" == "clang" ]; then
         using clang-10.0.0
@@ -155,9 +156,9 @@
 
     status "Checking disabling all readers and writers also builds"
     show_cmds
-        cmake ${SRC_DIR} ${CMAKE_FLAGS} ${COMMON_CMAKE_FLAGS} -DTINT_BUILD_SPV_READER=OFF -DTINT_BUILD_SPV_WRITER=OFF -DTINT_BUILD_WGSL_READER=OFF -DTINT_BUILD_WGSL_WRITER=OFF -DTINT_BUILD_MSL_WRITER=OFF -DTINT_BUILD_HLSL_WRITER=OFF
+        cmake ${SRC_DIR} ${CMAKE_FLAGS} ${COMMON_CMAKE_FLAGS} -DTINT_BUILD_SPV_READER=OFF -DTINT_BUILD_SPV_WRITER=OFF -DTINT_BUILD_WGSL_READER=OFF -DTINT_BUILD_WGSL_WRITER=OFF -DTINT_BUILD_MSL_WRITER=OFF -DTINT_BUILD_HLSL_WRITER=OFF -DTINT_BUILD_BENCHMARKS=OFF
         cmake --build . -- --jobs=$(nproc)
-        cmake ${SRC_DIR} ${CMAKE_FLAGS} ${COMMON_CMAKE_FLAGS} -DTINT_BUILD_SPV_READER=ON -DTINT_BUILD_SPV_WRITER=ON -DTINT_BUILD_WGSL_READER=ON -DTINT_BUILD_WGSL_WRITER=ON -DTINT_BUILD_MSL_WRITER=ON -DTINT_BUILD_HLSL_WRITER=ON
+        cmake ${SRC_DIR} ${CMAKE_FLAGS} ${COMMON_CMAKE_FLAGS} -DTINT_BUILD_SPV_READER=ON -DTINT_BUILD_SPV_WRITER=ON -DTINT_BUILD_WGSL_READER=ON -DTINT_BUILD_WGSL_WRITER=ON -DTINT_BUILD_MSL_WRITER=ON -DTINT_BUILD_HLSL_WRITER=ON -DTINT_BUILD_BENCHMARKS=ON
     hide_cmds
 else
     status "Unsupported build system: $BUILD_SYSTEM"
diff --git a/kokoro/windows/build.bat b/kokoro/windows/build.bat
index a7d28ce..e22d5d9 100644
--- a/kokoro/windows/build.bat
+++ b/kokoro/windows/build.bat
@@ -116,7 +116,7 @@
 @echo on

 mkdir %BUILD_DIR%

 cd /d %BUILD_DIR%

-set COMMON_CMAKE_FLAGS=-DTINT_BUILD_DOCS=O -DCMAKE_BUILD_TYPE=%BUILD_TYPE%

+set COMMON_CMAKE_FLAGS=-DTINT_BUILD_DOCS=O -DTINT_BUILD_BENCHMARKS=1 -DCMAKE_BUILD_TYPE=%BUILD_TYPE%

 @echo off

 

 call :status "Building tint"

diff --git a/samples/main.cc b/samples/main.cc
index 4348636..87f0b54 100644
--- a/samples/main.cc
+++ b/samples/main.cc
@@ -437,13 +437,7 @@
   }
 
   fseek(file, 0, SEEK_END);
-  uint64_t tell_file_size = static_cast<uint64_t>(ftell(file));
-  if (tell_file_size <= 0) {
-    std::cerr << "Input file of incorrect size: " << input_file << std::endl;
-    fclose(file);
-    return {};
-  }
-  const auto file_size = static_cast<size_t>(tell_file_size);
+  const auto file_size = static_cast<size_t>(ftell(file));
   if (0 != (file_size % sizeof(T))) {
     std::cerr << "File " << input_file
               << " does not contain an integral number of objects: "
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index be42ae5..d760dcb 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -598,7 +598,10 @@
   endif()
 endif()
 
-if(${TINT_BUILD_TESTS})
+################################################################################
+# Tests
+################################################################################
+if(TINT_BUILD_TESTS)
   set(TINT_TEST_SRCS
     ast/alias_test.cc
     ast/array_test.cc
@@ -1114,6 +1117,7 @@
   endif()
 
   add_executable(tint_unittests ${TINT_TEST_SRCS})
+  set_target_properties(${target} PROPERTIES FOLDER "Tests")
 
   if(NOT MSVC)
     target_compile_options(tint_unittests PRIVATE
@@ -1133,4 +1137,41 @@
   endif()
 
   add_test(NAME tint_unittests COMMAND tint_unittests)
-endif()
+endif(TINT_BUILD_TESTS)
+
+################################################################################
+# Benchmarks
+################################################################################
+if(TINT_BUILD_BENCHMARKS)
+  if(NOT TINT_BUILD_WGSL_READER)
+    message(FATAL_ERROR "TINT_BUILD_BENCHMARKS requires TINT_BUILD_WGSL_READER")
+  endif()
+
+  set(TINT_BENCHMARK_SRC
+    "benchmark/benchmark.cc"
+    "reader/wgsl/parser_bench.cc"
+  )
+
+  if (${TINT_BUILD_GLSL_WRITER})
+    list(APPEND TINT_BENCHMARK_SRC writer/glsl/generator_bench.cc)
+  endif()
+  if (${TINT_BUILD_HLSL_WRITER})
+    list(APPEND TINT_BENCHMARK_SRC writer/hlsl/generator_bench.cc)
+  endif()
+  if (${TINT_BUILD_MSL_WRITER})
+    list(APPEND TINT_BENCHMARK_SRC writer/msl/generator_bench.cc)
+  endif()
+  if (${TINT_BUILD_SPV_WRITER})
+    list(APPEND TINT_BENCHMARK_SRC writer/spirv/generator_bench.cc)
+  endif()
+  if (${TINT_BUILD_WGSL_WRITER})
+    list(APPEND TINT_BENCHMARK_SRC writer/wgsl/generator_bench.cc)
+  endif()
+
+  add_executable(tint-benchmark ${TINT_BENCHMARK_SRC})
+  set_target_properties(${target} PROPERTIES FOLDER "Benchmarks")
+
+  tint_core_compile_options(tint-benchmark)
+
+  target_link_libraries(tint-benchmark PRIVATE benchmark::benchmark libtint)
+endif(TINT_BUILD_BENCHMARKS)
diff --git a/src/benchmark/benchmark.cc b/src/benchmark/benchmark.cc
new file mode 100644
index 0000000..813af84
--- /dev/null
+++ b/src/benchmark/benchmark.cc
@@ -0,0 +1,123 @@
+// Copyright 2022 The Tint 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/benchmark/benchmark.h"
+
+#include <filesystem>
+#include <sstream>
+#include <utility>
+#include <vector>
+
+namespace tint::benchmark {
+namespace {
+
+std::filesystem::path kInputFileDir;
+
+/// Copies the content from the file named `input_file` to `buffer`,
+/// assuming each element in the file is of type `T`.  If any error occurs,
+/// writes error messages to the standard error stream and returns false.
+/// Assumes the size of a `T` object is divisible by its required alignment.
+/// @returns true if we successfully read the file.
+template <typename T>
+std::variant<std::vector<T>, Error> ReadFile(const std::string& input_file) {
+  FILE* file = nullptr;
+#if defined(_MSC_VER)
+  fopen_s(&file, input_file.c_str(), "rb");
+#else
+  file = fopen(input_file.c_str(), "rb");
+#endif
+  if (!file) {
+    return Error{"Failed to open " + input_file};
+  }
+
+  fseek(file, 0, SEEK_END);
+  const auto file_size = static_cast<size_t>(ftell(file));
+  if (0 != (file_size % sizeof(T))) {
+    std::stringstream err;
+    err << "File " << input_file
+        << " does not contain an integral number of objects: " << file_size
+        << " bytes in the file, require " << sizeof(T) << " bytes per object";
+    fclose(file);
+    return Error{err.str()};
+  }
+  fseek(file, 0, SEEK_SET);
+
+  std::vector<T> buffer;
+  buffer.resize(file_size / sizeof(T));
+
+  size_t bytes_read = fread(buffer.data(), 1, file_size, file);
+  fclose(file);
+  if (bytes_read != file_size) {
+    return Error{"Failed to read " + input_file};
+  }
+
+  return buffer;
+}
+
+bool FindBenchmarkInputDir() {
+  // Attempt to find the benchmark input files by searching up from the current
+  // working directory.
+  auto path = std::filesystem::current_path();
+  while (std::filesystem::is_directory(path)) {
+    auto test = path / "test" / "benchmark";
+    if (std::filesystem::is_directory(test)) {
+      kInputFileDir = test;
+      return true;
+    }
+    auto parent = path.parent_path();
+    if (path == parent) {
+      break;
+    }
+    path = parent;
+  }
+  return false;
+}
+
+}  // namespace
+
+std::variant<tint::Source::File, Error> LoadInputFile(std::string name) {
+  auto path = (kInputFileDir / name).string();
+  auto data = ReadFile<uint8_t>(path);
+  if (auto* buf = std::get_if<std::vector<uint8_t>>(&data)) {
+    return tint::Source::File(path, std::string(buf->begin(), buf->end()));
+  }
+  return std::get<Error>(data);
+}
+
+std::variant<ProgramAndFile, Error> LoadProgram(std::string name) {
+  auto res = benchmark::LoadInputFile(name);
+  if (auto err = std::get_if<benchmark::Error>(&res)) {
+    return *err;
+  }
+  auto& file = std::get<Source::File>(res);
+  auto program = reader::wgsl::Parse(&file);
+  if (program.Diagnostics().contains_errors()) {
+    return Error{program.Diagnostics().str()};
+  }
+  return ProgramAndFile{std::move(program), std::move(file)};
+}
+
+}  // namespace tint::benchmark
+
+int main(int argc, char** argv) {
+  ::benchmark::Initialize(&argc, argv);
+  if (::benchmark::ReportUnrecognizedArguments(argc, argv)) {
+    return 1;
+  }
+  if (!tint::benchmark::FindBenchmarkInputDir()) {
+    std::cerr << "failed to locate benchmark input files" << std::endl;
+    return 1;
+  }
+  ::benchmark::RunSpecifiedBenchmarks();
+}
diff --git a/src/benchmark/benchmark.h b/src/benchmark/benchmark.h
new file mode 100644
index 0000000..40de31d
--- /dev/null
+++ b/src/benchmark/benchmark.h
@@ -0,0 +1,69 @@
+// Copyright 2022 The Tint 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_BENCHMARK_BENCHMARK_H_
+#define SRC_BENCHMARK_BENCHMARK_H_
+
+#include <memory>
+#include <string>
+#include <variant>  // NOLINT: Found C system header after C++ system header.
+
+#include "benchmark/benchmark.h"
+#include "src/utils/concat.h"
+#include "tint/tint.h"
+
+namespace tint::benchmark {
+
+/// Error indicates an operation did not complete successfully.
+struct Error {
+  /// The error message.
+  std::string msg;
+};
+
+/// ProgramAndFile holds a Program and a Source::File.
+struct ProgramAndFile {
+  /// The tint program parsed from file.
+  Program program;
+  /// The source file
+  Source::File file;
+};
+
+/// LoadInputFile attempts to load a benchmark input file with the given file
+/// name.
+/// @param name the file name
+/// @returns either the loaded Source::File or an Error
+std::variant<Source::File, Error> LoadInputFile(std::string name);
+
+/// LoadInputFile attempts to load a benchmark input program with the given file
+/// name.
+/// @param name the file name
+/// @returns either the loaded Program or an Error
+std::variant<ProgramAndFile, Error> LoadProgram(std::string name);
+
+/// Declares a benchmark with the given function and WGSL file name
+#define TINT_BENCHMARK_WGSL_PROGRAM(FUNC, WGSL_NAME) \
+  BENCHMARK_CAPTURE(FUNC, WGSL_NAME, WGSL_NAME);
+
+/// Declares a set of benchmarks for the given function using a list of WGSL
+/// files in `<tint>/test/benchmark`.
+#define TINT_BENCHMARK_WGSL_PROGRAMS(FUNC)                   \
+  TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "empty.wgsl");           \
+  TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "particles.wgsl");       \
+  TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "simple_fragment.wgsl"); \
+  TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "simple_vertex.wgsl");   \
+  TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "simple_compute.wgsl");
+
+}  // namespace tint::benchmark
+
+#endif  // SRC_BENCHMARK_BENCHMARK_H_
diff --git a/src/reader/wgsl/parser_bench.cc b/src/reader/wgsl/parser_bench.cc
new file mode 100644
index 0000000..95fa446
--- /dev/null
+++ b/src/reader/wgsl/parser_bench.cc
@@ -0,0 +1,40 @@
+// Copyright 2022 The Tint 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 <string>
+
+#include "src/benchmark/benchmark.h"
+
+namespace tint::reader::wgsl {
+namespace {
+
+void ParseWGSL(::benchmark::State& state, std::string input_name) {
+  auto res = benchmark::LoadInputFile(input_name);
+  if (auto err = std::get_if<benchmark::Error>(&res)) {
+    state.SkipWithError(err->msg.c_str());
+    return;
+  }
+  auto& file = std::get<Source::File>(res);
+  for (auto _ : state) {
+    auto res = Parse(&file);
+    if (res.Diagnostics().contains_errors()) {
+      state.SkipWithError(res.Diagnostics().str().c_str());
+    }
+  }
+}
+
+TINT_BENCHMARK_WGSL_PROGRAMS(ParseWGSL);
+
+}  // namespace
+}  // namespace tint::reader::wgsl
diff --git a/src/writer/glsl/generator_bench.cc b/src/writer/glsl/generator_bench.cc
new file mode 100644
index 0000000..2ac109f
--- /dev/null
+++ b/src/writer/glsl/generator_bench.cc
@@ -0,0 +1,50 @@
+// Copyright 2022 The Tint 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 <string>
+
+#include "src/ast/module.h"
+#include "src/benchmark/benchmark.h"
+
+namespace tint::writer::glsl {
+namespace {
+
+void GenerateGLSL(::benchmark::State& state, std::string input_name) {
+  auto res = benchmark::LoadProgram(input_name);
+  if (auto err = std::get_if<benchmark::Error>(&res)) {
+    state.SkipWithError(err->msg.c_str());
+    return;
+  }
+  auto& program = std::get<benchmark::ProgramAndFile>(res).program;
+  std::vector<std::string> entry_points;
+  for (auto& fn : program.AST().Functions()) {
+    if (fn->IsEntryPoint()) {
+      entry_points.emplace_back(program.Symbols().NameFor(fn->symbol));
+    }
+  }
+
+  for (auto _ : state) {
+    for (auto& ep : entry_points) {
+      auto res = Generate(&program, {}, ep);
+      if (!res.error.empty()) {
+        state.SkipWithError(res.error.c_str());
+      }
+    }
+  }
+}
+
+TINT_BENCHMARK_WGSL_PROGRAMS(GenerateGLSL);
+
+}  // namespace
+}  // namespace tint::writer::glsl
diff --git a/src/writer/hlsl/generator_bench.cc b/src/writer/hlsl/generator_bench.cc
new file mode 100644
index 0000000..e9168af
--- /dev/null
+++ b/src/writer/hlsl/generator_bench.cc
@@ -0,0 +1,40 @@
+// Copyright 2022 The Tint 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 <string>
+
+#include "src/benchmark/benchmark.h"
+
+namespace tint::writer::hlsl {
+namespace {
+
+void GenerateHLSL(::benchmark::State& state, std::string input_name) {
+  auto res = benchmark::LoadProgram(input_name);
+  if (auto err = std::get_if<benchmark::Error>(&res)) {
+    state.SkipWithError(err->msg.c_str());
+    return;
+  }
+  auto& program = std::get<benchmark::ProgramAndFile>(res).program;
+  for (auto _ : state) {
+    auto res = Generate(&program, {});
+    if (!res.error.empty()) {
+      state.SkipWithError(res.error.c_str());
+    }
+  }
+}
+
+TINT_BENCHMARK_WGSL_PROGRAMS(GenerateHLSL);
+
+}  // namespace
+}  // namespace tint::writer::hlsl
diff --git a/src/writer/msl/generator_bench.cc b/src/writer/msl/generator_bench.cc
new file mode 100644
index 0000000..d2b53df
--- /dev/null
+++ b/src/writer/msl/generator_bench.cc
@@ -0,0 +1,40 @@
+// Copyright 2022 The Tint 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 <string>
+
+#include "src/benchmark/benchmark.h"
+
+namespace tint::writer::msl {
+namespace {
+
+void GenerateMSL(::benchmark::State& state, std::string input_name) {
+  auto res = benchmark::LoadProgram(input_name);
+  if (auto err = std::get_if<benchmark::Error>(&res)) {
+    state.SkipWithError(err->msg.c_str());
+    return;
+  }
+  auto& program = std::get<benchmark::ProgramAndFile>(res).program;
+  for (auto _ : state) {
+    auto res = Generate(&program, {});
+    if (!res.error.empty()) {
+      state.SkipWithError(res.error.c_str());
+    }
+  }
+}
+
+TINT_BENCHMARK_WGSL_PROGRAMS(GenerateMSL);
+
+}  // namespace
+}  // namespace tint::writer::msl
diff --git a/src/writer/spirv/generator_bench.cc b/src/writer/spirv/generator_bench.cc
new file mode 100644
index 0000000..fb937d1
--- /dev/null
+++ b/src/writer/spirv/generator_bench.cc
@@ -0,0 +1,40 @@
+// Copyright 2022 The Tint 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 <string>
+
+#include "src/benchmark/benchmark.h"
+
+namespace tint::writer::spirv {
+namespace {
+
+void GenerateSPIRV(::benchmark::State& state, std::string input_name) {
+  auto res = benchmark::LoadProgram(input_name);
+  if (auto err = std::get_if<benchmark::Error>(&res)) {
+    state.SkipWithError(err->msg.c_str());
+    return;
+  }
+  auto& program = std::get<benchmark::ProgramAndFile>(res).program;
+  for (auto _ : state) {
+    auto res = Generate(&program, {});
+    if (!res.error.empty()) {
+      state.SkipWithError(res.error.c_str());
+    }
+  }
+}
+
+TINT_BENCHMARK_WGSL_PROGRAMS(GenerateSPIRV);
+
+}  // namespace
+}  // namespace tint::writer::spirv
diff --git a/src/writer/wgsl/generator_bench.cc b/src/writer/wgsl/generator_bench.cc
new file mode 100644
index 0000000..524d9cf
--- /dev/null
+++ b/src/writer/wgsl/generator_bench.cc
@@ -0,0 +1,40 @@
+// Copyright 2022 The Tint 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 <string>
+
+#include "src/benchmark/benchmark.h"
+
+namespace tint::writer::wgsl {
+namespace {
+
+void GenerateWGSL(::benchmark::State& state, std::string input_name) {
+  auto res = benchmark::LoadProgram(input_name);
+  if (auto err = std::get_if<benchmark::Error>(&res)) {
+    state.SkipWithError(err->msg.c_str());
+    return;
+  }
+  auto& program = std::get<benchmark::ProgramAndFile>(res).program;
+  for (auto _ : state) {
+    auto res = Generate(&program, {});
+    if (!res.error.empty()) {
+      state.SkipWithError(res.error.c_str());
+    }
+  }
+}
+
+TINT_BENCHMARK_WGSL_PROGRAMS(GenerateWGSL);
+
+}  // namespace
+}  // namespace tint::writer::wgsl
diff --git a/test/benchmark/empty.wgsl b/test/benchmark/empty.wgsl
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/benchmark/empty.wgsl
diff --git a/test/benchmark/empty.wgsl.expected.hlsl b/test/benchmark/empty.wgsl.expected.hlsl
new file mode 100644
index 0000000..da1baa5
--- /dev/null
+++ b/test/benchmark/empty.wgsl.expected.hlsl
@@ -0,0 +1,4 @@
+[numthreads(1, 1, 1)]
+void unused_entry_point() {
+  return;
+}
diff --git a/test/benchmark/empty.wgsl.expected.msl b/test/benchmark/empty.wgsl.expected.msl
new file mode 100644
index 0000000..466ceaa
--- /dev/null
+++ b/test/benchmark/empty.wgsl.expected.msl
@@ -0,0 +1,3 @@
+#include <metal_stdlib>
+
+using namespace metal;
diff --git a/test/benchmark/empty.wgsl.expected.spvasm b/test/benchmark/empty.wgsl.expected.spvasm
new file mode 100644
index 0000000..65bef94
--- /dev/null
+++ b/test/benchmark/empty.wgsl.expected.spvasm
@@ -0,0 +1,16 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 5
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %unused_entry_point "unused_entry_point"
+               OpExecutionMode %unused_entry_point LocalSize 1 1 1
+               OpName %unused_entry_point "unused_entry_point"
+       %void = OpTypeVoid
+          %1 = OpTypeFunction %void
+%unused_entry_point = OpFunction %void None %1
+          %4 = OpLabel
+               OpReturn
+               OpFunctionEnd
diff --git a/test/benchmark/empty.wgsl.expected.wgsl b/test/benchmark/empty.wgsl.expected.wgsl
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/benchmark/empty.wgsl.expected.wgsl
diff --git a/test/benchmark/particles.wgsl b/test/benchmark/particles.wgsl
new file mode 100644
index 0000000..4734a37
--- /dev/null
+++ b/test/benchmark/particles.wgsl
@@ -0,0 +1,183 @@
+////////////////////////////////////////////////////////////////////////////////
+// Utilities
+////////////////////////////////////////////////////////////////////////////////
+var<private> rand_seed : vec2<f32>;
+
+fn rand() -> f32 {
+    rand_seed.x = fract(cos(dot(rand_seed, vec2<f32>(23.14077926, 232.61690225))) * 136.8168);
+    rand_seed.y = fract(cos(dot(rand_seed, vec2<f32>(54.47856553, 345.84153136))) * 534.7645);
+    return rand_seed.y;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Vertex shader
+////////////////////////////////////////////////////////////////////////////////
+struct RenderParams {
+  modelViewProjectionMatrix : mat4x4<f32>;
+  right : vec3<f32>;
+  up    : vec3<f32>;
+};
+[[binding(0), group(0)]] var<uniform> render_params : RenderParams;
+
+struct VertexInput {
+  [[location(0)]] position : vec3<f32>;
+  [[location(1)]] color    : vec4<f32>;
+  [[location(2)]] quad_pos : vec2<f32>; // -1..+1
+};
+
+struct VertexOutput {
+  [[builtin(position)]] position : vec4<f32>;
+  [[location(0)]]       color    : vec4<f32>;
+  [[location(1)]]       quad_pos : vec2<f32>; // -1..+1
+};
+
+[[stage(vertex)]]
+fn vs_main(in : VertexInput) -> VertexOutput {
+  var quad_pos = mat2x3<f32>(render_params.right, render_params.up) * in.quad_pos;
+  var position = in.position + quad_pos * 0.01;
+  var out : VertexOutput;
+  out.position = render_params.modelViewProjectionMatrix * vec4<f32>(position, 1.0);
+  out.color = in.color;
+  out.quad_pos = in.quad_pos;
+  return out;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Fragment shader
+////////////////////////////////////////////////////////////////////////////////
+[[stage(fragment)]]
+fn fs_main(in : VertexOutput) -> [[location(0)]] vec4<f32> {
+  var color = in.color;
+  // Apply a circular particle alpha mask
+  color.a = color.a * max(1.0 - length(in.quad_pos), 0.0);
+  return color;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Simulation Compute shader
+////////////////////////////////////////////////////////////////////////////////
+struct SimulationParams {
+  deltaTime : f32;
+  seed : vec4<f32>;
+};
+
+struct Particle {
+  position : vec3<f32>;
+  lifetime : f32;
+  color    : vec4<f32>;
+  velocity : vec3<f32>;
+};
+
+struct Particles {
+  particles : array<Particle>;
+};
+
+[[binding(0), group(0)]] var<uniform> sim_params : SimulationParams;
+[[binding(1), group(0)]] var<storage, read_write> data : Particles;
+[[binding(2), group(0)]] var texture : texture_2d<f32>;
+
+[[stage(compute), workgroup_size(64)]]
+fn simulate([[builtin(global_invocation_id)]] GlobalInvocationID : vec3<u32>) {
+  rand_seed = (sim_params.seed.xy + vec2<f32>(GlobalInvocationID.xy)) * sim_params.seed.zw;
+
+  let idx = GlobalInvocationID.x;
+  var particle = data.particles[idx];
+
+  // Apply gravity
+  particle.velocity.z = particle.velocity.z - sim_params.deltaTime * 0.5;
+
+  // Basic velocity integration
+  particle.position = particle.position + sim_params.deltaTime * particle.velocity;
+
+  // Age each particle. Fade out before vanishing.
+  particle.lifetime = particle.lifetime - sim_params.deltaTime;
+  particle.color.a = smoothStep(0.0, 0.5, particle.lifetime);
+
+  // If the lifetime has gone negative, then the particle is dead and should be
+  // respawned.
+  if (particle.lifetime < 0.0) {
+    // Use the probability map to find where the particle should be spawned.
+    // Starting with the 1x1 mip level.
+    var coord = vec2<i32>(0, 0);
+    for (var level = textureNumLevels(texture) - 1; level > 0; level = level - 1) {
+      // Load the probability value from the mip-level
+      // Generate a random number and using the probabilty values, pick the
+      // next texel in the next largest mip level:
+      //
+      // 0.0    probabilites.r    probabilites.g    probabilites.b   1.0
+      //  |              |              |              |              |
+      //  |   TOP-LEFT   |  TOP-RIGHT   | BOTTOM-LEFT  | BOTTOM_RIGHT |
+      //
+      let probabilites = textureLoad(texture, coord, level);
+      let value = vec4<f32>(rand());
+      let mask = (value >= vec4<f32>(0.0, probabilites.xyz)) & (value < probabilites);
+      coord = coord * 2;
+      coord.x = coord.x + select(0, 1, any(mask.yw)); // x  y
+      coord.y = coord.y + select(0, 1, any(mask.zw)); // z  w
+    }
+    let uv = vec2<f32>(coord) / vec2<f32>(textureDimensions(texture));
+    particle.position = vec3<f32>((uv - 0.5) * 3.0 * vec2<f32>(1.0, -1.0), 0.0);
+    particle.color = textureLoad(texture, coord, 0);
+    particle.velocity.x = (rand() - 0.5) * 0.1;
+    particle.velocity.y = (rand() - 0.5) * 0.1;
+    particle.velocity.z = rand() * 0.3;
+    particle.lifetime = 0.5 + rand() * 2.0;
+  }
+
+  // Store the new particle value
+  data.particles[idx] = particle;
+}
+
+struct UBO {
+  width : u32;
+};
+
+struct Buffer {
+  weights : array<f32>;
+};
+
+[[binding(3), group(0)]] var<uniform> ubo : UBO;
+[[binding(4), group(0)]] var<storage, read> buf_in : Buffer;
+[[binding(5), group(0)]] var<storage, read_write> buf_out : Buffer;
+[[binding(6), group(0)]] var tex_in : texture_2d<f32>;
+[[binding(7), group(0)]] var tex_out : texture_storage_2d<rgba8unorm, write>;
+
+////////////////////////////////////////////////////////////////////////////////
+// import_level
+//
+// Loads the alpha channel from a texel of the source image, and writes it to
+// the buf_out.weights.
+////////////////////////////////////////////////////////////////////////////////
+[[stage(compute), workgroup_size(64)]]
+fn import_level([[builtin(global_invocation_id)]] coord : vec3<u32>) {
+  _ = &buf_in;
+  let offset = coord.x + coord.y * ubo.width;
+  buf_out.weights[offset] = textureLoad(tex_in, vec2<i32>(coord.xy), 0).w;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// export_level
+//
+// Loads 4 f32 weight values from buf_in.weights, and stores summed value into
+// buf_out.weights, along with the calculated 'probabilty' vec4 values into the
+// mip level of tex_out. See simulate() in particle.wgsl to understand the
+// probability logic.
+////////////////////////////////////////////////////////////////////////////////
+[[stage(compute), workgroup_size(64)]]
+fn export_level([[builtin(global_invocation_id)]] coord : vec3<u32>) {
+  if (all(coord.xy < vec2<u32>(textureDimensions(tex_out)))) {
+    let dst_offset = coord.x    + coord.y    * ubo.width;
+    let src_offset = coord.x*2u + coord.y*2u * ubo.width;
+
+    let a = buf_in.weights[src_offset + 0u];
+    let b = buf_in.weights[src_offset + 1u];
+    let c = buf_in.weights[src_offset + 0u + ubo.width];
+    let d = buf_in.weights[src_offset + 1u + ubo.width];
+    let sum = dot(vec4<f32>(a, b, c, d), vec4<f32>(1.0));
+
+    buf_out.weights[dst_offset] = sum / 4.0;
+
+    let probabilities = vec4<f32>(a, a+b, a+b+c, sum) / max(sum, 0.0001);
+    textureStore(tex_out, vec2<i32>(coord.xy), probabilities);
+  }
+}
diff --git a/test/benchmark/particles.wgsl.expected.hlsl b/test/benchmark/particles.wgsl.expected.hlsl
new file mode 100644
index 0000000..2a91f43
--- /dev/null
+++ b/test/benchmark/particles.wgsl.expected.hlsl
@@ -0,0 +1,204 @@
+static float2 rand_seed = float2(0.0f, 0.0f);
+
+float rand() {
+  rand_seed.x = frac((cos(dot(rand_seed, float2(23.140779495f, 232.616897583f))) * 136.816802979f));
+  rand_seed.y = frac((cos(dot(rand_seed, float2(54.478565216f, 345.841522217f))) * 534.764526367f));
+  return rand_seed.y;
+}
+
+cbuffer cbuffer_render_params : register(b0, space0) {
+  uint4 render_params[6];
+};
+
+struct VertexInput {
+  float3 position;
+  float4 color;
+  float2 quad_pos;
+};
+struct VertexOutput {
+  float4 position;
+  float4 color;
+  float2 quad_pos;
+};
+struct tint_symbol_5 {
+  float3 position : TEXCOORD0;
+  float4 color : TEXCOORD1;
+  float2 quad_pos : TEXCOORD2;
+};
+struct tint_symbol_6 {
+  float4 color : TEXCOORD0;
+  float2 quad_pos : TEXCOORD1;
+  float4 position : SV_Position;
+};
+
+float4x4 tint_symbol_17(uint4 buffer[6], uint offset) {
+  const uint scalar_offset = ((offset + 0u)) / 4;
+  const uint scalar_offset_1 = ((offset + 16u)) / 4;
+  const uint scalar_offset_2 = ((offset + 32u)) / 4;
+  const uint scalar_offset_3 = ((offset + 48u)) / 4;
+  return float4x4(asfloat(buffer[scalar_offset / 4]), asfloat(buffer[scalar_offset_1 / 4]), asfloat(buffer[scalar_offset_2 / 4]), asfloat(buffer[scalar_offset_3 / 4]));
+}
+
+VertexOutput vs_main_inner(VertexInput tint_symbol) {
+  float3 quad_pos = mul(tint_symbol.quad_pos, float2x3(asfloat(render_params[4].xyz), asfloat(render_params[5].xyz)));
+  float3 position = (tint_symbol.position + (quad_pos * 0.01f));
+  VertexOutput tint_symbol_1 = (VertexOutput)0;
+  tint_symbol_1.position = mul(float4(position, 1.0f), tint_symbol_17(render_params, 0u));
+  tint_symbol_1.color = tint_symbol.color;
+  tint_symbol_1.quad_pos = tint_symbol.quad_pos;
+  return tint_symbol_1;
+}
+
+tint_symbol_6 vs_main(tint_symbol_5 tint_symbol_4) {
+  const VertexInput tint_symbol_32 = {tint_symbol_4.position, tint_symbol_4.color, tint_symbol_4.quad_pos};
+  const VertexOutput inner_result = vs_main_inner(tint_symbol_32);
+  tint_symbol_6 wrapper_result = (tint_symbol_6)0;
+  wrapper_result.position = inner_result.position;
+  wrapper_result.color = inner_result.color;
+  wrapper_result.quad_pos = inner_result.quad_pos;
+  return wrapper_result;
+}
+
+struct tint_symbol_8 {
+  float4 color : TEXCOORD0;
+  float2 quad_pos : TEXCOORD1;
+  float4 position : SV_Position;
+};
+struct tint_symbol_9 {
+  float4 value : SV_Target0;
+};
+
+float4 fs_main_inner(VertexOutput tint_symbol) {
+  float4 color = tint_symbol.color;
+  color.a = (color.a * max((1.0f - length(tint_symbol.quad_pos)), 0.0f));
+  return color;
+}
+
+tint_symbol_9 fs_main(tint_symbol_8 tint_symbol_7) {
+  const VertexOutput tint_symbol_33 = {tint_symbol_7.position, tint_symbol_7.color, tint_symbol_7.quad_pos};
+  const float4 inner_result_1 = fs_main_inner(tint_symbol_33);
+  tint_symbol_9 wrapper_result_1 = (tint_symbol_9)0;
+  wrapper_result_1.value = inner_result_1;
+  return wrapper_result_1;
+}
+
+struct Particle {
+  float3 position;
+  float lifetime;
+  float4 color;
+  float3 velocity;
+};
+
+cbuffer cbuffer_sim_params : register(b0, space0) {
+  uint4 sim_params[2];
+};
+RWByteAddressBuffer data : register(u1, space0);
+Texture2D<float4> tint_symbol_2 : register(t2, space0);
+
+struct tint_symbol_11 {
+  uint3 GlobalInvocationID : SV_DispatchThreadID;
+};
+
+Particle tint_symbol_20(RWByteAddressBuffer buffer, uint offset) {
+  const Particle tint_symbol_34 = {asfloat(buffer.Load3((offset + 0u))), asfloat(buffer.Load((offset + 12u))), asfloat(buffer.Load4((offset + 16u))), asfloat(buffer.Load3((offset + 32u)))};
+  return tint_symbol_34;
+}
+
+void tint_symbol_25(RWByteAddressBuffer buffer, uint offset, Particle value) {
+  buffer.Store3((offset + 0u), asuint(value.position));
+  buffer.Store((offset + 12u), asuint(value.lifetime));
+  buffer.Store4((offset + 16u), asuint(value.color));
+  buffer.Store3((offset + 32u), asuint(value.velocity));
+}
+
+void simulate_inner(uint3 GlobalInvocationID) {
+  rand_seed = ((asfloat(sim_params[1]).xy + float2(GlobalInvocationID.xy)) * asfloat(sim_params[1]).zw);
+  const uint idx = GlobalInvocationID.x;
+  Particle particle = tint_symbol_20(data, (48u * idx));
+  particle.velocity.z = (particle.velocity.z - (asfloat(sim_params[0].x) * 0.5f));
+  particle.position = (particle.position + (asfloat(sim_params[0].x) * particle.velocity));
+  particle.lifetime = (particle.lifetime - asfloat(sim_params[0].x));
+  particle.color.a = smoothstep(0.0f, 0.5f, particle.lifetime);
+  if ((particle.lifetime < 0.0f)) {
+    int2 coord = int2(0, 0);
+    {
+      int3 tint_tmp;
+      tint_symbol_2.GetDimensions(0, tint_tmp.x, tint_tmp.y, tint_tmp.z);
+      int level = (tint_tmp.z - 1);
+      [loop] for(; (level > 0); level = (level - 1)) {
+        const float4 probabilites = tint_symbol_2.Load(int3(coord, level));
+        const float4 value = float4((rand()).xxxx);
+        const bool4 mask = ((value >= float4(0.0f, probabilites.xyz)) & (value < probabilites));
+        coord = (coord * 2);
+        coord.x = (coord.x + (any(mask.yw) ? 1 : 0));
+        coord.y = (coord.y + (any(mask.zw) ? 1 : 0));
+      }
+    }
+    int2 tint_tmp_1;
+    tint_symbol_2.GetDimensions(tint_tmp_1.x, tint_tmp_1.y);
+    const float2 uv = (float2(coord) / float2(tint_tmp_1));
+    particle.position = float3((((uv - 0.5f) * 3.0f) * float2(1.0f, -1.0f)), 0.0f);
+    particle.color = tint_symbol_2.Load(int3(coord, 0));
+    particle.velocity.x = ((rand() - 0.5f) * 0.100000001f);
+    particle.velocity.y = ((rand() - 0.5f) * 0.100000001f);
+    particle.velocity.z = (rand() * 0.300000012f);
+    particle.lifetime = (0.5f + (rand() * 2.0f));
+  }
+  tint_symbol_25(data, (48u * idx), particle);
+}
+
+[numthreads(64, 1, 1)]
+void simulate(tint_symbol_11 tint_symbol_10) {
+  simulate_inner(tint_symbol_10.GlobalInvocationID);
+  return;
+}
+
+cbuffer cbuffer_ubo : register(b3, space0) {
+  uint4 ubo[1];
+};
+ByteAddressBuffer buf_in : register(t4, space0);
+RWByteAddressBuffer buf_out : register(u5, space0);
+Texture2D<float4> tex_in : register(t6, space0);
+RWTexture2D<float4> tex_out : register(u7, space0);
+
+struct tint_symbol_13 {
+  uint3 coord : SV_DispatchThreadID;
+};
+
+void import_level_inner(uint3 coord) {
+  const uint offset = (coord.x + (coord.y * ubo[0].x));
+  buf_out.Store((4u * offset), asuint(tex_in.Load(int3(int2(coord.xy), 0)).w));
+}
+
+[numthreads(64, 1, 1)]
+void import_level(tint_symbol_13 tint_symbol_12) {
+  import_level_inner(tint_symbol_12.coord);
+  return;
+}
+
+struct tint_symbol_15 {
+  uint3 coord : SV_DispatchThreadID;
+};
+
+void export_level_inner(uint3 coord) {
+  int2 tint_tmp_2;
+  tex_out.GetDimensions(tint_tmp_2.x, tint_tmp_2.y);
+  if (all((coord.xy < uint2(tint_tmp_2)))) {
+    const uint dst_offset = (coord.x + (coord.y * ubo[0].x));
+    const uint src_offset = ((coord.x * 2u) + ((coord.y * 2u) * ubo[0].x));
+    const float a_1 = asfloat(buf_in.Load((4u * (src_offset + 0u))));
+    const float b = asfloat(buf_in.Load((4u * (src_offset + 1u))));
+    const float c = asfloat(buf_in.Load((4u * ((src_offset + 0u) + ubo[0].x))));
+    const float d = asfloat(buf_in.Load((4u * ((src_offset + 1u) + ubo[0].x))));
+    const float sum = dot(float4(a_1, b, c, d), float4((1.0f).xxxx));
+    buf_out.Store((4u * dst_offset), asuint((sum / 4.0f)));
+    const float4 probabilities = (float4(a_1, (a_1 + b), ((a_1 + b) + c), sum) / max(sum, 0.0001f));
+    tex_out[int2(coord.xy)] = probabilities;
+  }
+}
+
+[numthreads(64, 1, 1)]
+void export_level(tint_symbol_15 tint_symbol_14) {
+  export_level_inner(tint_symbol_14.coord);
+  return;
+}
diff --git a/test/benchmark/particles.wgsl.expected.msl b/test/benchmark/particles.wgsl.expected.msl
new file mode 100644
index 0000000..5644e7c
--- /dev/null
+++ b/test/benchmark/particles.wgsl.expected.msl
@@ -0,0 +1,175 @@
+#include <metal_stdlib>
+
+using namespace metal;
+
+template<typename T, int N, int M>
+inline vec<T, M> operator*(matrix<T, N, M> lhs, packed_vec<T, N> rhs) {
+  return lhs * vec<T, N>(rhs);
+}
+
+template<typename T, int N, int M>
+inline vec<T, N> operator*(packed_vec<T, M> lhs, matrix<T, N, M> rhs) {
+  return vec<T, M>(lhs) * rhs;
+}
+
+struct RenderParams {
+  /* 0x0000 */ float4x4 modelViewProjectionMatrix;
+  /* 0x0040 */ packed_float3 right;
+  /* 0x004c */ int8_t tint_pad[4];
+  /* 0x0050 */ packed_float3 up;
+  /* 0x005c */ int8_t tint_pad_1[4];
+};
+struct VertexInput {
+  float3 position;
+  float4 color;
+  float2 quad_pos;
+};
+struct VertexOutput {
+  float4 position;
+  float4 color;
+  float2 quad_pos;
+};
+struct tint_symbol_2 {
+  float3 position [[attribute(0)]];
+  float4 color [[attribute(1)]];
+  float2 quad_pos [[attribute(2)]];
+};
+struct tint_symbol_3 {
+  float4 color [[user(locn0)]];
+  float2 quad_pos [[user(locn1)]];
+  float4 position [[position]];
+};
+struct tint_symbol_5 {
+  float4 color [[user(locn0)]];
+  float2 quad_pos [[user(locn1)]];
+};
+struct tint_symbol_6 {
+  float4 value [[color(0)]];
+};
+struct SimulationParams {
+  /* 0x0000 */ float deltaTime;
+  /* 0x0004 */ int8_t tint_pad_2[12];
+  /* 0x0010 */ float4 seed;
+};
+struct Particle {
+  /* 0x0000 */ packed_float3 position;
+  /* 0x000c */ float lifetime;
+  /* 0x0010 */ float4 color;
+  /* 0x0020 */ packed_float3 velocity;
+  /* 0x002c */ int8_t tint_pad_3[4];
+};
+struct Particles {
+  /* 0x0000 */ Particle particles[1];
+};
+struct UBO {
+  /* 0x0000 */ uint width;
+};
+struct Buffer {
+  /* 0x0000 */ float weights[1];
+};
+
+float rand(thread float2* const tint_symbol_9) {
+  (*(tint_symbol_9))[0] = fract((cos(dot(*(tint_symbol_9), float2(23.140779495f, 232.616897583f))) * 136.816802979f));
+  (*(tint_symbol_9))[1] = fract((cos(dot(*(tint_symbol_9), float2(54.478565216f, 345.841522217f))) * 534.764526367f));
+  return (*(tint_symbol_9))[1];
+}
+
+VertexOutput vs_main_inner(VertexInput in, const constant RenderParams* const tint_symbol_10) {
+  float3 quad_pos = (float2x3((*(tint_symbol_10)).right, (*(tint_symbol_10)).up) * in.quad_pos);
+  float3 position = (in.position + (quad_pos * 0.01f));
+  VertexOutput out = {};
+  out.position = ((*(tint_symbol_10)).modelViewProjectionMatrix * float4(position, 1.0f));
+  out.color = in.color;
+  out.quad_pos = in.quad_pos;
+  return out;
+}
+
+vertex tint_symbol_3 vs_main(const constant RenderParams* tint_symbol_11 [[buffer(0)]], tint_symbol_2 tint_symbol_1 [[stage_in]]) {
+  VertexInput const tint_symbol_7 = {.position=tint_symbol_1.position, .color=tint_symbol_1.color, .quad_pos=tint_symbol_1.quad_pos};
+  VertexOutput const inner_result = vs_main_inner(tint_symbol_7, tint_symbol_11);
+  tint_symbol_3 wrapper_result = {};
+  wrapper_result.position = inner_result.position;
+  wrapper_result.color = inner_result.color;
+  wrapper_result.quad_pos = inner_result.quad_pos;
+  return wrapper_result;
+}
+
+float4 fs_main_inner(VertexOutput in) {
+  float4 color = in.color;
+  color[3] = (color[3] * fmax((1.0f - length(in.quad_pos)), 0.0f));
+  return color;
+}
+
+fragment tint_symbol_6 fs_main(float4 position [[position]], tint_symbol_5 tint_symbol_4 [[stage_in]]) {
+  VertexOutput const tint_symbol_8 = {.position=position, .color=tint_symbol_4.color, .quad_pos=tint_symbol_4.quad_pos};
+  float4 const inner_result_1 = fs_main_inner(tint_symbol_8);
+  tint_symbol_6 wrapper_result_1 = {};
+  wrapper_result_1.value = inner_result_1;
+  return wrapper_result_1;
+}
+
+void simulate_inner(uint3 GlobalInvocationID, thread float2* const tint_symbol_12, const constant SimulationParams* const tint_symbol_13, device Particles* const tint_symbol_14, texture2d<float, access::sample> tint_symbol_15) {
+  *(tint_symbol_12) = ((float4((*(tint_symbol_13)).seed).xy + float2(uint3(GlobalInvocationID).xy)) * float4((*(tint_symbol_13)).seed).zw);
+  uint const idx = GlobalInvocationID[0];
+  Particle particle = (*(tint_symbol_14)).particles[idx];
+  particle.velocity[2] = (particle.velocity[2] - ((*(tint_symbol_13)).deltaTime * 0.5f));
+  particle.position = (particle.position + ((*(tint_symbol_13)).deltaTime * particle.velocity));
+  particle.lifetime = (particle.lifetime - (*(tint_symbol_13)).deltaTime);
+  particle.color[3] = smoothstep(0.0f, 0.5f, particle.lifetime);
+  if ((particle.lifetime < 0.0f)) {
+    int2 coord = int2(0, 0);
+    for(int level = as_type<int>((as_type<uint>(int(tint_symbol_15.get_num_mip_levels())) - as_type<uint>(1))); (level > 0); level = as_type<int>((as_type<uint>(level) - as_type<uint>(1)))) {
+      float4 const probabilites = tint_symbol_15.read(uint2(coord), level);
+      float4 const value = float4(rand(tint_symbol_12));
+      bool4 const mask = ((value >= float4(0.0f, float4(probabilites).xyz)) & (value < probabilites));
+      coord = as_type<int2>((as_type<uint2>(coord) * as_type<uint>(2)));
+      coord[0] = as_type<int>((as_type<uint>(coord[0]) + as_type<uint>(select(0, 1, any(bool4(mask).yw)))));
+      coord[1] = as_type<int>((as_type<uint>(coord[1]) + as_type<uint>(select(0, 1, any(bool4(mask).zw)))));
+    }
+    float2 const uv = (float2(coord) / float2(int2(tint_symbol_15.get_width(), tint_symbol_15.get_height())));
+    particle.position = float3((((uv - 0.5f) * 3.0f) * float2(1.0f, -1.0f)), 0.0f);
+    particle.color = tint_symbol_15.read(uint2(coord), 0);
+    particle.velocity[0] = ((rand(tint_symbol_12) - 0.5f) * 0.100000001f);
+    particle.velocity[1] = ((rand(tint_symbol_12) - 0.5f) * 0.100000001f);
+    particle.velocity[2] = (rand(tint_symbol_12) * 0.300000012f);
+    particle.lifetime = (0.5f + (rand(tint_symbol_12) * 2.0f));
+  }
+  (*(tint_symbol_14)).particles[idx] = particle;
+}
+
+kernel void simulate(const constant SimulationParams* tint_symbol_17 [[buffer(0)]], device Particles* tint_symbol_18 [[buffer(1)]], texture2d<float, access::sample> tint_symbol_19 [[texture(0)]], uint3 GlobalInvocationID [[thread_position_in_grid]]) {
+  thread float2 tint_symbol_16 = 0.0f;
+  simulate_inner(GlobalInvocationID, &(tint_symbol_16), tint_symbol_17, tint_symbol_18, tint_symbol_19);
+  return;
+}
+
+void import_level_inner(uint3 coord, const constant UBO* const tint_symbol_20, device Buffer* const tint_symbol_21, texture2d<float, access::sample> tint_symbol_22) {
+  uint const offset = (coord[0] + (coord[1] * (*(tint_symbol_20)).width));
+  (*(tint_symbol_21)).weights[offset] = tint_symbol_22.read(uint2(int2(uint3(coord).xy)), 0)[3];
+}
+
+kernel void import_level(const constant UBO* tint_symbol_23 [[buffer(2)]], device Buffer* tint_symbol_24 [[buffer(3)]], texture2d<float, access::sample> tint_symbol_25 [[texture(1)]], uint3 coord [[thread_position_in_grid]]) {
+  import_level_inner(coord, tint_symbol_23, tint_symbol_24, tint_symbol_25);
+  return;
+}
+
+void export_level_inner(uint3 coord, texture2d<float, access::write> tint_symbol_26, const constant UBO* const tint_symbol_27, const device Buffer* const tint_symbol_28, device Buffer* const tint_symbol_29) {
+  if (all((uint3(coord).xy < uint2(int2(tint_symbol_26.get_width(), tint_symbol_26.get_height()))))) {
+    uint const dst_offset = (coord[0] + (coord[1] * (*(tint_symbol_27)).width));
+    uint const src_offset = ((coord[0] * 2u) + ((coord[1] * 2u) * (*(tint_symbol_27)).width));
+    float const a_1 = (*(tint_symbol_28)).weights[(src_offset + 0u)];
+    float const b = (*(tint_symbol_28)).weights[(src_offset + 1u)];
+    float const c = (*(tint_symbol_28)).weights[((src_offset + 0u) + (*(tint_symbol_27)).width)];
+    float const d = (*(tint_symbol_28)).weights[((src_offset + 1u) + (*(tint_symbol_27)).width)];
+    float const sum = dot(float4(a_1, b, c, d), float4(1.0f));
+    (*(tint_symbol_29)).weights[dst_offset] = (sum / 4.0f);
+    float4 const probabilities = (float4(a_1, (a_1 + b), ((a_1 + b) + c), sum) / fmax(sum, 0.0001f));
+    tint_symbol_26.write(probabilities, uint2(int2(uint3(coord).xy)));
+  }
+}
+
+kernel void export_level(texture2d<float, access::write> tint_symbol_30 [[texture(2)]], const constant UBO* tint_symbol_31 [[buffer(2)]], const device Buffer* tint_symbol_32 [[buffer(4)]], device Buffer* tint_symbol_33 [[buffer(3)]], uint3 coord [[thread_position_in_grid]]) {
+  export_level_inner(coord, tint_symbol_30, tint_symbol_31, tint_symbol_32, tint_symbol_33);
+  return;
+}
+
diff --git a/test/benchmark/particles.wgsl.expected.spvasm b/test/benchmark/particles.wgsl.expected.spvasm
new file mode 100644
index 0000000..fe13031
--- /dev/null
+++ b/test/benchmark/particles.wgsl.expected.spvasm
@@ -0,0 +1,646 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 423
+; Schema: 0
+               OpCapability Shader
+               OpCapability ImageQuery
+         %67 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Vertex %vs_main "vs_main" %position_1 %color_1 %quad_pos_1 %position_2 %color_2 %quad_pos_2 %vertex_point_size
+               OpEntryPoint Fragment %fs_main "fs_main" %position_3 %color_3 %quad_pos_3 %value_1
+               OpEntryPoint GLCompute %simulate "simulate" %GlobalInvocationID_1
+               OpEntryPoint GLCompute %import_level "import_level" %coord_1
+               OpEntryPoint GLCompute %export_level "export_level" %coord_2
+               OpExecutionMode %fs_main OriginUpperLeft
+               OpExecutionMode %simulate LocalSize 64 1 1
+               OpExecutionMode %import_level LocalSize 64 1 1
+               OpExecutionMode %export_level LocalSize 64 1 1
+               OpName %position_1 "position_1"
+               OpName %color_1 "color_1"
+               OpName %quad_pos_1 "quad_pos_1"
+               OpName %position_2 "position_2"
+               OpName %color_2 "color_2"
+               OpName %quad_pos_2 "quad_pos_2"
+               OpName %vertex_point_size "vertex_point_size"
+               OpName %position_3 "position_3"
+               OpName %color_3 "color_3"
+               OpName %quad_pos_3 "quad_pos_3"
+               OpName %value_1 "value_1"
+               OpName %GlobalInvocationID_1 "GlobalInvocationID_1"
+               OpName %coord_1 "coord_1"
+               OpName %coord_2 "coord_2"
+               OpName %rand_seed "rand_seed"
+               OpName %RenderParams "RenderParams"
+               OpMemberName %RenderParams 0 "modelViewProjectionMatrix"
+               OpMemberName %RenderParams 1 "right"
+               OpMemberName %RenderParams 2 "up"
+               OpName %render_params "render_params"
+               OpName %SimulationParams "SimulationParams"
+               OpMemberName %SimulationParams 0 "deltaTime"
+               OpMemberName %SimulationParams 1 "seed"
+               OpName %sim_params "sim_params"
+               OpName %Particles "Particles"
+               OpMemberName %Particles 0 "particles"
+               OpName %Particle "Particle"
+               OpMemberName %Particle 0 "position"
+               OpMemberName %Particle 1 "lifetime"
+               OpMemberName %Particle 2 "color"
+               OpMemberName %Particle 3 "velocity"
+               OpName %data "data"
+               OpName %texture "texture"
+               OpName %UBO "UBO"
+               OpMemberName %UBO 0 "width"
+               OpName %ubo "ubo"
+               OpName %Buffer "Buffer"
+               OpMemberName %Buffer 0 "weights"
+               OpName %buf_in "buf_in"
+               OpName %buf_out "buf_out"
+               OpName %tex_in "tex_in"
+               OpName %tex_out "tex_out"
+               OpName %rand "rand"
+               OpName %VertexOutput "VertexOutput"
+               OpMemberName %VertexOutput 0 "position"
+               OpMemberName %VertexOutput 1 "color"
+               OpMemberName %VertexOutput 2 "quad_pos"
+               OpName %VertexInput "VertexInput"
+               OpMemberName %VertexInput 0 "position"
+               OpMemberName %VertexInput 1 "color"
+               OpMemberName %VertexInput 2 "quad_pos"
+               OpName %vs_main_inner "vs_main_inner"
+               OpName %in "in"
+               OpName %quad_pos "quad_pos"
+               OpName %position "position"
+               OpName %out "out"
+               OpName %vs_main "vs_main"
+               OpName %fs_main_inner "fs_main_inner"
+               OpName %in_0 "in"
+               OpName %color "color"
+               OpName %fs_main "fs_main"
+               OpName %simulate_inner "simulate_inner"
+               OpName %GlobalInvocationID "GlobalInvocationID"
+               OpName %particle "particle"
+               OpName %coord "coord"
+               OpName %level "level"
+               OpName %simulate "simulate"
+               OpName %import_level_inner "import_level_inner"
+               OpName %coord_0 "coord"
+               OpName %import_level "import_level"
+               OpName %export_level_inner "export_level_inner"
+               OpName %coord_3 "coord"
+               OpName %export_level "export_level"
+               OpDecorate %position_1 Location 0
+               OpDecorate %color_1 Location 1
+               OpDecorate %quad_pos_1 Location 2
+               OpDecorate %position_2 BuiltIn Position
+               OpDecorate %color_2 Location 0
+               OpDecorate %quad_pos_2 Location 1
+               OpDecorate %vertex_point_size BuiltIn PointSize
+               OpDecorate %position_3 BuiltIn FragCoord
+               OpDecorate %color_3 Location 0
+               OpDecorate %quad_pos_3 Location 1
+               OpDecorate %value_1 Location 0
+               OpDecorate %GlobalInvocationID_1 BuiltIn GlobalInvocationId
+               OpDecorate %coord_1 BuiltIn GlobalInvocationId
+               OpDecorate %coord_2 BuiltIn GlobalInvocationId
+               OpDecorate %RenderParams Block
+               OpMemberDecorate %RenderParams 0 Offset 0
+               OpMemberDecorate %RenderParams 0 ColMajor
+               OpMemberDecorate %RenderParams 0 MatrixStride 16
+               OpMemberDecorate %RenderParams 1 Offset 64
+               OpMemberDecorate %RenderParams 2 Offset 80
+               OpDecorate %render_params NonWritable
+               OpDecorate %render_params Binding 0
+               OpDecorate %render_params DescriptorSet 0
+               OpDecorate %SimulationParams Block
+               OpMemberDecorate %SimulationParams 0 Offset 0
+               OpMemberDecorate %SimulationParams 1 Offset 16
+               OpDecorate %sim_params NonWritable
+               OpDecorate %sim_params Binding 0
+               OpDecorate %sim_params DescriptorSet 0
+               OpDecorate %Particles Block
+               OpMemberDecorate %Particles 0 Offset 0
+               OpMemberDecorate %Particle 0 Offset 0
+               OpMemberDecorate %Particle 1 Offset 12
+               OpMemberDecorate %Particle 2 Offset 16
+               OpMemberDecorate %Particle 3 Offset 32
+               OpDecorate %_runtimearr_Particle ArrayStride 48
+               OpDecorate %data Binding 1
+               OpDecorate %data DescriptorSet 0
+               OpDecorate %texture Binding 2
+               OpDecorate %texture DescriptorSet 0
+               OpDecorate %UBO Block
+               OpMemberDecorate %UBO 0 Offset 0
+               OpDecorate %ubo NonWritable
+               OpDecorate %ubo Binding 3
+               OpDecorate %ubo DescriptorSet 0
+               OpDecorate %Buffer Block
+               OpMemberDecorate %Buffer 0 Offset 0
+               OpDecorate %_runtimearr_float ArrayStride 4
+               OpDecorate %buf_in NonWritable
+               OpDecorate %buf_in Binding 4
+               OpDecorate %buf_in DescriptorSet 0
+               OpDecorate %buf_out Binding 5
+               OpDecorate %buf_out DescriptorSet 0
+               OpDecorate %tex_in Binding 6
+               OpDecorate %tex_in DescriptorSet 0
+               OpDecorate %tex_out NonReadable
+               OpDecorate %tex_out Binding 7
+               OpDecorate %tex_out DescriptorSet 0
+               OpMemberDecorate %VertexOutput 0 Offset 0
+               OpMemberDecorate %VertexOutput 1 Offset 16
+               OpMemberDecorate %VertexOutput 2 Offset 32
+               OpMemberDecorate %VertexInput 0 Offset 0
+               OpMemberDecorate %VertexInput 1 Offset 16
+               OpMemberDecorate %VertexInput 2 Offset 32
+      %float = OpTypeFloat 32
+    %v3float = OpTypeVector %float 3
+%_ptr_Input_v3float = OpTypePointer Input %v3float
+ %position_1 = OpVariable %_ptr_Input_v3float Input
+    %v4float = OpTypeVector %float 4
+%_ptr_Input_v4float = OpTypePointer Input %v4float
+    %color_1 = OpVariable %_ptr_Input_v4float Input
+    %v2float = OpTypeVector %float 2
+%_ptr_Input_v2float = OpTypePointer Input %v2float
+ %quad_pos_1 = OpVariable %_ptr_Input_v2float Input
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+         %13 = OpConstantNull %v4float
+ %position_2 = OpVariable %_ptr_Output_v4float Output %13
+    %color_2 = OpVariable %_ptr_Output_v4float Output %13
+%_ptr_Output_v2float = OpTypePointer Output %v2float
+         %17 = OpConstantNull %v2float
+ %quad_pos_2 = OpVariable %_ptr_Output_v2float Output %17
+%_ptr_Output_float = OpTypePointer Output %float
+         %20 = OpConstantNull %float
+%vertex_point_size = OpVariable %_ptr_Output_float Output %20
+ %position_3 = OpVariable %_ptr_Input_v4float Input
+    %color_3 = OpVariable %_ptr_Input_v4float Input
+ %quad_pos_3 = OpVariable %_ptr_Input_v2float Input
+    %value_1 = OpVariable %_ptr_Output_v4float Output %13
+       %uint = OpTypeInt 32 0
+     %v3uint = OpTypeVector %uint 3
+%_ptr_Input_v3uint = OpTypePointer Input %v3uint
+%GlobalInvocationID_1 = OpVariable %_ptr_Input_v3uint Input
+    %coord_1 = OpVariable %_ptr_Input_v3uint Input
+    %coord_2 = OpVariable %_ptr_Input_v3uint Input
+%_ptr_Private_v2float = OpTypePointer Private %v2float
+  %rand_seed = OpVariable %_ptr_Private_v2float Private %17
+%mat4v4float = OpTypeMatrix %v4float 4
+%RenderParams = OpTypeStruct %mat4v4float %v3float %v3float
+%_ptr_Uniform_RenderParams = OpTypePointer Uniform %RenderParams
+%render_params = OpVariable %_ptr_Uniform_RenderParams Uniform
+%SimulationParams = OpTypeStruct %float %v4float
+%_ptr_Uniform_SimulationParams = OpTypePointer Uniform %SimulationParams
+ %sim_params = OpVariable %_ptr_Uniform_SimulationParams Uniform
+   %Particle = OpTypeStruct %v3float %float %v4float %v3float
+%_runtimearr_Particle = OpTypeRuntimeArray %Particle
+  %Particles = OpTypeStruct %_runtimearr_Particle
+%_ptr_StorageBuffer_Particles = OpTypePointer StorageBuffer %Particles
+       %data = OpVariable %_ptr_StorageBuffer_Particles StorageBuffer
+         %47 = OpTypeImage %float 2D 0 0 0 1 Unknown
+%_ptr_UniformConstant_47 = OpTypePointer UniformConstant %47
+    %texture = OpVariable %_ptr_UniformConstant_47 UniformConstant
+        %UBO = OpTypeStruct %uint
+%_ptr_Uniform_UBO = OpTypePointer Uniform %UBO
+        %ubo = OpVariable %_ptr_Uniform_UBO Uniform
+%_runtimearr_float = OpTypeRuntimeArray %float
+     %Buffer = OpTypeStruct %_runtimearr_float
+%_ptr_StorageBuffer_Buffer = OpTypePointer StorageBuffer %Buffer
+     %buf_in = OpVariable %_ptr_StorageBuffer_Buffer StorageBuffer
+    %buf_out = OpVariable %_ptr_StorageBuffer_Buffer StorageBuffer
+     %tex_in = OpVariable %_ptr_UniformConstant_47 UniformConstant
+         %59 = OpTypeImage %float 2D 0 0 0 2 Rgba8
+%_ptr_UniformConstant_59 = OpTypePointer UniformConstant %59
+    %tex_out = OpVariable %_ptr_UniformConstant_59 UniformConstant
+         %60 = OpTypeFunction %float
+     %uint_0 = OpConstant %uint 0
+%_ptr_Private_float = OpTypePointer Private %float
+%float_23_1407795 = OpConstant %float 23.1407795
+%float_232_616898 = OpConstant %float 232.616898
+         %73 = OpConstantComposite %v2float %float_23_1407795 %float_232_616898
+%float_136_816803 = OpConstant %float 136.816803
+     %uint_1 = OpConstant %uint 1
+%float_54_4785652 = OpConstant %float 54.4785652
+%float_345_841522 = OpConstant %float 345.841522
+         %84 = OpConstantComposite %v2float %float_54_4785652 %float_345_841522
+%float_534_764526 = OpConstant %float 534.764526
+%VertexOutput = OpTypeStruct %v4float %v4float %v2float
+%VertexInput = OpTypeStruct %v3float %v4float %v2float
+         %89 = OpTypeFunction %VertexOutput %VertexInput
+%mat2v3float = OpTypeMatrix %v3float 2
+%_ptr_Uniform_v3float = OpTypePointer Uniform %v3float
+     %uint_2 = OpConstant %uint 2
+%_ptr_Function_v3float = OpTypePointer Function %v3float
+        %107 = OpConstantNull %v3float
+%float_0_00999999978 = OpConstant %float 0.00999999978
+%_ptr_Function_VertexOutput = OpTypePointer Function %VertexOutput
+        %116 = OpConstantNull %VertexOutput
+%_ptr_Function_v4float = OpTypePointer Function %v4float
+%_ptr_Uniform_mat4v4float = OpTypePointer Uniform %mat4v4float
+    %float_1 = OpConstant %float 1
+%_ptr_Function_v2float = OpTypePointer Function %v2float
+       %void = OpTypeVoid
+        %135 = OpTypeFunction %void
+        %147 = OpTypeFunction %v4float %VertexOutput
+     %uint_3 = OpConstant %uint 3
+%_ptr_Function_float = OpTypePointer Function %float
+    %float_0 = OpConstant %float 0
+        %172 = OpTypeFunction %void %v3uint
+%_ptr_Uniform_v4float = OpTypePointer Uniform %v4float
+     %v2uint = OpTypeVector %uint 2
+%_ptr_StorageBuffer_Particle = OpTypePointer StorageBuffer %Particle
+%_ptr_Function_Particle = OpTypePointer Function %Particle
+        %194 = OpConstantNull %Particle
+%_ptr_Uniform_float = OpTypePointer Uniform %float
+  %float_0_5 = OpConstant %float 0.5
+       %bool = OpTypeBool
+        %int = OpTypeInt 32 1
+      %v2int = OpTypeVector %int 2
+      %int_0 = OpConstant %int 0
+        %232 = OpConstantComposite %v2int %int_0 %int_0
+%_ptr_Function_v2int = OpTypePointer Function %v2int
+        %235 = OpConstantNull %v2int
+      %int_1 = OpConstant %int 1
+%_ptr_Function_int = OpTypePointer Function %int
+        %242 = OpConstantNull %int
+     %v4bool = OpTypeVector %bool 4
+      %int_2 = OpConstant %int 2
+     %v2bool = OpTypeVector %bool 2
+    %float_3 = OpConstant %float 3
+   %float_n1 = OpConstant %float -1
+        %302 = OpConstantComposite %v2float %float_1 %float_n1
+%float_0_100000001 = OpConstant %float 0.100000001
+%float_0_300000012 = OpConstant %float 0.300000012
+    %float_2 = OpConstant %float 2
+%_ptr_Uniform_uint = OpTypePointer Uniform %uint
+%_ptr_StorageBuffer_float = OpTypePointer StorageBuffer %float
+        %402 = OpConstantComposite %v4float %float_1 %float_1 %float_1 %float_1
+    %float_4 = OpConstant %float 4
+%float_9_99999975en05 = OpConstant %float 9.99999975e-05
+       %rand = OpFunction %float None %60
+         %62 = OpLabel
+         %65 = OpAccessChain %_ptr_Private_float %rand_seed %uint_0
+         %70 = OpLoad %v2float %rand_seed
+         %69 = OpDot %float %70 %73
+         %68 = OpExtInst %float %67 Cos %69
+         %75 = OpFMul %float %68 %float_136_816803
+         %66 = OpExtInst %float %67 Fract %75
+               OpStore %65 %66
+         %77 = OpAccessChain %_ptr_Private_float %rand_seed %uint_1
+         %81 = OpLoad %v2float %rand_seed
+         %80 = OpDot %float %81 %84
+         %79 = OpExtInst %float %67 Cos %80
+         %86 = OpFMul %float %79 %float_534_764526
+         %78 = OpExtInst %float %67 Fract %86
+               OpStore %77 %78
+         %87 = OpAccessChain %_ptr_Private_float %rand_seed %uint_1
+         %88 = OpLoad %float %87
+               OpReturnValue %88
+               OpFunctionEnd
+%vs_main_inner = OpFunction %VertexOutput None %89
+         %in = OpFunctionParameter %VertexInput
+         %94 = OpLabel
+   %quad_pos = OpVariable %_ptr_Function_v3float Function %107
+   %position = OpVariable %_ptr_Function_v3float Function %107
+        %out = OpVariable %_ptr_Function_VertexOutput Function %116
+         %97 = OpAccessChain %_ptr_Uniform_v3float %render_params %uint_1
+         %98 = OpLoad %v3float %97
+        %100 = OpAccessChain %_ptr_Uniform_v3float %render_params %uint_2
+        %101 = OpLoad %v3float %100
+        %102 = OpCompositeConstruct %mat2v3float %98 %101
+        %103 = OpCompositeExtract %v2float %in 2
+        %104 = OpMatrixTimesVector %v3float %102 %103
+               OpStore %quad_pos %104
+        %108 = OpCompositeExtract %v3float %in 0
+        %109 = OpLoad %v3float %quad_pos
+        %111 = OpVectorTimesScalar %v3float %109 %float_0_00999999978
+        %112 = OpFAdd %v3float %108 %111
+               OpStore %position %112
+        %118 = OpAccessChain %_ptr_Function_v4float %out %uint_0
+        %120 = OpAccessChain %_ptr_Uniform_mat4v4float %render_params %uint_0
+        %121 = OpLoad %mat4v4float %120
+        %122 = OpLoad %v3float %position
+        %123 = OpCompositeExtract %float %122 0
+        %124 = OpCompositeExtract %float %122 1
+        %125 = OpCompositeExtract %float %122 2
+        %127 = OpCompositeConstruct %v4float %123 %124 %125 %float_1
+        %128 = OpMatrixTimesVector %v4float %121 %127
+               OpStore %118 %128
+        %129 = OpAccessChain %_ptr_Function_v4float %out %uint_1
+        %130 = OpCompositeExtract %v4float %in 1
+               OpStore %129 %130
+        %132 = OpAccessChain %_ptr_Function_v2float %out %uint_2
+        %133 = OpCompositeExtract %v2float %in 2
+               OpStore %132 %133
+        %134 = OpLoad %VertexOutput %out
+               OpReturnValue %134
+               OpFunctionEnd
+    %vs_main = OpFunction %void None %135
+        %138 = OpLabel
+        %140 = OpLoad %v3float %position_1
+        %141 = OpLoad %v4float %color_1
+        %142 = OpLoad %v2float %quad_pos_1
+        %143 = OpCompositeConstruct %VertexInput %140 %141 %142
+        %139 = OpFunctionCall %VertexOutput %vs_main_inner %143
+        %144 = OpCompositeExtract %v4float %139 0
+               OpStore %position_2 %144
+        %145 = OpCompositeExtract %v4float %139 1
+               OpStore %color_2 %145
+        %146 = OpCompositeExtract %v2float %139 2
+               OpStore %quad_pos_2 %146
+               OpStore %vertex_point_size %float_1
+               OpReturn
+               OpFunctionEnd
+%fs_main_inner = OpFunction %v4float None %147
+       %in_0 = OpFunctionParameter %VertexOutput
+        %150 = OpLabel
+      %color = OpVariable %_ptr_Function_v4float Function %13
+        %151 = OpCompositeExtract %v4float %in_0 1
+               OpStore %color %151
+        %155 = OpAccessChain %_ptr_Function_float %color %uint_3
+        %156 = OpAccessChain %_ptr_Function_float %color %uint_3
+        %157 = OpLoad %float %156
+        %160 = OpCompositeExtract %v2float %in_0 2
+        %159 = OpExtInst %float %67 Length %160
+        %161 = OpFSub %float %float_1 %159
+        %158 = OpExtInst %float %67 NMax %161 %float_0
+        %163 = OpFMul %float %157 %158
+               OpStore %155 %163
+        %164 = OpLoad %v4float %color
+               OpReturnValue %164
+               OpFunctionEnd
+    %fs_main = OpFunction %void None %135
+        %166 = OpLabel
+        %168 = OpLoad %v4float %position_3
+        %169 = OpLoad %v4float %color_3
+        %170 = OpLoad %v2float %quad_pos_3
+        %171 = OpCompositeConstruct %VertexOutput %168 %169 %170
+        %167 = OpFunctionCall %v4float %fs_main_inner %171
+               OpStore %value_1 %167
+               OpReturn
+               OpFunctionEnd
+%simulate_inner = OpFunction %void None %172
+%GlobalInvocationID = OpFunctionParameter %v3uint
+        %175 = OpLabel
+   %particle = OpVariable %_ptr_Function_Particle Function %194
+      %coord = OpVariable %_ptr_Function_v2int Function %235
+      %level = OpVariable %_ptr_Function_int Function %242
+        %270 = OpVariable %_ptr_Function_v2int Function %235
+        %297 = OpVariable %_ptr_Function_v2float Function %17
+        %177 = OpAccessChain %_ptr_Uniform_v4float %sim_params %uint_1
+        %178 = OpLoad %v4float %177
+        %179 = OpVectorShuffle %v2float %178 %178 0 1
+        %182 = OpVectorShuffle %v2uint %GlobalInvocationID %GlobalInvocationID 0 1
+        %180 = OpConvertUToF %v2float %182
+        %183 = OpFAdd %v2float %179 %180
+        %184 = OpAccessChain %_ptr_Uniform_v4float %sim_params %uint_1
+        %185 = OpLoad %v4float %184
+        %186 = OpVectorShuffle %v2float %185 %185 2 3
+        %187 = OpFMul %v2float %183 %186
+               OpStore %rand_seed %187
+        %188 = OpCompositeExtract %uint %GlobalInvocationID 0
+        %190 = OpAccessChain %_ptr_StorageBuffer_Particle %data %uint_0 %188
+        %191 = OpLoad %Particle %190
+               OpStore %particle %191
+        %195 = OpAccessChain %_ptr_Function_float %particle %uint_3 %uint_2
+        %196 = OpAccessChain %_ptr_Function_float %particle %uint_3 %uint_2
+        %197 = OpLoad %float %196
+        %199 = OpAccessChain %_ptr_Uniform_float %sim_params %uint_0
+        %200 = OpLoad %float %199
+        %202 = OpFMul %float %200 %float_0_5
+        %203 = OpFSub %float %197 %202
+               OpStore %195 %203
+        %204 = OpAccessChain %_ptr_Function_v3float %particle %uint_0
+        %205 = OpAccessChain %_ptr_Function_v3float %particle %uint_0
+        %206 = OpLoad %v3float %205
+        %207 = OpAccessChain %_ptr_Uniform_float %sim_params %uint_0
+        %208 = OpLoad %float %207
+        %209 = OpAccessChain %_ptr_Function_v3float %particle %uint_3
+        %210 = OpLoad %v3float %209
+        %211 = OpVectorTimesScalar %v3float %210 %208
+        %212 = OpFAdd %v3float %206 %211
+               OpStore %204 %212
+        %213 = OpAccessChain %_ptr_Function_float %particle %uint_1
+        %214 = OpAccessChain %_ptr_Function_float %particle %uint_1
+        %215 = OpLoad %float %214
+        %216 = OpAccessChain %_ptr_Uniform_float %sim_params %uint_0
+        %217 = OpLoad %float %216
+        %218 = OpFSub %float %215 %217
+               OpStore %213 %218
+        %219 = OpAccessChain %_ptr_Function_float %particle %uint_2 %uint_3
+        %221 = OpAccessChain %_ptr_Function_float %particle %uint_1
+        %222 = OpLoad %float %221
+        %220 = OpExtInst %float %67 SmoothStep %float_0 %float_0_5 %222
+               OpStore %219 %220
+        %223 = OpAccessChain %_ptr_Function_float %particle %uint_1
+        %224 = OpLoad %float %223
+        %225 = OpFOrdLessThan %bool %224 %float_0
+               OpSelectionMerge %227 None
+               OpBranchConditional %225 %228 %227
+        %228 = OpLabel
+               OpStore %coord %232
+        %237 = OpLoad %47 %texture
+        %236 = OpImageQueryLevels %int %237
+        %239 = OpISub %int %236 %int_1
+               OpStore %level %239
+               OpBranch %243
+        %243 = OpLabel
+               OpLoopMerge %244 %245 None
+               OpBranch %246
+        %246 = OpLabel
+        %248 = OpLoad %int %level
+        %249 = OpSGreaterThan %bool %248 %int_0
+        %247 = OpLogicalNot %bool %249
+               OpSelectionMerge %250 None
+               OpBranchConditional %247 %251 %250
+        %251 = OpLabel
+               OpBranch %244
+        %250 = OpLabel
+        %253 = OpLoad %47 %texture
+        %254 = OpLoad %v2int %coord
+        %255 = OpLoad %int %level
+        %252 = OpImageFetch %v4float %253 %254 Lod %255
+        %256 = OpFunctionCall %float %rand
+        %257 = OpCompositeConstruct %v4float %256 %256 %256 %256
+        %258 = OpVectorShuffle %v3float %252 %252 0 1 2
+        %259 = OpCompositeExtract %float %258 0
+        %260 = OpCompositeExtract %float %258 1
+        %261 = OpCompositeExtract %float %258 2
+        %262 = OpCompositeConstruct %v4float %float_0 %259 %260 %261
+        %263 = OpFOrdGreaterThanEqual %v4bool %257 %262
+        %265 = OpFOrdLessThan %v4bool %257 %252
+        %266 = OpLogicalAnd %v4bool %263 %265
+        %267 = OpLoad %v2int %coord
+        %271 = OpCompositeConstruct %v2int %int_2 %int_2
+        %269 = OpIMul %v2int %267 %271
+               OpStore %coord %269
+        %272 = OpAccessChain %_ptr_Function_int %coord %uint_0
+        %273 = OpAccessChain %_ptr_Function_int %coord %uint_0
+        %274 = OpLoad %int %273
+        %278 = OpVectorShuffle %v2bool %266 %266 1 3
+        %276 = OpAny %bool %278
+        %275 = OpSelect %int %276 %int_1 %int_0
+        %279 = OpIAdd %int %274 %275
+               OpStore %272 %279
+        %280 = OpAccessChain %_ptr_Function_int %coord %uint_1
+        %281 = OpAccessChain %_ptr_Function_int %coord %uint_1
+        %282 = OpLoad %int %281
+        %285 = OpVectorShuffle %v2bool %266 %266 2 3
+        %284 = OpAny %bool %285
+        %283 = OpSelect %int %284 %int_1 %int_0
+        %286 = OpIAdd %int %282 %283
+               OpStore %280 %286
+               OpBranch %245
+        %245 = OpLabel
+        %287 = OpLoad %int %level
+        %288 = OpISub %int %287 %int_1
+               OpStore %level %288
+               OpBranch %243
+        %244 = OpLabel
+        %290 = OpLoad %v2int %coord
+        %289 = OpConvertSToF %v2float %290
+        %293 = OpLoad %47 %texture
+        %292 = OpImageQuerySizeLod %v2int %293 %int_0
+        %291 = OpConvertSToF %v2float %292
+        %294 = OpFDiv %v2float %289 %291
+        %295 = OpAccessChain %_ptr_Function_v3float %particle %uint_0
+        %298 = OpCompositeConstruct %v2float %float_0_5 %float_0_5
+        %296 = OpFSub %v2float %294 %298
+        %300 = OpVectorTimesScalar %v2float %296 %float_3
+        %303 = OpFMul %v2float %300 %302
+        %304 = OpCompositeExtract %float %303 0
+        %305 = OpCompositeExtract %float %303 1
+        %306 = OpCompositeConstruct %v3float %304 %305 %float_0
+               OpStore %295 %306
+        %307 = OpAccessChain %_ptr_Function_v4float %particle %uint_2
+        %309 = OpLoad %47 %texture
+        %310 = OpLoad %v2int %coord
+        %308 = OpImageFetch %v4float %309 %310 Lod %int_0
+               OpStore %307 %308
+        %311 = OpAccessChain %_ptr_Function_float %particle %uint_3 %uint_0
+        %312 = OpFunctionCall %float %rand
+        %313 = OpFSub %float %312 %float_0_5
+        %315 = OpFMul %float %313 %float_0_100000001
+               OpStore %311 %315
+        %316 = OpAccessChain %_ptr_Function_float %particle %uint_3 %uint_1
+        %317 = OpFunctionCall %float %rand
+        %318 = OpFSub %float %317 %float_0_5
+        %319 = OpFMul %float %318 %float_0_100000001
+               OpStore %316 %319
+        %320 = OpAccessChain %_ptr_Function_float %particle %uint_3 %uint_2
+        %321 = OpFunctionCall %float %rand
+        %323 = OpFMul %float %321 %float_0_300000012
+               OpStore %320 %323
+        %324 = OpAccessChain %_ptr_Function_float %particle %uint_1
+        %325 = OpFunctionCall %float %rand
+        %327 = OpFMul %float %325 %float_2
+        %328 = OpFAdd %float %float_0_5 %327
+               OpStore %324 %328
+               OpBranch %227
+        %227 = OpLabel
+        %329 = OpAccessChain %_ptr_StorageBuffer_Particle %data %uint_0 %188
+        %330 = OpLoad %Particle %particle
+               OpStore %329 %330
+               OpReturn
+               OpFunctionEnd
+   %simulate = OpFunction %void None %135
+        %332 = OpLabel
+        %334 = OpLoad %v3uint %GlobalInvocationID_1
+        %333 = OpFunctionCall %void %simulate_inner %334
+               OpReturn
+               OpFunctionEnd
+%import_level_inner = OpFunction %void None %172
+    %coord_0 = OpFunctionParameter %v3uint
+        %337 = OpLabel
+        %339 = OpCompositeExtract %uint %coord_0 0
+        %340 = OpCompositeExtract %uint %coord_0 1
+        %342 = OpAccessChain %_ptr_Uniform_uint %ubo %uint_0
+        %343 = OpLoad %uint %342
+        %344 = OpIMul %uint %340 %343
+        %345 = OpIAdd %uint %339 %344
+        %347 = OpAccessChain %_ptr_StorageBuffer_float %buf_out %uint_0 %345
+        %349 = OpLoad %47 %tex_in
+        %351 = OpVectorShuffle %v2uint %coord_0 %coord_0 0 1
+        %350 = OpBitcast %v2int %351
+        %348 = OpImageFetch %v4float %349 %350 Lod %int_0
+        %352 = OpCompositeExtract %float %348 3
+               OpStore %347 %352
+               OpReturn
+               OpFunctionEnd
+%import_level = OpFunction %void None %135
+        %354 = OpLabel
+        %356 = OpLoad %v3uint %coord_1
+        %355 = OpFunctionCall %void %import_level_inner %356
+               OpReturn
+               OpFunctionEnd
+%export_level_inner = OpFunction %void None %172
+    %coord_3 = OpFunctionParameter %v3uint
+        %359 = OpLabel
+        %413 = OpVariable %_ptr_Function_v4float Function %13
+        %361 = OpVectorShuffle %v2uint %coord_3 %coord_3 0 1
+        %364 = OpLoad %59 %tex_out
+        %363 = OpImageQuerySize %v2int %364
+        %362 = OpBitcast %v2uint %363
+        %365 = OpULessThan %v2bool %361 %362
+        %360 = OpAll %bool %365
+               OpSelectionMerge %366 None
+               OpBranchConditional %360 %367 %366
+        %367 = OpLabel
+        %368 = OpCompositeExtract %uint %coord_3 0
+        %369 = OpCompositeExtract %uint %coord_3 1
+        %370 = OpAccessChain %_ptr_Uniform_uint %ubo %uint_0
+        %371 = OpLoad %uint %370
+        %372 = OpIMul %uint %369 %371
+        %373 = OpIAdd %uint %368 %372
+        %374 = OpCompositeExtract %uint %coord_3 0
+        %375 = OpIMul %uint %374 %uint_2
+        %376 = OpCompositeExtract %uint %coord_3 1
+        %377 = OpIMul %uint %376 %uint_2
+        %378 = OpAccessChain %_ptr_Uniform_uint %ubo %uint_0
+        %379 = OpLoad %uint %378
+        %380 = OpIMul %uint %377 %379
+        %381 = OpIAdd %uint %375 %380
+        %382 = OpIAdd %uint %381 %uint_0
+        %383 = OpAccessChain %_ptr_StorageBuffer_float %buf_in %uint_0 %382
+        %384 = OpLoad %float %383
+        %385 = OpIAdd %uint %381 %uint_1
+        %386 = OpAccessChain %_ptr_StorageBuffer_float %buf_in %uint_0 %385
+        %387 = OpLoad %float %386
+        %388 = OpIAdd %uint %381 %uint_0
+        %389 = OpAccessChain %_ptr_Uniform_uint %ubo %uint_0
+        %390 = OpLoad %uint %389
+        %391 = OpIAdd %uint %388 %390
+        %392 = OpAccessChain %_ptr_StorageBuffer_float %buf_in %uint_0 %391
+        %393 = OpLoad %float %392
+        %394 = OpIAdd %uint %381 %uint_1
+        %395 = OpAccessChain %_ptr_Uniform_uint %ubo %uint_0
+        %396 = OpLoad %uint %395
+        %397 = OpIAdd %uint %394 %396
+        %398 = OpAccessChain %_ptr_StorageBuffer_float %buf_in %uint_0 %397
+        %399 = OpLoad %float %398
+        %401 = OpCompositeConstruct %v4float %384 %387 %393 %399
+        %400 = OpDot %float %401 %402
+        %403 = OpAccessChain %_ptr_StorageBuffer_float %buf_out %uint_0 %373
+        %405 = OpFDiv %float %400 %float_4
+               OpStore %403 %405
+        %406 = OpFAdd %float %384 %387
+        %407 = OpFAdd %float %384 %387
+        %408 = OpFAdd %float %407 %393
+        %409 = OpCompositeConstruct %v4float %384 %406 %408 %400
+        %410 = OpExtInst %float %67 NMax %400 %float_9_99999975en05
+        %414 = OpCompositeConstruct %v4float %410 %410 %410 %410
+        %412 = OpFDiv %v4float %409 %414
+        %416 = OpLoad %59 %tex_out
+        %418 = OpVectorShuffle %v2uint %coord_3 %coord_3 0 1
+        %417 = OpBitcast %v2int %418
+               OpImageWrite %416 %417 %412
+               OpBranch %366
+        %366 = OpLabel
+               OpReturn
+               OpFunctionEnd
+%export_level = OpFunction %void None %135
+        %420 = OpLabel
+        %422 = OpLoad %v3uint %coord_2
+        %421 = OpFunctionCall %void %export_level_inner %422
+               OpReturn
+               OpFunctionEnd
diff --git a/test/benchmark/particles.wgsl.expected.wgsl b/test/benchmark/particles.wgsl.expected.wgsl
new file mode 100644
index 0000000..856f496
--- /dev/null
+++ b/test/benchmark/particles.wgsl.expected.wgsl
@@ -0,0 +1,144 @@
+var<private> rand_seed : vec2<f32>;
+
+fn rand() -> f32 {
+  rand_seed.x = fract((cos(dot(rand_seed, vec2<f32>(23.140779495, 232.616897583))) * 136.816802979));
+  rand_seed.y = fract((cos(dot(rand_seed, vec2<f32>(54.478565216, 345.841522217))) * 534.764526367));
+  return rand_seed.y;
+}
+
+struct RenderParams {
+  modelViewProjectionMatrix : mat4x4<f32>;
+  right : vec3<f32>;
+  up : vec3<f32>;
+};
+
+[[binding(0), group(0)]] var<uniform> render_params : RenderParams;
+
+struct VertexInput {
+  [[location(0)]]
+  position : vec3<f32>;
+  [[location(1)]]
+  color : vec4<f32>;
+  [[location(2)]]
+  quad_pos : vec2<f32>;
+};
+
+struct VertexOutput {
+  [[builtin(position)]]
+  position : vec4<f32>;
+  [[location(0)]]
+  color : vec4<f32>;
+  [[location(1)]]
+  quad_pos : vec2<f32>;
+};
+
+[[stage(vertex)]]
+fn vs_main(in : VertexInput) -> VertexOutput {
+  var quad_pos = (mat2x3<f32>(render_params.right, render_params.up) * in.quad_pos);
+  var position = (in.position + (quad_pos * 0.01));
+  var out : VertexOutput;
+  out.position = (render_params.modelViewProjectionMatrix * vec4<f32>(position, 1.0));
+  out.color = in.color;
+  out.quad_pos = in.quad_pos;
+  return out;
+}
+
+[[stage(fragment)]]
+fn fs_main(in : VertexOutput) -> [[location(0)]] vec4<f32> {
+  var color = in.color;
+  color.a = (color.a * max((1.0 - length(in.quad_pos)), 0.0));
+  return color;
+}
+
+struct SimulationParams {
+  deltaTime : f32;
+  seed : vec4<f32>;
+};
+
+struct Particle {
+  position : vec3<f32>;
+  lifetime : f32;
+  color : vec4<f32>;
+  velocity : vec3<f32>;
+};
+
+struct Particles {
+  particles : array<Particle>;
+};
+
+[[binding(0), group(0)]] var<uniform> sim_params : SimulationParams;
+
+[[binding(1), group(0)]] var<storage, read_write> data : Particles;
+
+[[binding(2), group(0)]] var texture : texture_2d<f32>;
+
+[[stage(compute), workgroup_size(64)]]
+fn simulate([[builtin(global_invocation_id)]] GlobalInvocationID : vec3<u32>) {
+  rand_seed = ((sim_params.seed.xy + vec2<f32>(GlobalInvocationID.xy)) * sim_params.seed.zw);
+  let idx = GlobalInvocationID.x;
+  var particle = data.particles[idx];
+  particle.velocity.z = (particle.velocity.z - (sim_params.deltaTime * 0.5));
+  particle.position = (particle.position + (sim_params.deltaTime * particle.velocity));
+  particle.lifetime = (particle.lifetime - sim_params.deltaTime);
+  particle.color.a = smoothStep(0.0, 0.5, particle.lifetime);
+  if ((particle.lifetime < 0.0)) {
+    var coord = vec2<i32>(0, 0);
+    for(var level = (textureNumLevels(texture) - 1); (level > 0); level = (level - 1)) {
+      let probabilites = textureLoad(texture, coord, level);
+      let value = vec4<f32>(rand());
+      let mask = ((value >= vec4<f32>(0.0, probabilites.xyz)) & (value < probabilites));
+      coord = (coord * 2);
+      coord.x = (coord.x + select(0, 1, any(mask.yw)));
+      coord.y = (coord.y + select(0, 1, any(mask.zw)));
+    }
+    let uv = (vec2<f32>(coord) / vec2<f32>(textureDimensions(texture)));
+    particle.position = vec3<f32>((((uv - 0.5) * 3.0) * vec2<f32>(1.0, -1.0)), 0.0);
+    particle.color = textureLoad(texture, coord, 0);
+    particle.velocity.x = ((rand() - 0.5) * 0.100000001);
+    particle.velocity.y = ((rand() - 0.5) * 0.100000001);
+    particle.velocity.z = (rand() * 0.300000012);
+    particle.lifetime = (0.5 + (rand() * 2.0));
+  }
+  data.particles[idx] = particle;
+}
+
+struct UBO {
+  width : u32;
+};
+
+struct Buffer {
+  weights : array<f32>;
+};
+
+[[binding(3), group(0)]] var<uniform> ubo : UBO;
+
+[[binding(4), group(0)]] var<storage, read> buf_in : Buffer;
+
+[[binding(5), group(0)]] var<storage, read_write> buf_out : Buffer;
+
+[[binding(6), group(0)]] var tex_in : texture_2d<f32>;
+
+[[binding(7), group(0)]] var tex_out : texture_storage_2d<rgba8unorm, write>;
+
+[[stage(compute), workgroup_size(64)]]
+fn import_level([[builtin(global_invocation_id)]] coord : vec3<u32>) {
+  _ = &(buf_in);
+  let offset = (coord.x + (coord.y * ubo.width));
+  buf_out.weights[offset] = textureLoad(tex_in, vec2<i32>(coord.xy), 0).w;
+}
+
+[[stage(compute), workgroup_size(64)]]
+fn export_level([[builtin(global_invocation_id)]] coord : vec3<u32>) {
+  if (all((coord.xy < vec2<u32>(textureDimensions(tex_out))))) {
+    let dst_offset = (coord.x + (coord.y * ubo.width));
+    let src_offset = ((coord.x * 2u) + ((coord.y * 2u) * ubo.width));
+    let a = buf_in.weights[(src_offset + 0u)];
+    let b = buf_in.weights[(src_offset + 1u)];
+    let c = buf_in.weights[((src_offset + 0u) + ubo.width)];
+    let d = buf_in.weights[((src_offset + 1u) + ubo.width)];
+    let sum = dot(vec4<f32>(a, b, c, d), vec4<f32>(1.0));
+    buf_out.weights[dst_offset] = (sum / 4.0);
+    let probabilities = (vec4<f32>(a, (a + b), ((a + b) + c), sum) / max(sum, 0.0001));
+    textureStore(tex_out, vec2<i32>(coord.xy), probabilities);
+  }
+}
diff --git a/test/benchmark/simple_compute.wgsl b/test/benchmark/simple_compute.wgsl
new file mode 100644
index 0000000..f5b883a
--- /dev/null
+++ b/test/benchmark/simple_compute.wgsl
@@ -0,0 +1,10 @@
+struct SB {
+  data : array<i32>;
+};
+
+[[group(0), binding(0)]] var<storage, read_write> buffer : SB;
+
+[[stage(compute), workgroup_size(1, 2, 3)]]
+fn main([[builtin(global_invocation_id)]] id : vec3<u32>) {
+  buffer.data[id.x] = buffer.data[id.x] + 1;
+}
diff --git a/test/benchmark/simple_compute.wgsl.expected.hlsl b/test/benchmark/simple_compute.wgsl.expected.hlsl
new file mode 100644
index 0000000..92d72b0
--- /dev/null
+++ b/test/benchmark/simple_compute.wgsl.expected.hlsl
@@ -0,0 +1,15 @@
+RWByteAddressBuffer buffer : register(u0, space0);
+
+struct tint_symbol_1 {
+  uint3 id : SV_DispatchThreadID;
+};
+
+void main_inner(uint3 id) {
+  buffer.Store((4u * id.x), asuint((asint(buffer.Load((4u * id.x))) + 1)));
+}
+
+[numthreads(1, 2, 3)]
+void main(tint_symbol_1 tint_symbol) {
+  main_inner(tint_symbol.id);
+  return;
+}
diff --git a/test/benchmark/simple_compute.wgsl.expected.msl b/test/benchmark/simple_compute.wgsl.expected.msl
new file mode 100644
index 0000000..a2119e6
--- /dev/null
+++ b/test/benchmark/simple_compute.wgsl.expected.msl
@@ -0,0 +1,16 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct SB {
+  /* 0x0000 */ int data[1];
+};
+
+void tint_symbol_1_inner(uint3 id, device SB* const tint_symbol_2) {
+  (*(tint_symbol_2)).data[id[0]] = as_type<int>((as_type<uint>((*(tint_symbol_2)).data[id[0]]) + as_type<uint>(1)));
+}
+
+kernel void tint_symbol_1(device SB* tint_symbol_3 [[buffer(0)]], uint3 id [[thread_position_in_grid]]) {
+  tint_symbol_1_inner(id, tint_symbol_3);
+  return;
+}
+
diff --git a/test/benchmark/simple_compute.wgsl.expected.spvasm b/test/benchmark/simple_compute.wgsl.expected.spvasm
new file mode 100644
index 0000000..35c2109
--- /dev/null
+++ b/test/benchmark/simple_compute.wgsl.expected.spvasm
@@ -0,0 +1,55 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 29
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main" %id_1
+               OpExecutionMode %main LocalSize 1 2 3
+               OpName %id_1 "id_1"
+               OpName %SB "SB"
+               OpMemberName %SB 0 "data"
+               OpName %buffer "buffer"
+               OpName %main_inner "main_inner"
+               OpName %id "id"
+               OpName %main "main"
+               OpDecorate %id_1 BuiltIn GlobalInvocationId
+               OpDecorate %SB Block
+               OpMemberDecorate %SB 0 Offset 0
+               OpDecorate %_runtimearr_int ArrayStride 4
+               OpDecorate %buffer DescriptorSet 0
+               OpDecorate %buffer Binding 0
+       %uint = OpTypeInt 32 0
+     %v3uint = OpTypeVector %uint 3
+%_ptr_Input_v3uint = OpTypePointer Input %v3uint
+       %id_1 = OpVariable %_ptr_Input_v3uint Input
+        %int = OpTypeInt 32 1
+%_runtimearr_int = OpTypeRuntimeArray %int
+         %SB = OpTypeStruct %_runtimearr_int
+%_ptr_StorageBuffer_SB = OpTypePointer StorageBuffer %SB
+     %buffer = OpVariable %_ptr_StorageBuffer_SB StorageBuffer
+       %void = OpTypeVoid
+         %10 = OpTypeFunction %void %v3uint
+     %uint_0 = OpConstant %uint 0
+%_ptr_StorageBuffer_int = OpTypePointer StorageBuffer %int
+      %int_1 = OpConstant %int 1
+         %24 = OpTypeFunction %void
+ %main_inner = OpFunction %void None %10
+         %id = OpFunctionParameter %v3uint
+         %14 = OpLabel
+         %16 = OpCompositeExtract %uint %id 0
+         %18 = OpAccessChain %_ptr_StorageBuffer_int %buffer %uint_0 %16
+         %19 = OpCompositeExtract %uint %id 0
+         %20 = OpAccessChain %_ptr_StorageBuffer_int %buffer %uint_0 %19
+         %21 = OpLoad %int %20
+         %23 = OpIAdd %int %21 %int_1
+               OpStore %18 %23
+               OpReturn
+               OpFunctionEnd
+       %main = OpFunction %void None %24
+         %26 = OpLabel
+         %28 = OpLoad %v3uint %id_1
+         %27 = OpFunctionCall %void %main_inner %28
+               OpReturn
+               OpFunctionEnd
diff --git a/test/benchmark/simple_compute.wgsl.expected.wgsl b/test/benchmark/simple_compute.wgsl.expected.wgsl
new file mode 100644
index 0000000..ac98410
--- /dev/null
+++ b/test/benchmark/simple_compute.wgsl.expected.wgsl
@@ -0,0 +1,10 @@
+struct SB {
+  data : array<i32>;
+};
+
+[[group(0), binding(0)]] var<storage, read_write> buffer : SB;
+
+[[stage(compute), workgroup_size(1, 2, 3)]]
+fn main([[builtin(global_invocation_id)]] id : vec3<u32>) {
+  buffer.data[id.x] = (buffer.data[id.x] + 1);
+}
diff --git a/test/benchmark/simple_fragment.wgsl b/test/benchmark/simple_fragment.wgsl
new file mode 100644
index 0000000..bb5ed94
--- /dev/null
+++ b/test/benchmark/simple_fragment.wgsl
@@ -0,0 +1,12 @@
+struct Input {
+  [[location(0)]] color: vec4<f32>;
+};
+
+struct Output {
+  [[location(0)]] color: vec4<f32>;
+};
+
+[[stage(fragment)]]
+fn main(in : Input) -> Output {
+  return Output(in.color);
+}
diff --git a/test/benchmark/simple_fragment.wgsl.expected.hlsl b/test/benchmark/simple_fragment.wgsl.expected.hlsl
new file mode 100644
index 0000000..4a462c8
--- /dev/null
+++ b/test/benchmark/simple_fragment.wgsl.expected.hlsl
@@ -0,0 +1,25 @@
+struct Input {
+  float4 color;
+};
+struct Output {
+  float4 color;
+};
+struct tint_symbol_2 {
+  float4 color : TEXCOORD0;
+};
+struct tint_symbol_3 {
+  float4 color : SV_Target0;
+};
+
+Output main_inner(Input tint_symbol) {
+  const Output tint_symbol_4 = {tint_symbol.color};
+  return tint_symbol_4;
+}
+
+tint_symbol_3 main(tint_symbol_2 tint_symbol_1) {
+  const Input tint_symbol_5 = {tint_symbol_1.color};
+  const Output inner_result = main_inner(tint_symbol_5);
+  tint_symbol_3 wrapper_result = (tint_symbol_3)0;
+  wrapper_result.color = inner_result.color;
+  return wrapper_result;
+}
diff --git a/test/benchmark/simple_fragment.wgsl.expected.msl b/test/benchmark/simple_fragment.wgsl.expected.msl
new file mode 100644
index 0000000..0bc1116
--- /dev/null
+++ b/test/benchmark/simple_fragment.wgsl.expected.msl
@@ -0,0 +1,29 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct Input {
+  float4 color;
+};
+struct Output {
+  float4 color;
+};
+struct tint_symbol_2 {
+  float4 color [[user(locn0)]];
+};
+struct tint_symbol_3 {
+  float4 color [[color(0)]];
+};
+
+Output tint_symbol_inner(Input in) {
+  Output const tint_symbol_4 = {.color=in.color};
+  return tint_symbol_4;
+}
+
+fragment tint_symbol_3 tint_symbol(tint_symbol_2 tint_symbol_1 [[stage_in]]) {
+  Input const tint_symbol_5 = {.color=tint_symbol_1.color};
+  Output const inner_result = tint_symbol_inner(tint_symbol_5);
+  tint_symbol_3 wrapper_result = {};
+  wrapper_result.color = inner_result.color;
+  return wrapper_result;
+}
+
diff --git a/test/benchmark/simple_fragment.wgsl.expected.spvasm b/test/benchmark/simple_fragment.wgsl.expected.spvasm
new file mode 100644
index 0000000..531ed06
--- /dev/null
+++ b/test/benchmark/simple_fragment.wgsl.expected.spvasm
@@ -0,0 +1,50 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 24
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main" %color_1 %color_2
+               OpExecutionMode %main OriginUpperLeft
+               OpName %color_1 "color_1"
+               OpName %color_2 "color_2"
+               OpName %Output "Output"
+               OpMemberName %Output 0 "color"
+               OpName %Input "Input"
+               OpMemberName %Input 0 "color"
+               OpName %main_inner "main_inner"
+               OpName %in "in"
+               OpName %main "main"
+               OpDecorate %color_1 Location 0
+               OpDecorate %color_2 Location 0
+               OpMemberDecorate %Output 0 Offset 0
+               OpMemberDecorate %Input 0 Offset 0
+      %float = OpTypeFloat 32
+    %v4float = OpTypeVector %float 4
+%_ptr_Input_v4float = OpTypePointer Input %v4float
+    %color_1 = OpVariable %_ptr_Input_v4float Input
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+          %7 = OpConstantNull %v4float
+    %color_2 = OpVariable %_ptr_Output_v4float Output %7
+     %Output = OpTypeStruct %v4float
+      %Input = OpTypeStruct %v4float
+          %8 = OpTypeFunction %Output %Input
+       %void = OpTypeVoid
+         %16 = OpTypeFunction %void
+ %main_inner = OpFunction %Output None %8
+         %in = OpFunctionParameter %Input
+         %13 = OpLabel
+         %14 = OpCompositeExtract %v4float %in 0
+         %15 = OpCompositeConstruct %Output %14
+               OpReturnValue %15
+               OpFunctionEnd
+       %main = OpFunction %void None %16
+         %19 = OpLabel
+         %21 = OpLoad %v4float %color_1
+         %22 = OpCompositeConstruct %Input %21
+         %20 = OpFunctionCall %Output %main_inner %22
+         %23 = OpCompositeExtract %v4float %20 0
+               OpStore %color_2 %23
+               OpReturn
+               OpFunctionEnd
diff --git a/test/benchmark/simple_fragment.wgsl.expected.wgsl b/test/benchmark/simple_fragment.wgsl.expected.wgsl
new file mode 100644
index 0000000..1f4225e
--- /dev/null
+++ b/test/benchmark/simple_fragment.wgsl.expected.wgsl
@@ -0,0 +1,14 @@
+struct Input {
+  [[location(0)]]
+  color : vec4<f32>;
+};
+
+struct Output {
+  [[location(0)]]
+  color : vec4<f32>;
+};
+
+[[stage(fragment)]]
+fn main(in : Input) -> Output {
+  return Output(in.color);
+}
diff --git a/test/benchmark/simple_vertex.wgsl b/test/benchmark/simple_vertex.wgsl
new file mode 100644
index 0000000..8f9a07b
--- /dev/null
+++ b/test/benchmark/simple_vertex.wgsl
@@ -0,0 +1,12 @@
+struct Input {
+  [[location(0)]] position: vec4<f32>;
+};
+
+struct Output {
+  [[builtin(position)]] position : vec4<f32>;
+};
+
+[[stage(vertex)]]
+fn main(in : Input) -> Output {
+  return Output(in.position);
+}
diff --git a/test/benchmark/simple_vertex.wgsl.expected.hlsl b/test/benchmark/simple_vertex.wgsl.expected.hlsl
new file mode 100644
index 0000000..03b0d54
--- /dev/null
+++ b/test/benchmark/simple_vertex.wgsl.expected.hlsl
@@ -0,0 +1,25 @@
+struct Input {
+  float4 position;
+};
+struct Output {
+  float4 position;
+};
+struct tint_symbol_2 {
+  float4 position : TEXCOORD0;
+};
+struct tint_symbol_3 {
+  float4 position : SV_Position;
+};
+
+Output main_inner(Input tint_symbol) {
+  const Output tint_symbol_4 = {tint_symbol.position};
+  return tint_symbol_4;
+}
+
+tint_symbol_3 main(tint_symbol_2 tint_symbol_1) {
+  const Input tint_symbol_5 = {tint_symbol_1.position};
+  const Output inner_result = main_inner(tint_symbol_5);
+  tint_symbol_3 wrapper_result = (tint_symbol_3)0;
+  wrapper_result.position = inner_result.position;
+  return wrapper_result;
+}
diff --git a/test/benchmark/simple_vertex.wgsl.expected.msl b/test/benchmark/simple_vertex.wgsl.expected.msl
new file mode 100644
index 0000000..3c18b5f
--- /dev/null
+++ b/test/benchmark/simple_vertex.wgsl.expected.msl
@@ -0,0 +1,29 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct Input {
+  float4 position;
+};
+struct Output {
+  float4 position;
+};
+struct tint_symbol_2 {
+  float4 position [[attribute(0)]];
+};
+struct tint_symbol_3 {
+  float4 position [[position]];
+};
+
+Output tint_symbol_inner(Input in) {
+  Output const tint_symbol_4 = {.position=in.position};
+  return tint_symbol_4;
+}
+
+vertex tint_symbol_3 tint_symbol(tint_symbol_2 tint_symbol_1 [[stage_in]]) {
+  Input const tint_symbol_5 = {.position=tint_symbol_1.position};
+  Output const inner_result = tint_symbol_inner(tint_symbol_5);
+  tint_symbol_3 wrapper_result = {};
+  wrapper_result.position = inner_result.position;
+  return wrapper_result;
+}
+
diff --git a/test/benchmark/simple_vertex.wgsl.expected.spvasm b/test/benchmark/simple_vertex.wgsl.expected.spvasm
new file mode 100644
index 0000000..6490ac0
--- /dev/null
+++ b/test/benchmark/simple_vertex.wgsl.expected.spvasm
@@ -0,0 +1,56 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 28
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Vertex %main "main" %position_1 %position_2 %vertex_point_size
+               OpName %position_1 "position_1"
+               OpName %position_2 "position_2"
+               OpName %vertex_point_size "vertex_point_size"
+               OpName %Output "Output"
+               OpMemberName %Output 0 "position"
+               OpName %Input "Input"
+               OpMemberName %Input 0 "position"
+               OpName %main_inner "main_inner"
+               OpName %in "in"
+               OpName %main "main"
+               OpDecorate %position_1 Location 0
+               OpDecorate %position_2 BuiltIn Position
+               OpDecorate %vertex_point_size BuiltIn PointSize
+               OpMemberDecorate %Output 0 Offset 0
+               OpMemberDecorate %Input 0 Offset 0
+      %float = OpTypeFloat 32
+    %v4float = OpTypeVector %float 4
+%_ptr_Input_v4float = OpTypePointer Input %v4float
+ %position_1 = OpVariable %_ptr_Input_v4float Input
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+          %7 = OpConstantNull %v4float
+ %position_2 = OpVariable %_ptr_Output_v4float Output %7
+%_ptr_Output_float = OpTypePointer Output %float
+         %10 = OpConstantNull %float
+%vertex_point_size = OpVariable %_ptr_Output_float Output %10
+     %Output = OpTypeStruct %v4float
+      %Input = OpTypeStruct %v4float
+         %11 = OpTypeFunction %Output %Input
+       %void = OpTypeVoid
+         %19 = OpTypeFunction %void
+    %float_1 = OpConstant %float 1
+ %main_inner = OpFunction %Output None %11
+         %in = OpFunctionParameter %Input
+         %16 = OpLabel
+         %17 = OpCompositeExtract %v4float %in 0
+         %18 = OpCompositeConstruct %Output %17
+               OpReturnValue %18
+               OpFunctionEnd
+       %main = OpFunction %void None %19
+         %22 = OpLabel
+         %24 = OpLoad %v4float %position_1
+         %25 = OpCompositeConstruct %Input %24
+         %23 = OpFunctionCall %Output %main_inner %25
+         %26 = OpCompositeExtract %v4float %23 0
+               OpStore %position_2 %26
+               OpStore %vertex_point_size %float_1
+               OpReturn
+               OpFunctionEnd
diff --git a/test/benchmark/simple_vertex.wgsl.expected.wgsl b/test/benchmark/simple_vertex.wgsl.expected.wgsl
new file mode 100644
index 0000000..7bc18b1
--- /dev/null
+++ b/test/benchmark/simple_vertex.wgsl.expected.wgsl
@@ -0,0 +1,14 @@
+struct Input {
+  [[location(0)]]
+  position : vec4<f32>;
+};
+
+struct Output {
+  [[builtin(position)]]
+  position : vec4<f32>;
+};
+
+[[stage(vertex)]]
+fn main(in : Input) -> Output {
+  return Output(in.position);
+}
diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt
index f54ed0a..47d6664 100644
--- a/third_party/CMakeLists.txt
+++ b/third_party/CMakeLists.txt
@@ -12,6 +12,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+if (${TINT_BUILD_BENCHMARKS})
+  set(BENCHMARK_ENABLE_TESTING FALSE CACHE BOOL FALSE FORCE)
+  add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/benchmark EXCLUDE_FROM_ALL)
+endif()
+
 if (${TINT_BUILD_TESTS} AND NOT TARGET gmock)
   set(gtest_force_shared_crt ON CACHE BOOL "Controls whether a shared run-time library should be used even when Google Test is built as static library" FORCE)
   add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/googletest EXCLUDE_FROM_ALL)
diff --git a/tools/benchdiff b/tools/benchdiff
new file mode 100755
index 0000000..3cc175f
--- /dev/null
+++ b/tools/benchdiff
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+# Copyright 2022 The Tint 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 -e # Fail on any error.
+
+if [ ! -x "$(which go)" ] ; then
+    echo "error: go needs to be on \$PATH to use $0"
+    exit 1
+fi
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd )"
+ROOT_DIR="$( cd "${SCRIPT_DIR}/.." >/dev/null 2>&1 && pwd )"
+BINARY="${SCRIPT_DIR}/bin/benchdiff"
+
+# Rebuild the binary.
+# Note, go caches build artifacts, so this is quick for repeat calls
+pushd "${SCRIPT_DIR}/src/cmd/benchdiff" > /dev/null
+    go build -o "${BINARY}" main.go
+popd > /dev/null
+
+"${BINARY}" "$@"
diff --git a/tools/src/bench/bench.go b/tools/src/bench/bench.go
new file mode 100644
index 0000000..8bb1d99
--- /dev/null
+++ b/tools/src/bench/bench.go
@@ -0,0 +1,150 @@
+// Copyright 2022 The Tint 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.
+
+// Package bench provides types and methods for parsing Google benchmark results.
+package bench
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// Test holds the results of a single benchmark test.
+type Test struct {
+	Name       string
+	NumTasks   uint
+	NumThreads uint
+	Duration   time.Duration
+	Iterations uint
+}
+
+var testVarRE = regexp.MustCompile(`([\w])+:([0-9]+)`)
+
+func (t *Test) parseName() {
+	for _, match := range testVarRE.FindAllStringSubmatch(t.Name, -1) {
+		if len(match) != 3 {
+			continue
+		}
+		n, err := strconv.Atoi(match[2])
+		if err != nil {
+			continue
+		}
+		switch match[1] {
+		case "threads":
+			t.NumThreads = uint(n)
+		case "tasks":
+			t.NumTasks = uint(n)
+		}
+	}
+}
+
+// Benchmark holds a set of benchmark test results.
+type Benchmark struct {
+	Tests []Test
+}
+
+// Parse parses the benchmark results from the string s.
+// Parse will handle the json and 'console' formats.
+func Parse(s string) (Benchmark, error) {
+	type Parser = func(s string) (Benchmark, error)
+	for _, parser := range []Parser{parseConsole, parseJSON} {
+		b, err := parser(s)
+		switch err {
+		case nil:
+			return b, nil
+		case errWrongFormat:
+		default:
+			return Benchmark{}, err
+		}
+	}
+
+	return Benchmark{}, errors.New("Unrecognised file format")
+}
+
+var errWrongFormat = errors.New("Wrong format")
+var consoleLineRE = regexp.MustCompile(`([\w/:]+)\s+([0-9]+(?:.[0-9]+)?) ns\s+[0-9]+(?:.[0-9]+) ns\s+([0-9]+)`)
+
+func parseConsole(s string) (Benchmark, error) {
+	blocks := strings.Split(s, "------------------------------------------------------------------------------------------")
+	if len(blocks) != 3 {
+		return Benchmark{}, errWrongFormat
+	}
+
+	lines := strings.Split(blocks[2], "\n")
+	b := Benchmark{
+		Tests: make([]Test, 0, len(lines)),
+	}
+	for _, line := range lines {
+		if len(line) == 0 {
+			continue
+		}
+		matches := consoleLineRE.FindStringSubmatch(line)
+		if len(matches) != 4 {
+			return Benchmark{}, fmt.Errorf("Unable to parse the line:\n" + line)
+		}
+		ns, err := strconv.ParseFloat(matches[2], 64)
+		if err != nil {
+			return Benchmark{}, fmt.Errorf("Unable to parse the duration: " + matches[2])
+		}
+		iterations, err := strconv.Atoi(matches[3])
+		if err != nil {
+			return Benchmark{}, fmt.Errorf("Unable to parse the number of iterations: " + matches[3])
+		}
+
+		t := Test{
+			Name:       matches[1],
+			Duration:   time.Nanosecond * time.Duration(ns),
+			Iterations: uint(iterations),
+		}
+		t.parseName()
+		b.Tests = append(b.Tests, t)
+	}
+	return b, nil
+}
+
+func parseJSON(s string) (Benchmark, error) {
+	type T struct {
+		Name       string  `json:"name"`
+		Iterations uint    `json:"iterations"`
+		Time       float64 `json:"real_time"`
+	}
+	type B struct {
+		Tests []T `json:"benchmarks"`
+	}
+	b := B{}
+	d := json.NewDecoder(strings.NewReader(s))
+	if err := d.Decode(&b); err != nil {
+		return Benchmark{}, err
+	}
+
+	out := Benchmark{
+		Tests: make([]Test, len(b.Tests)),
+	}
+	for i, test := range b.Tests {
+		t := Test{
+			Name:       test.Name,
+			Duration:   time.Nanosecond * time.Duration(int64(test.Time)),
+			Iterations: test.Iterations,
+		}
+		t.parseName()
+		out.Tests[i] = t
+	}
+
+	return out, nil
+}
diff --git a/tools/src/cmd/benchdiff/main.go b/tools/src/cmd/benchdiff/main.go
new file mode 100644
index 0000000..14ec4f3
--- /dev/null
+++ b/tools/src/cmd/benchdiff/main.go
@@ -0,0 +1,177 @@
+// Copyright 2022 The Tint 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.
+
+// benchdiff is a tool that compares two Google benchmark results and displays
+// sorted performance differences.
+package main
+
+import (
+	"errors"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+	"text/tabwriter"
+	"time"
+
+	"dawn.googlesource.com/tint/tools/src/bench"
+)
+
+var (
+	minDiff    = flag.Duration("min-diff", time.Microsecond*10, "Filter away time diffs less than this duration")
+	minRelDiff = flag.Float64("min-rel-diff", 0.01, "Filter away absolute relative diffs between [1, 1+x]")
+)
+
+func main() {
+	flag.ErrHelp = errors.New("benchdiff is a tool to compare two benchmark results")
+	flag.Parse()
+	flag.Usage = func() {
+		fmt.Fprintln(os.Stderr, "benchdiff <benchmark-a> <benchmark-b>")
+		flag.PrintDefaults()
+	}
+
+	args := flag.Args()
+	if len(args) < 2 {
+		flag.Usage()
+		os.Exit(1)
+	}
+
+	pathA, pathB := args[0], args[1]
+
+	if err := run(pathA, pathB); err != nil {
+		fmt.Fprintln(os.Stderr, err)
+		os.Exit(-1)
+	}
+}
+
+func run(pathA, pathB string) error {
+	fileA, err := ioutil.ReadFile(pathA)
+	if err != nil {
+		return err
+	}
+	benchA, err := bench.Parse(string(fileA))
+	if err != nil {
+		return err
+	}
+
+	fileB, err := ioutil.ReadFile(pathB)
+	if err != nil {
+		return err
+	}
+	benchB, err := bench.Parse(string(fileB))
+	if err != nil {
+		return err
+	}
+
+	compare(benchA, benchB, fileName(pathA), fileName(pathB))
+
+	return nil
+}
+
+func fileName(path string) string {
+	_, name := filepath.Split(path)
+	return name
+}
+
+func compare(benchA, benchB bench.Benchmark, nameA, nameB string) {
+	type times struct {
+		a time.Duration
+		b time.Duration
+	}
+	byName := map[string]times{}
+	for _, test := range benchA.Tests {
+		byName[test.Name] = times{a: test.Duration}
+	}
+	for _, test := range benchB.Tests {
+		t := byName[test.Name]
+		t.b = test.Duration
+		byName[test.Name] = t
+	}
+
+	type delta struct {
+		name       string
+		times      times
+		relDiff    float64
+		absRelDiff float64
+	}
+	deltas := []delta{}
+	for name, times := range byName {
+		if times.a == 0 || times.b == 0 {
+			continue // Assuming test was missing from a or b
+		}
+		diff := times.b - times.a
+		absDiff := diff
+		if absDiff < 0 {
+			absDiff = -absDiff
+		}
+		if absDiff < *minDiff {
+			continue
+		}
+
+		relDiff := float64(times.b) / float64(times.a)
+		absRelDiff := relDiff
+		if absRelDiff < 1 {
+			absRelDiff = 1.0 / absRelDiff
+		}
+		if absRelDiff < (1.0 + *minRelDiff) {
+			continue
+		}
+
+		d := delta{
+			name:       name,
+			times:      times,
+			relDiff:    relDiff,
+			absRelDiff: absRelDiff,
+		}
+		deltas = append(deltas, d)
+	}
+
+	sort.Slice(deltas, func(i, j int) bool { return deltas[j].relDiff < deltas[i].relDiff })
+
+	fmt.Println("A:", nameA)
+	fmt.Println("B:", nameB)
+	fmt.Println()
+
+	buf := strings.Builder{}
+	{
+		w := tabwriter.NewWriter(&buf, 1, 1, 0, ' ', 0)
+		fmt.Fprintln(w, "Test name\t | Δ (A → B)\t | % (A → B)\t | % (B → A)\t | × (A → B)\t | × (B → A)\t | A \t | B")
+		fmt.Fprintln(w, "\t-+\t-+\t-+\t-+\t-+\t-+\t-+\t-")
+		for _, delta := range deltas {
+			a2b := delta.times.b - delta.times.a
+			fmt.Fprintf(w, "%v \t | %v \t | %+2.1f%% \t | %+2.1f%% \t | %+.4f \t | %+.4f \t | %v \t | %v \t|\n",
+				delta.name,
+				a2b, // Δ (A → B)
+				100*float64(a2b)/float64(delta.times.a),       // % (A → B)
+				100*float64(-a2b)/float64(delta.times.b),      // % (B → A)
+				float64(delta.times.b)/float64(delta.times.a), // × (A → B)
+				float64(delta.times.a)/float64(delta.times.b), // × (B → A)
+				delta.times.a, // A
+				delta.times.b, // B
+			)
+		}
+		w.Flush()
+	}
+
+	// Split the table by line so we can add in a header line
+	lines := strings.Split(buf.String(), "\n")
+	fmt.Println(lines[0])
+	fmt.Println(strings.ReplaceAll(lines[1], " ", "-"))
+	for _, l := range lines[2:] {
+		fmt.Println(l)
+	}
+}