Import Tint changes from Dawn

Changes:
  - 46bd2e101797c5b016303a54b8cee001567b3fc8 Move remote-compile to src/tint/cmd by dan sinclair <dsinclair@chromium.org>
  - 67f09927c0c06bc0f52e816d2420533e4433b1f2 Add TextureBuiltinsFromUniform transform by Shrek Shao <shrekshao@google.com>
GitOrigin-RevId: 46bd2e101797c5b016303a54b8cee001567b3fc8
Change-Id: I222b75bd78ee3b7f3189c00b275279fdb9feff6e
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/142200
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/include/tint/tint.h b/include/tint/tint.h
index c8f6581..899de9a 100644
--- a/include/tint/tint.h
+++ b/include/tint/tint.h
@@ -25,6 +25,7 @@
 #include "src/tint/api/options/array_length_from_uniform.h"
 #include "src/tint/api/options/binding_remapper.h"
 #include "src/tint/api/options/external_texture.h"
+#include "src/tint/api/options/texture_builtins_from_uniform.h"
 #include "src/tint/api/tint.h"
 #include "src/tint/lang/core/type/manager.h"
 #include "src/tint/lang/wgsl/ast/transform/first_index_offset.h"
diff --git a/src/tint/BUILD.gn b/src/tint/BUILD.gn
index b97374d..ea62726 100644
--- a/src/tint/BUILD.gn
+++ b/src/tint/BUILD.gn
@@ -199,6 +199,10 @@
   }
 }
 
+source_set("winsock") {
+  libs = [ "ws2_32.lib" ]
+}
+
 ###############################################################################
 # Aliases.
 ###############################################################################
diff --git a/src/tint/CMakeLists.txt b/src/tint/CMakeLists.txt
index 1720ead..ef4ce00 100644
--- a/src/tint/CMakeLists.txt
+++ b/src/tint/CMakeLists.txt
@@ -448,6 +448,8 @@
         target_link_libraries(${TARGET} PRIVATE
           glslang-default-resource-limits
         )
+      elseif(${DEPENDENCY} STREQUAL "winsock")
+        target_link_libraries(${TARGET} PRIVATE ws2_32)
       else()
         message(FATAL_ERROR "unhandled external dependency ${DEPENDENCY}")
       endif()
@@ -492,6 +494,9 @@
 ################################################################################
 include("BUILD.cmake")
 
+find_package(Threads REQUIRED)
+target_link_libraries(tint_cmd_remote_compile_cmd PRIVATE Threads::Threads)
+
 # If we're building on mac / ios and we have CoreGraphics, then we can use the
 # metal API to validate our shaders. This is roughly 4x faster than invoking
 # the metal shader compiler executable.
@@ -500,6 +505,7 @@
 
   if(LIB_CORE_GRAPHICS)
     target_sources("tint_lang_msl_validate" PRIVATE "lang/msl/validate/msl_metal.mm")
+    target_compile_definitions("tint_cmd_remote_compile_cmd" PRIVATE "-DTINT_ENABLE_MSL_VALIDATION_USING_METAL_API=1")
     target_compile_definitions("tint_lang_msl_validate" PUBLIC "-DTINT_ENABLE_MSL_VALIDATION_USING_METAL_API=1")
     target_compile_options("tint_lang_msl_validate" PRIVATE "-fmodules" "-fcxx-modules")
     target_link_options("tint_lang_msl_validate" PUBLIC "-framework" "CoreGraphics")
diff --git a/src/tint/api/options/BUILD.cmake b/src/tint/api/options/BUILD.cmake
index 2edad38..8117549 100644
--- a/src/tint/api/options/BUILD.cmake
+++ b/src/tint/api/options/BUILD.cmake
@@ -29,6 +29,7 @@
   api/options/binding_remapper.h
   api/options/external_texture.h
   api/options/options.cc
+  api/options/texture_builtins_from_uniform.h
 )
 
 tint_target_add_dependencies(tint_api_options lib
diff --git a/src/tint/api/options/BUILD.gn b/src/tint/api/options/BUILD.gn
index e24f6f1..9608ec4 100644
--- a/src/tint/api/options/BUILD.gn
+++ b/src/tint/api/options/BUILD.gn
@@ -30,6 +30,7 @@
     "binding_remapper.h",
     "external_texture.h",
     "options.cc",
+    "texture_builtins_from_uniform.h",
   ]
   deps = [
     "${tint_src_dir}/api/common",
diff --git a/src/tint/api/options/texture_builtins_from_uniform.h b/src/tint/api/options/texture_builtins_from_uniform.h
new file mode 100644
index 0000000..4dc2eff
--- /dev/null
+++ b/src/tint/api/options/texture_builtins_from_uniform.h
@@ -0,0 +1,52 @@
+// Copyright 2023 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_TINT_API_OPTIONS_TEXTURE_BUILTINS_FROM_UNIFORM_H_
+#define SRC_TINT_API_OPTIONS_TEXTURE_BUILTINS_FROM_UNIFORM_H_
+
+#include <unordered_map>
+#include <utility>
+
+#include "src/tint/api/common/binding_point.h"
+#include "src/tint/utils/reflection/reflection.h"
+
+namespace tint {
+
+/// Options used to specify a mapping of binding points to indices into a UBO
+/// from which to load buffer sizes.
+struct TextureBuiltinsFromUniformOptions {
+    /// Indicate the type of field for each entry to push.
+    enum class Field {
+        /// The number of mip levels of the bonnd texture view.
+        TextureNumLevels,
+        /// The number of samples per texel of the bound multipsampled texture.
+        TextureNumSamples,
+    };
+
+    /// Records the field and the byte offset of the data to push in the internal uniform buffer.
+    using FieldAndOffset = std::pair<Field, uint32_t>;
+    /// Maps from binding point to data entry with the information to populate the data.
+    using BindingPointToFieldAndOffset = std::unordered_map<BindingPoint, FieldAndOffset>;
+
+    /// The binding point to use to generate a uniform buffer from which to read
+    /// buffer sizes.
+    BindingPoint ubo_binding = {};
+
+    /// Reflect the fields of this class so that it can be used by tint::ForeachField()
+    TINT_REFLECT(ubo_binding);
+};
+
+}  // namespace tint
+
+#endif  // SRC_TINT_API_OPTIONS_TEXTURE_BUILTINS_FROM_UNIFORM_H_
diff --git a/src/tint/cmd/BUILD.cmake b/src/tint/cmd/BUILD.cmake
index f547319..cbad9b9 100644
--- a/src/tint/cmd/BUILD.cmake
+++ b/src/tint/cmd/BUILD.cmake
@@ -24,5 +24,6 @@
 include(cmd/common/BUILD.cmake)
 include(cmd/info/BUILD.cmake)
 include(cmd/loopy/BUILD.cmake)
+include(cmd/remote_compile/BUILD.cmake)
 include(cmd/test/BUILD.cmake)
 include(cmd/tint/BUILD.cmake)
diff --git a/src/tint/cmd/remote_compile/BUILD.cfg b/src/tint/cmd/remote_compile/BUILD.cfg
new file mode 100644
index 0000000..046b778
--- /dev/null
+++ b/src/tint/cmd/remote_compile/BUILD.cfg
@@ -0,0 +1,3 @@
+{
+    "cmd": { "OutputName": "tint_remote_compile" }
+}
diff --git a/src/tint/cmd/remote_compile/BUILD.cmake b/src/tint/cmd/remote_compile/BUILD.cmake
new file mode 100644
index 0000000..a92b5bd
--- /dev/null
+++ b/src/tint/cmd/remote_compile/BUILD.cmake
@@ -0,0 +1,45 @@
+# Copyright 2023 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.
+
+################################################################################
+# File generated by tools/src/cmd/gen
+# using the template:
+#   tools/src/cmd/gen/build/BUILD.cmake.tmpl
+#
+# Do not modify this file directly
+################################################################################
+
+################################################################################
+# Target:    tint_cmd_remote_compile_cmd
+# Kind:      cmd
+################################################################################
+tint_add_target(tint_cmd_remote_compile_cmd cmd
+  cmd/remote_compile/main.cc
+)
+
+tint_target_add_dependencies(tint_cmd_remote_compile_cmd cmd
+  tint_lang_wgsl_ast
+  tint_utils_macros
+  tint_utils_socket
+  tint_utils_text
+  tint_utils_traits
+)
+
+if(TINT_BUILD_MSL_WRITER)
+  tint_target_add_dependencies(tint_cmd_remote_compile_cmd cmd
+    tint_lang_msl_validate
+  )
+endif(TINT_BUILD_MSL_WRITER)
+
+tint_target_set_output_name(tint_cmd_remote_compile_cmd cmd "tint_remote_compile")
diff --git a/src/tint/cmd/remote_compile/BUILD.gn b/src/tint/cmd/remote_compile/BUILD.gn
new file mode 100644
index 0000000..033196a
--- /dev/null
+++ b/src/tint/cmd/remote_compile/BUILD.gn
@@ -0,0 +1,41 @@
+# Copyright 2023 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.
+
+################################################################################
+# File generated by tools/src/cmd/gen
+# using the template:
+#   tools/src/cmd/gen/build/BUILD.gn.tmpl
+#
+# Do not modify this file directly
+################################################################################
+
+import("../../../../scripts/tint_overrides_with_defaults.gni")
+
+import("${tint_src_dir}/tint.gni")
+
+executable("remote_compile") {
+  output_name = "tint_remote_compile"
+  sources = [ "main.cc" ]
+  deps = [
+    "${tint_src_dir}/lang/wgsl/ast",
+    "${tint_src_dir}/utils/macros",
+    "${tint_src_dir}/utils/socket",
+    "${tint_src_dir}/utils/text",
+    "${tint_src_dir}/utils/traits",
+  ]
+
+  if (tint_build_msl_writer) {
+    deps += [ "${tint_src_dir}/lang/msl/validate" ]
+  }
+}
diff --git a/src/tint/cmd/remote_compile/main.cc b/src/tint/cmd/remote_compile/main.cc
new file mode 100644
index 0000000..a13870e
--- /dev/null
+++ b/src/tint/cmd/remote_compile/main.cc
@@ -0,0 +1,494 @@
+// Copyright 2021 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.
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <fstream>
+#include <iostream>
+#include <regex>
+#include <sstream>
+#include <string>
+#include <thread>
+#include <type_traits>
+#include <vector>
+
+#include "src/tint/lang/msl/validate/val.h"
+#include "src/tint/utils/macros/compiler.h"
+#include "src/tint/utils/socket/socket.h"
+
+namespace {
+
+#if 0
+#define DEBUG(msg, ...) printf(msg "\n", ##__VA_ARGS__)
+#else
+#define DEBUG(...)
+#endif
+
+/// The return structure of a compile function
+struct CompileResult {
+    /// True if shader compiled
+    bool success = false;
+    /// Output of the compiler
+    std::string output;
+};
+
+/// Print the tool usage, and exit with 1.
+[[noreturn]] void ShowUsage() {
+    const char* name = "tint-remote-compile";
+    printf(R"(%s is a tool for compiling a shader on a remote machine
+
+usage as server:
+  %s -s [-p port-number]
+
+usage as client:
+  %s [-p port-number] [server-address] shader-file-path
+
+  [server-address] can be omitted if the TINT_REMOTE_COMPILE_ADDRESS environment
+  variable is set.
+  Alternatively, you can pass xcrun arguments so %s can be used as a
+  drop-in replacement.
+)",
+           name, name, name, name);
+    exit(1);
+}
+
+/// The protocol version code. Bump each time the protocol changes
+constexpr uint32_t kProtocolVersion = 1;
+
+/// Supported shader source languages
+enum SourceLanguage {
+    MSL,
+};
+
+/// Stream is a serialization wrapper around a socket
+struct Stream {
+    /// The underlying socket
+    Socket* const socket;
+    /// Error state
+    std::string error;
+
+    /// Writes a uint32_t to the socket
+    Stream operator<<(uint32_t v) {
+        if (error.empty()) {
+            Write(&v, sizeof(v));
+        }
+        return *this;
+    }
+
+    /// Reads a uint32_t from the socket
+    Stream operator>>(uint32_t& v) {
+        if (error.empty()) {
+            Read(&v, sizeof(v));
+        }
+        return *this;
+    }
+
+    /// Writes a std::string to the socket
+    Stream operator<<(const std::string& v) {
+        if (error.empty()) {
+            uint32_t count = static_cast<uint32_t>(v.size());
+            *this << count;
+            if (count) {
+                Write(v.data(), count);
+            }
+        }
+        return *this;
+    }
+
+    /// Reads a std::string from the socket
+    Stream operator>>(std::string& v) {
+        uint32_t count = 0;
+        *this >> count;
+        if (count) {
+            std::vector<char> buf(count);
+            if (Read(buf.data(), count)) {
+                v = std::string(buf.data(), buf.size());
+            }
+        } else {
+            v.clear();
+        }
+        return *this;
+    }
+
+    /// Writes an enum value to the socket
+    template <typename T>
+    std::enable_if_t<std::is_enum<T>::value, Stream> operator<<(T e) {
+        return *this << static_cast<uint32_t>(e);
+    }
+
+    /// Reads an enum value from the socket
+    template <typename T>
+    std::enable_if_t<std::is_enum<T>::value, Stream> operator>>(T& e) {
+        uint32_t v;
+        *this >> v;
+        e = static_cast<T>(v);
+        return *this;
+    }
+
+  private:
+    bool Write(const void* data, size_t size) {
+        if (error.empty()) {
+            if (!socket->Write(data, size)) {
+                error = "Socket::Write() failed";
+            }
+        }
+        return error.empty();
+    }
+
+    bool Read(void* data, size_t size) {
+        auto buf = reinterpret_cast<uint8_t*>(data);
+        while (size > 0 && error.empty()) {
+            if (auto n = socket->Read(buf, size)) {
+                if (n > size) {
+                    error = "Socket::Read() returned more bytes than requested";
+                    return false;
+                }
+                size -= n;
+                buf += n;
+            } else {
+                error = "Socket::Read() failed";
+            }
+        }
+        return error.empty();
+    }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// Messages
+////////////////////////////////////////////////////////////////////////////////
+
+/// Base class for all messages
+struct Message {
+    /// The type of the message
+    enum class Type {
+        ConnectionRequest,
+        ConnectionResponse,
+        CompileRequest,
+        CompileResponse,
+    };
+
+    explicit Message(Type ty) : type(ty) {}
+
+    const Type type;
+};
+
+struct ConnectionResponse : Message {  // Server -> Client
+    ConnectionResponse() : Message(Type::ConnectionResponse) {}
+
+    template <typename T>
+    void Serialize(T&& f) {
+        f(error);
+    }
+
+    std::string error;
+};
+
+struct ConnectionRequest : Message {  // Client -> Server
+    using Response = ConnectionResponse;
+
+    explicit ConnectionRequest(uint32_t proto_ver = kProtocolVersion)
+        : Message(Type::ConnectionRequest), protocol_version(proto_ver) {}
+
+    template <typename T>
+    void Serialize(T&& f) {
+        f(protocol_version);
+    }
+
+    uint32_t protocol_version;
+};
+
+struct CompileResponse : Message {  //  Server -> Client
+    CompileResponse() : Message(Type::CompileResponse) {}
+
+    template <typename T>
+    void Serialize(T&& f) {
+        f(error);
+    }
+
+    std::string error;
+};
+
+struct CompileRequest : Message {  // Client -> Server
+    using Response = CompileResponse;
+
+    CompileRequest() : Message(Type::CompileRequest) {}
+    CompileRequest(SourceLanguage lang, int ver_major, int ver_minor, std::string src)
+        : Message(Type::CompileRequest),
+          language(lang),
+          version_major(uint32_t(ver_major)),
+          version_minor(uint32_t(ver_minor)),
+          source(src) {}
+
+    template <typename T>
+    void Serialize(T&& f) {
+        f(language);
+        f(source);
+        f(version_major);
+        f(version_minor);
+    }
+
+    SourceLanguage language = SourceLanguage::MSL;
+    uint32_t version_major = 0;
+    uint32_t version_minor = 0;
+    std::string source;
+};
+
+/// Writes the message `m` to the stream `s`
+template <typename MESSAGE>
+std::enable_if_t<std::is_base_of<Message, MESSAGE>::value, Stream>& operator<<(Stream& s,
+                                                                               const MESSAGE& m) {
+    s << m.type;
+    const_cast<MESSAGE&>(m).Serialize([&s](const auto& value) { s << value; });
+    return s;
+}
+
+/// Reads the message `m` from the stream `s`
+template <typename MESSAGE>
+std::enable_if_t<std::is_base_of<Message, MESSAGE>::value, Stream>& operator>>(Stream& s,
+                                                                               MESSAGE& m) {
+    Message::Type ty;
+    s >> ty;
+    if (s.error.empty()) {
+        if (ty == m.type) {
+            m.Serialize([&s](auto& value) { s >> value; });
+        } else {
+            std::stringstream ss;
+            ss << "expected message type " << static_cast<int>(m.type) << ", got "
+               << static_cast<int>(ty);
+            s.error = ss.str();
+        }
+    }
+    return s;
+}
+
+/// Writes the request message `req` to the stream `s`, then reads and returns
+/// the response message from the same stream.
+template <typename REQUEST, typename RESPONSE = typename REQUEST::Response>
+RESPONSE Send(Stream& s, const REQUEST& req) {
+    s << req;
+    if (s.error.empty()) {
+        RESPONSE resp;
+        s >> resp;
+        if (s.error.empty()) {
+            return resp;
+        }
+    }
+    return {};
+}
+
+}  // namespace
+
+bool RunServer(std::string port);
+bool RunClient(std::string address,
+               std::string port,
+               std::string file,
+               int version_major,
+               int version_minor);
+
+int main(int argc, char* argv[]) {
+    bool run_server = false;
+    int version_major = 0;
+    int version_minor = 0;
+    std::string port = "19000";
+
+    std::regex metal_version_re{"^-?-std=macos-metal([0-9]+)\\.([0-9]+)"};
+
+    std::vector<std::string> args;
+    for (int i = 1; i < argc; i++) {
+        std::string arg = argv[i];
+        if (arg == "-s" || arg == "--server") {
+            run_server = true;
+            continue;
+        }
+        if (arg == "-p" || arg == "--port") {
+            if (i < argc - 1) {
+                i++;
+                port = argv[i];
+            } else {
+                printf("expected port number");
+                exit(1);
+            }
+            continue;
+        }
+
+        // xcrun flags are ignored so this executable can be used as a replacement for xcrun.
+        if ((arg == "-x" || arg == "-sdk") && (i < argc - 1)) {
+            i++;
+            continue;
+        }
+        if (arg == "metal") {
+            for (; i < argc; i++) {
+                arg = argv[i];
+                // metal_version_re
+                std::smatch metal_version_match;
+                if (std::regex_match(arg, metal_version_match, metal_version_re)) {
+                    version_major = std::atoi(metal_version_match[1].str().c_str());
+                    version_minor = std::atoi(metal_version_match[2].str().c_str());
+                    continue;
+                }
+                if (arg == "-c") {
+                    break;
+                }
+            }
+            continue;
+        }
+
+        args.emplace_back(arg);
+    }
+
+    bool success = false;
+
+    if (run_server) {
+        success = RunServer(port);
+    } else {
+        std::string address;
+        std::string file;
+        switch (args.size()) {
+            case 1:
+                TINT_BEGIN_DISABLE_WARNING(DEPRECATED);
+                if (auto* addr = getenv("TINT_REMOTE_COMPILE_ADDRESS")) {
+                    address = addr;
+                }
+                TINT_END_DISABLE_WARNING(DEPRECATED);
+                file = args[0];
+                break;
+            case 2:
+                address = args[0];
+                file = args[1];
+                break;
+            default:
+                std::cerr << "expected 1 or 2 arguments, got " << args.size() << std::endl
+                          << std::endl;
+                ShowUsage();
+        }
+        if (address.empty() || file.empty()) {
+            ShowUsage();
+        }
+        success = RunClient(address, port, file, version_major, version_minor);
+    }
+
+    if (!success) {
+        exit(1);
+    }
+
+    return 0;
+}
+
+bool RunServer(std::string port) {
+    auto server_socket = Socket::Listen("", port.c_str());
+    if (!server_socket) {
+        std::cout << "Failed to listen on port " << port << std::endl;
+        return false;
+    }
+    std::cout << "Listening on port " << port.c_str() << "..." << std::endl;
+    while (auto conn = server_socket->Accept()) {
+        std::thread([=] {
+            DEBUG("Client connected...");
+            Stream stream{conn.get(), ""};
+
+            {
+                ConnectionRequest req;
+                stream >> req;
+                if (!stream.error.empty()) {
+                    DEBUG("%s", stream.error.c_str());
+                    return;
+                }
+                ConnectionResponse resp;
+                if (req.protocol_version != kProtocolVersion) {
+                    DEBUG("Protocol version mismatch");
+                    resp.error = "Protocol version mismatch";
+                    stream << resp;
+                    return;
+                }
+                stream << resp;
+            }
+            DEBUG("Connection established");
+            {
+                CompileRequest req;
+                stream >> req;
+                if (!stream.error.empty()) {
+                    DEBUG("%s\n", stream.error.c_str());
+                    return;
+                }
+#ifdef TINT_ENABLE_MSL_VALIDATION_USING_METAL_API
+                if (req.language == SourceLanguage::MSL) {
+                    auto version = tint::msl::validate::MslVersion::kMsl_1_2;
+                    if (req.version_major == 2 && req.version_minor == 1) {
+                        version = tint::msl::validate::MslVersion::kMsl_2_1;
+                    }
+                    auto result = tint::msl::validate::UsingMetalAPI(req.source, version);
+                    CompileResponse resp;
+                    if (result.failed) {
+                        resp.error = result.output;
+                    }
+                    stream << resp;
+                    return;
+                }
+#endif
+                CompileResponse resp;
+                resp.error = "server cannot compile this type of shader";
+                stream << resp;
+            }
+        }).detach();
+    }
+    return true;
+}
+
+bool RunClient(std::string address,
+               std::string port,
+               std::string file,
+               int version_major,
+               int version_minor) {
+    // Read the file
+    std::ifstream input(file, std::ios::binary);
+    if (!input) {
+        std::cerr << "Couldn't open '" << file << "'" << std::endl;
+        return false;
+    }
+    std::string source((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
+
+    constexpr const int timeout_ms = 10000;
+    DEBUG("Connecting to %s:%s...", address.c_str(), port.c_str());
+    auto conn = Socket::Connect(address.c_str(), port.c_str(), timeout_ms);
+    if (!conn) {
+        std::cerr << "Connection failed" << std::endl;
+        return false;
+    }
+
+    Stream stream{conn.get(), ""};
+
+    DEBUG("Sending connection request...");
+    auto conn_resp = Send(stream, ConnectionRequest{kProtocolVersion});
+    if (!stream.error.empty()) {
+        std::cerr << stream.error << std::endl;
+        return false;
+    }
+    if (!conn_resp.error.empty()) {
+        std::cerr << conn_resp.error << std::endl;
+        return false;
+    }
+    DEBUG("Connection established. Requesting compile...");
+    auto comp_resp =
+        Send(stream, CompileRequest{SourceLanguage::MSL, version_major, version_minor, source});
+    if (!stream.error.empty()) {
+        std::cerr << stream.error << std::endl;
+        return false;
+    }
+    if (!comp_resp.error.empty()) {
+        std::cerr << comp_resp.error << std::endl;
+        return false;
+    }
+    DEBUG("Compilation successful");
+    return true;
+}
diff --git a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
index 127a2da..895fd56 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer.cc
@@ -62,6 +62,7 @@
 #include "src/tint/lang/wgsl/ast/transform/single_entry_point.h"
 #include "src/tint/lang/wgsl/ast/transform/std140.h"
 #include "src/tint/lang/wgsl/ast/transform/texture_1d_to_2d.h"
+#include "src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform.h"
 #include "src/tint/lang/wgsl/ast/transform/unshadow.h"
 #include "src/tint/lang/wgsl/ast/transform/zero_init_workgroup_memory.h"
 #include "src/tint/lang/wgsl/ast/variable_decl_statement.h"
@@ -229,6 +230,15 @@
 
     manager.Add<ast::transform::RemovePhonies>();
 
+    // TextureBuiltinsFromUniform must come before CombineSamplers to preserve texture binding point
+    // info, instead of combined sampler binding point. As a result, TextureBuiltinsFromUniform also
+    // comes before BindingRemapper so the binding point info it reflects is before remapping.
+    if (options.texture_builtins_from_uniform) {
+        manager.Add<ast::transform::TextureBuiltinsFromUniform>();
+        data.Add<ast::transform::TextureBuiltinsFromUniform::Config>(
+            options.texture_builtins_from_uniform->ubo_binding);
+    }
+
     data.Add<ast::transform::CombineSamplers::BindingInfo>(options.binding_map,
                                                            options.placeholder_binding_point);
     manager.Add<ast::transform::CombineSamplers>();
@@ -254,6 +264,10 @@
     SanitizedResult result;
     ast::transform::DataMap outputs;
     result.program = manager.Run(in, data, outputs);
+    if (auto* res = outputs.Get<ast::transform::TextureBuiltinsFromUniform::Result>()) {
+        result.needs_internal_uniform_buffer = true;
+        result.bindpoint_to_data = std::move(res->bindpoint_to_data);
+    }
     return result;
 }
 
diff --git a/src/tint/lang/glsl/writer/ast_printer/ast_printer.h b/src/tint/lang/glsl/writer/ast_printer/ast_printer.h
index 6c8f5fb..ad44120 100644
--- a/src/tint/lang/glsl/writer/ast_printer/ast_printer.h
+++ b/src/tint/lang/glsl/writer/ast_printer/ast_printer.h
@@ -21,6 +21,7 @@
 #include <unordered_set>
 #include <utility>
 
+#include "src/tint/api/options/texture_builtins_from_uniform.h"
 #include "src/tint/lang/core/builtin_value.h"
 #include "src/tint/lang/glsl/writer/common/version.h"
 #include "src/tint/lang/wgsl/program/program_builder.h"
@@ -53,6 +54,13 @@
 
     /// The sanitized program.
     Program program;
+
+    /// True if the shader needs a UBO.
+    bool needs_internal_uniform_buffer = false;
+
+    /// Store a map of global texture variable binding point to the byte offset and data type to
+    /// push into the internal uniform buffer.
+    TextureBuiltinsFromUniformOptions::BindingPointToFieldAndOffset bindpoint_to_data;
 };
 
 /// Sanitize a program in preparation for generating GLSL.
diff --git a/src/tint/lang/glsl/writer/common/options.h b/src/tint/lang/glsl/writer/common/options.h
index e2aadd7..2f55c4b 100644
--- a/src/tint/lang/glsl/writer/common/options.h
+++ b/src/tint/lang/glsl/writer/common/options.h
@@ -15,10 +15,12 @@
 #ifndef SRC_TINT_LANG_GLSL_WRITER_COMMON_OPTIONS_H_
 #define SRC_TINT_LANG_GLSL_WRITER_COMMON_OPTIONS_H_
 
+#include <optional>
 #include <string>
 #include <unordered_map>
 
 #include "src/tint/api/options/external_texture.h"
+#include "src/tint/api/options/texture_builtins_from_uniform.h"
 #include "src/tint/lang/core/access.h"
 #include "src/tint/lang/glsl/writer/common/version.h"
 #include "src/tint/lang/wgsl/sem/sampler_texture_pair.h"
@@ -66,6 +68,11 @@
     /// Options used in the binding mappings for external textures
     ExternalTextureOptions external_texture_options = {};
 
+    /// Options used to map WGSL textureNumLevels/textureNumSamples builtins to internal uniform
+    /// buffer values. If not specified, emits corresponding GLSL builtins
+    /// textureQueryLevels/textureSamples directly.
+    std::optional<TextureBuiltinsFromUniformOptions> texture_builtins_from_uniform = std::nullopt;
+
     /// The GLSL version to emit
     Version version;
 
@@ -74,6 +81,7 @@
                  allow_collisions,
                  disable_workgroup_init,
                  external_texture_options,
+                 texture_builtins_from_uniform,
                  version);
 };
 
diff --git a/src/tint/lang/glsl/writer/output.h b/src/tint/lang/glsl/writer/output.h
index 084b0ef..286af3f 100644
--- a/src/tint/lang/glsl/writer/output.h
+++ b/src/tint/lang/glsl/writer/output.h
@@ -19,6 +19,7 @@
 #include <utility>
 #include <vector>
 
+#include "src/tint/api/options/texture_builtins_from_uniform.h"
 #include "src/tint/lang/wgsl/ast/pipeline_stage.h"
 
 namespace tint::glsl::writer {
@@ -39,6 +40,13 @@
 
     /// The list of entry points in the generated GLSL.
     std::vector<std::pair<std::string, ast::PipelineStage>> entry_points;
+
+    /// True if the shader needs a UBO.
+    bool needs_internal_uniform_buffer = false;
+
+    /// Store a map of global texture variable binding points to the byte offset and data type to
+    /// push into the internal uniform buffer.
+    TextureBuiltinsFromUniformOptions::BindingPointToFieldAndOffset bindpoint_to_data;
 };
 
 }  // namespace tint::glsl::writer
diff --git a/src/tint/lang/glsl/writer/writer.cc b/src/tint/lang/glsl/writer/writer.cc
index 4124346..e02c6bb 100644
--- a/src/tint/lang/glsl/writer/writer.cc
+++ b/src/tint/lang/glsl/writer/writer.cc
@@ -15,6 +15,7 @@
 #include "src/tint/lang/glsl/writer/writer.h"
 
 #include <memory>
+#include <utility>
 
 #include "src/tint/lang/glsl/writer/ast_printer/ast_printer.h"
 #include "src/tint/lang/wgsl/ast/transform/binding_remapper.h"
@@ -43,6 +44,8 @@
 
     Output output;
     output.glsl = impl->Result();
+    output.needs_internal_uniform_buffer = sanitized_result.needs_internal_uniform_buffer;
+    output.bindpoint_to_data = std::move(sanitized_result.bindpoint_to_data);
 
     // Collect the list of entry points in the sanitized program.
     for (auto* func : sanitized_result.program.AST().Functions()) {
diff --git a/src/tint/lang/msl/validate/val.h b/src/tint/lang/msl/validate/val.h
index 498516b..78096dc 100644
--- a/src/tint/lang/msl/validate/val.h
+++ b/src/tint/lang/msl/validate/val.h
@@ -40,7 +40,7 @@
 struct Result {
     /// True if validation passed
     bool failed = false;
-    /// Output of DXC.
+    /// Output of Metal compiler.
     std::string output;
 };
 
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.cmake b/src/tint/lang/wgsl/ast/transform/BUILD.cmake
index e56ea18..d91332e 100644
--- a/src/tint/lang/wgsl/ast/transform/BUILD.cmake
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.cmake
@@ -115,6 +115,8 @@
   lang/wgsl/ast/transform/substitute_override.h
   lang/wgsl/ast/transform/texture_1d_to_2d.cc
   lang/wgsl/ast/transform/texture_1d_to_2d.h
+  lang/wgsl/ast/transform/texture_builtins_from_uniform.cc
+  lang/wgsl/ast/transform/texture_builtins_from_uniform.h
   lang/wgsl/ast/transform/transform.cc
   lang/wgsl/ast/transform/transform.h
   lang/wgsl/ast/transform/truncate_interstage_variables.cc
@@ -213,6 +215,7 @@
   lang/wgsl/ast/transform/std140_test.cc
   lang/wgsl/ast/transform/substitute_override_test.cc
   lang/wgsl/ast/transform/texture_1d_to_2d_test.cc
+  lang/wgsl/ast/transform/texture_builtins_from_uniform_test.cc
   lang/wgsl/ast/transform/transform_test.cc
   lang/wgsl/ast/transform/truncate_interstage_variables_test.cc
   lang/wgsl/ast/transform/unshadow_test.cc
diff --git a/src/tint/lang/wgsl/ast/transform/BUILD.gn b/src/tint/lang/wgsl/ast/transform/BUILD.gn
index 8df2f94..c55205d 100644
--- a/src/tint/lang/wgsl/ast/transform/BUILD.gn
+++ b/src/tint/lang/wgsl/ast/transform/BUILD.gn
@@ -120,6 +120,8 @@
     "substitute_override.h",
     "texture_1d_to_2d.cc",
     "texture_1d_to_2d.h",
+    "texture_builtins_from_uniform.cc",
+    "texture_builtins_from_uniform.h",
     "transform.cc",
     "transform.h",
     "truncate_interstage_variables.cc",
@@ -216,6 +218,7 @@
       "std140_test.cc",
       "substitute_override_test.cc",
       "texture_1d_to_2d_test.cc",
+      "texture_builtins_from_uniform_test.cc",
       "transform_test.cc",
       "truncate_interstage_variables_test.cc",
       "unshadow_test.cc",
diff --git a/src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform.cc b/src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform.cc
new file mode 100644
index 0000000..4c4baee
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform.cc
@@ -0,0 +1,491 @@
+// Copyright 2023 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/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform.h"
+
+#include <memory>
+#include <queue>
+#include <string>
+#include <utility>
+#include <variant>
+#include <vector>
+
+#include "src/tint/lang/wgsl/program/clone_context.h"
+#include "src/tint/lang/wgsl/program/program_builder.h"
+#include "src/tint/lang/wgsl/resolver/resolve.h"
+#include "src/tint/lang/wgsl/sem/call.h"
+#include "src/tint/lang/wgsl/sem/function.h"
+#include "src/tint/lang/wgsl/sem/module.h"
+#include "src/tint/lang/wgsl/sem/statement.h"
+#include "src/tint/lang/wgsl/sem/variable.h"
+
+#include "src/tint/utils/containers/hashmap.h"
+#include "src/tint/utils/containers/vector.h"
+#include "src/tint/utils/rtti/switch.h"
+
+TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::TextureBuiltinsFromUniform);
+TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::TextureBuiltinsFromUniform::Config);
+TINT_INSTANTIATE_TYPEINFO(tint::ast::transform::TextureBuiltinsFromUniform::Result);
+
+namespace tint::ast::transform {
+
+namespace {
+
+/// The member name of the texture builtin values.
+constexpr std::string_view kTextureBuiltinValuesMemberNamePrefix = "texture_builtin_value_";
+
+bool ShouldRun(const Program* program) {
+    for (auto* fn : program->AST().Functions()) {
+        if (auto* sem_fn = program->Sem().Get(fn)) {
+            for (auto* builtin : sem_fn->DirectlyCalledBuiltins()) {
+                // GLSL ES  has no native support for the counterpart of
+                // textureNumLevels (textureQueryLevels) and textureNumSamples (textureSamples)
+                if (builtin->Type() == core::Function::kTextureNumLevels) {
+                    return true;
+                }
+                if (builtin->Type() == core::Function::kTextureNumSamples) {
+                    return true;
+                }
+            }
+        }
+    }
+    return false;
+}
+
+}  // namespace
+
+TextureBuiltinsFromUniform::TextureBuiltinsFromUniform() = default;
+TextureBuiltinsFromUniform::~TextureBuiltinsFromUniform() = default;
+
+/// PIMPL state for the transform
+struct TextureBuiltinsFromUniform::State {
+    /// Constructor
+    /// @param program the source program
+    /// @param in the input transform data
+    /// @param out the output transform data
+    explicit State(const Program* program, const DataMap& in, DataMap& out)
+        : src(program), inputs(in), outputs(out) {}
+
+    /// Runs the transform
+    /// @returns the new program or SkipTransform if the transform is not required
+    ApplyResult Run() {
+        auto* cfg = inputs.Get<Config>();
+        if (cfg == nullptr) {
+            b.Diagnostics().add_error(
+                diag::System::Transform,
+                "missing transform data for " +
+                    std::string(tint::TypeInfo::Of<TextureBuiltinsFromUniform>().name));
+            return resolver::Resolve(b);
+        }
+
+        if (!ShouldRun(ctx.src)) {
+            return SkipTransform;
+        }
+
+        // The dependency order declartions guaranteed that we traverse interested functions in the
+        // following order:
+        // 1. texture builtins
+        // 2. user function directly calls texture builtins
+        // 3. user function calls 2.
+        // 4. user function calls 3.
+        // ...
+        // n. entry point function.
+        for (auto* fn_decl : sem.Module()->DependencyOrderedDeclarations()) {
+            if (auto* fn = sem.Get<sem::Function>(fn_decl)) {
+                for (auto* call : fn->DirectCalls()) {
+                    auto* call_expr = call->Declaration();
+
+                    tint::Switch(
+                        call->Target(),
+                        [&](const sem::Builtin* builtin) {
+                            if (builtin->Type() != core::Function::kTextureNumLevels &&
+                                builtin->Type() != core::Function::kTextureNumSamples) {
+                                return;
+                            }
+                            if (auto* call_stmt =
+                                    call->Stmt()->Declaration()->As<CallStatement>()) {
+                                if (call_stmt->expr == call->Declaration()) {
+                                    // textureNumLevels() / textureNumSamples() is used as a
+                                    // statement. The argument expression must be side-effect free,
+                                    // so just drop the statement.
+                                    RemoveStatement(ctx, call_stmt);
+                                    return;
+                                }
+                            }
+
+                            auto* texture_expr = call->Declaration()->args[0];
+                            auto* texture_sem = sem.GetVal(texture_expr)->RootIdentifier();
+                            TINT_ASSERT(texture_sem);
+
+                            TextureBuiltinsFromUniformOptions::Field dataType =
+                                GetFieldFromBuiltinFunctionType(builtin->Type());
+
+                            tint::Switch(
+                                texture_sem,
+                                [&](const sem::GlobalVariable* global) {
+                                    // This texture variable is a global variable.
+                                    auto binding = GetAndRecordGlobalBinding(global, dataType);
+                                    // Record the call and binding to be replaced later.
+                                    builtin_to_replace.Add(call_expr, binding);
+                                },
+                                [&](const sem::Variable* variable) {
+                                    // This texture variable is a user function parameter.
+                                    auto new_param =
+                                        GetAndRecordFunctionParameter(fn, variable, dataType);
+                                    // Record the call and new_param to be replaced later.
+                                    builtin_to_replace.Add(call_expr, new_param);
+                                },
+                                [&](Default) {
+                                    TINT_ICE() << "unexpected texture root identifier";
+                                });
+                        },
+                        [&](const sem::Function* user_fn) {
+                            auto user_param_to_info = fn_to_data.Find(user_fn);
+                            if (!user_param_to_info) {
+                                // Uninterested function not calling texture builtins with function
+                                // texture param.
+                                return;
+                            }
+                            TINT_ASSERT(call->Arguments().Length() ==
+                                        user_fn->Declaration()->params.Length());
+                            for (size_t i = 0; i < call->Arguments().Length(); i++) {
+                                auto param = user_fn->Declaration()->params[i];
+                                auto info = user_param_to_info->Get(param);
+                                if (info.has_value()) {
+                                    auto* arg = call->Arguments()[i];
+                                    auto* texture_sem = arg->RootIdentifier();
+                                    auto& args = call_to_data.GetOrCreate(call_expr, [&] {
+                                        return Vector<
+                                            std::variant<BindingPoint, const ast::Parameter*>, 4>();
+                                    });
+
+                                    tint::Switch(
+                                        texture_sem,
+                                        [&](const sem::GlobalVariable* global) {
+                                            // This texture variable is a global variable.
+                                            auto binding =
+                                                GetAndRecordGlobalBinding(global, info->field);
+                                            // Record the binding to add to args.
+                                            args.Push(binding);
+                                        },
+                                        [&](const sem::Variable* variable) {
+                                            // This texture variable is a user function parameter.
+                                            auto new_param = GetAndRecordFunctionParameter(
+                                                fn, variable, info->field);
+                                            // Record adding extra function parameter
+                                            args.Push(new_param);
+                                        },
+                                        [&](Default) {
+                                            TINT_ICE() << "unexpected texture root identifier";
+                                        });
+                                }
+                            }
+                        });
+                }
+            }
+        }
+
+        // If there's no interested texture builtin at all, skip the transform.
+        if (bindpoint_to_data.empty()) {
+            return SkipTransform;
+        }
+
+        // If any functions need extra params, add them now.
+        if (!fn_to_data.IsEmpty()) {
+            for (auto pair : fn_to_data) {
+                auto* fn = pair.key;
+
+                // Reorder the param to a vector to make sure params are in the correct order.
+                Vector<const ast::Parameter*, 4> extra_params_in_order;
+                extra_params_in_order.Resize(pair.value.Count());
+                for (auto t_p : pair.value) {
+                    TINT_ASSERT(t_p.value.extra_idx < extra_params_in_order.Length());
+                    extra_params_in_order[t_p.value.extra_idx] = t_p.value.param;
+                }
+
+                for (auto p : extra_params_in_order) {
+                    ctx.InsertBack(fn->Declaration()->params, p);
+                }
+            }
+        }
+
+        // Replace all interested texture builtin calls.
+        for (auto pair : builtin_to_replace) {
+            auto call = pair.key;
+            if (std::holds_alternative<BindingPoint>(pair.value)) {
+                // This texture is a global variable with binding point.
+                // Read builtin value from uniform buffer.
+                auto* builtin_value = GetUniformValue(std::get<BindingPoint>(pair.value));
+                ctx.Replace(call, builtin_value);
+            } else {
+                // Otherwise this value comes from a function param
+                auto* param = std::get<const ast::Parameter*>(pair.value);
+                ctx.Replace(call, b.Expr(param));
+            }
+        }
+
+        // Insert all extra args to interested function calls.
+        for (auto pair : call_to_data) {
+            auto call = pair.key;
+            for (auto new_arg_info : pair.value) {
+                if (std::holds_alternative<BindingPoint>(new_arg_info)) {
+                    // This texture is a global variable with binding point.
+                    // Read builtin value from uniform buffer.
+                    auto* builtin_value = GetUniformValue(std::get<BindingPoint>(new_arg_info));
+                    ctx.InsertBack(call->args, builtin_value);
+                } else {
+                    // Otherwise this value comes from a function param
+                    auto* param = std::get<const ast::Parameter*>(new_arg_info);
+                    ctx.InsertBack(call->args, b.Expr(param));
+                }
+            }
+        }
+
+        outputs.Add<Result>(bindpoint_to_data);
+
+        ctx.Clone();
+        return resolver::Resolve(b);
+    }
+
+  private:
+    /// The source program
+    const Program* const src;
+    /// The transform inputs
+    const DataMap& inputs;
+    /// The transform outputs
+    DataMap& outputs;
+    /// The target program builder
+    ProgramBuilder b;
+    /// The clone context
+    program::CloneContext ctx = {&b, src, /* auto_clone_symbols */ true};
+    /// Alias to the semantic info in ctx.src
+    const sem::Info& sem = ctx.src->Sem();
+
+    /// The bindpoint to byte offset and field to pass out in transform result.
+    /// For one texture type, it could only be passed into one of the
+    /// textureNumLevels or textureNumSamples because their accepting param texture
+    /// type is different. There cannot be a binding entry with both field type.
+    /// Note: because this transform must be run before CombineSampler and BindingRemapper,
+    /// the binding number here is before remapped.
+    Result::BindingPointToFieldAndOffset bindpoint_to_data;
+
+    struct FunctionExtraParamInfo {
+        using Field = TextureBuiltinsFromUniformOptions::Field;
+        // The kind of texture information this parameter holds.
+        Field field = Field::TextureNumLevels;
+
+        // The extra passed in param that corresponds to the texture param.
+        const ast::Parameter* param = nullptr;
+
+        // id of this extra param e.g. f(t0, foo, t1, e0, e1) e0 and e1 are extra params, their
+        // extra_idx are 0 and 1. This is to help sort extra ids in the correct order.
+        size_t extra_idx = 0;
+    };
+
+    /// Store a map from function to a collection of extra params that need adding.
+    /// The value of the map is made a map instead of a vector to make it easier to find the param.
+    /// for call sites. e.g. fn f(t: texture_2d<f32>) -> u32 {
+    ///   return textureNumLevels(t);
+    /// }
+    /// ->
+    /// fn f(t : texture_2d<f32>, tint_symbol : u32) -> u32 {
+    ///   return tint_symbol;
+    /// }
+    Hashmap<const sem::Function*, Hashmap<const ast::Parameter*, FunctionExtraParamInfo, 4>, 8>
+        fn_to_data;
+
+    /// For each callsite of the above functions, record a vector of extra call args that need
+    /// inserting. e.g. f(tex)
+    /// ->
+    /// f(tex, internal_uniform.texture_builtin_value), if tex is from a global
+    /// variable, store the BindingPoint. or f(tex, extra_param_tex), if tex is from a function
+    /// param, store the texture function parameter pointer.
+    Hashmap<const CallExpression*, Vector<std::variant<BindingPoint, const ast::Parameter*>, 4>, 8>
+        call_to_data;
+
+    /// Texture builtin calls to be replaced by either uniform values or function parameters.
+    Hashmap<const CallExpression*, std::variant<BindingPoint, const ast::Parameter*>, 8>
+        builtin_to_replace;
+
+    /// A map from global texture bindpoint to the symbol storing its builtin value in the uniform
+    /// buffer struct.
+    Hashmap<BindingPoint, Symbol, 16> bindpoint_to_syms;
+
+    /// The internal uniform buffer
+    const Variable* ubo = nullptr;
+    /// Get or create a UBO including u32 scalars for texture builtin values.
+    /// @returns the symbol of the uniform buffer variable.
+    Symbol GetUboSym() {
+        if (ubo) {
+            // Already created
+            return ubo->name->symbol;
+        }
+
+        auto* cfg = inputs.Get<Config>();
+
+        Vector<const ast::StructMember*, 16> new_members;
+        new_members.Resize(bindpoint_to_data.size());
+        for (auto it : bindpoint_to_data) {
+            // Emit a u32 scalar for each binding that needs builtin value passed in.
+            size_t i = it.second.second / sizeof(uint32_t);
+            TINT_ASSERT(i < new_members.Length());
+            // Append the vector index with the variable name to avoid unstable naming issue.
+            auto sym = b.Symbols().New(std::string(kTextureBuiltinValuesMemberNamePrefix) +
+                                       std::to_string(i));
+            bindpoint_to_syms.Add(it.first, sym);
+            new_members[i] = b.Member(sym, b.ty.u32());
+        }
+
+        // Find if there's any existing global variable using the same cfg->ubo_binding
+        for (auto* var : src->AST().Globals<Var>()) {
+            if (var->HasBindingPoint()) {
+                auto* global_sem = sem.Get<sem::GlobalVariable>(var);
+
+                // The original binding point
+                BindingPoint binding_point = *global_sem->BindingPoint();
+
+                if (binding_point == cfg->ubo_binding) {
+                    // This ubo_binding struct already exists.
+                    // which should only be added by other *FromUniform transforms.
+                    // Replace it with a new struct including the new_member.
+                    // Then remove the old structure global declaration.
+
+                    ubo = var->As<Variable>();
+
+                    auto* ty = global_sem->Type()->UnwrapRef();
+                    auto* str = ty->As<sem::Struct>();
+                    if (TINT_UNLIKELY(!str)) {
+                        TINT_ICE()
+                            << "existing ubo binding " << cfg->ubo_binding << " is not a struct.";
+                        return ctx.Clone(ubo->name->symbol);
+                    }
+
+                    for (auto new_member : new_members) {
+                        ctx.InsertBack(str->Declaration()->members, new_member);
+                    }
+                    return ctx.Clone(ubo->name->symbol);
+                }
+            }
+        }
+
+        auto* buffer_struct = b.Structure(b.Sym(), std::move(new_members));
+        ubo = b.GlobalVar(b.Sym(), b.ty.Of(buffer_struct), core::AddressSpace::kUniform,
+                          b.Group(core::AInt(cfg->ubo_binding.group)),
+                          b.Binding(core::AInt(cfg->ubo_binding.binding)));
+        return ubo->name->symbol;
+    }
+
+    /// Get the expression of retrieving the builtin value from the uniform buffer.
+    /// @param binding of the global variable.
+    /// @returns an expression of the builtin value.
+    const ast::Expression* GetUniformValue(const BindingPoint& binding) {
+        auto iter = bindpoint_to_data.find(binding);
+        TINT_ASSERT(iter != bindpoint_to_data.end());
+
+        // Make sure GetUboSym() is called first to initialize the uniform buffer struct.
+        auto ubo_sym = GetUboSym();
+        // Load the builtin value from the UBO.
+        auto member_sym = bindpoint_to_syms.Get(binding);
+        TINT_ASSERT(member_sym.has_value());
+        auto* builtin_value = b.MemberAccessor(ubo_sym, *member_sym);
+
+        return builtin_value;
+    }
+
+    /// Get and return the binding of the global texture variable. Record in bindpoint_to_data if
+    /// first visited.
+    /// @param global global variable of the texture variable.
+    /// @param field type of the interested builtin function data related to this texture.
+    /// @returns binding of the global variable.
+    BindingPoint GetAndRecordGlobalBinding(const sem::GlobalVariable* global,
+                                           TextureBuiltinsFromUniformOptions::Field field) {
+        auto binding = global->BindingPoint().value();
+        auto iter = bindpoint_to_data.find(binding);
+        if (iter == bindpoint_to_data.end()) {
+            // First visit, recording the binding.
+            uint32_t index = static_cast<uint32_t>(bindpoint_to_data.size());
+            bindpoint_to_data.emplace(
+                binding,
+                Result::FieldAndOffset{field, index * static_cast<uint32_t>(sizeof(uint32_t))});
+        }
+        return binding;
+    }
+
+    /// Find which function param is the given texture variable.
+    /// Add a new u32 param relates to this texture param. Record in fn_to_data if first visited.
+    /// @param fn the current function scope.
+    /// @param var the texture variable.
+    /// @param field type of the interested builtin function data related to this texture.
+    /// @returns the new u32 function parameter.
+    const ast::Parameter* GetAndRecordFunctionParameter(
+        const sem::Function* fn,
+        const sem::Variable* var,
+        TextureBuiltinsFromUniformOptions::Field field) {
+        auto& param_to_info = fn_to_data.GetOrCreate(
+            fn, [&] { return Hashmap<const ast::Parameter*, FunctionExtraParamInfo, 4>(); });
+
+        const ast::Parameter* param = nullptr;
+        for (auto p : fn->Declaration()->params) {
+            if (p->As<Variable>() == var->Declaration()) {
+                param = p;
+                break;
+            }
+        }
+        TINT_ASSERT(param);
+        // Get or record a new u32 param to this function if first visited.
+        auto entry = param_to_info.Get(param);
+        if (entry.has_value()) {
+            return entry->param;
+        }
+        const ast::Parameter* new_param = b.Param(b.Sym(), b.ty.u32());
+        size_t idx = param_to_info.Count();
+        param_to_info.Add(param, FunctionExtraParamInfo{field, new_param, idx});
+        return new_param;
+    }
+
+    /// Get the uniform options field for the builtin function.
+    /// @param type of the builtin function
+    /// @returns corresponding TextureBuiltinsFromUniformOptions::Field for the builtin
+    static TextureBuiltinsFromUniformOptions::Field GetFieldFromBuiltinFunctionType(
+        core::Function type) {
+        switch (type) {
+            case core::Function::kTextureNumLevels:
+                return TextureBuiltinsFromUniformOptions::Field::TextureNumLevels;
+            case core::Function::kTextureNumSamples:
+                return TextureBuiltinsFromUniformOptions::Field::TextureNumSamples;
+            default:
+                TINT_UNREACHABLE() << "unsupported builtin function type " << type;
+        }
+        return TextureBuiltinsFromUniformOptions::Field::TextureNumLevels;
+    }
+};
+
+Transform::ApplyResult TextureBuiltinsFromUniform::Apply(const Program* src,
+                                                         const DataMap& inputs,
+                                                         DataMap& outputs) const {
+    return State{src, inputs, outputs}.Run();
+}
+
+TextureBuiltinsFromUniform::Config::Config(BindingPoint ubo_bp) : ubo_binding(ubo_bp) {}
+TextureBuiltinsFromUniform::Config::Config(const Config&) = default;
+TextureBuiltinsFromUniform::Config& TextureBuiltinsFromUniform::Config::operator=(const Config&) =
+    default;
+TextureBuiltinsFromUniform::Config::~Config() = default;
+
+TextureBuiltinsFromUniform::Result::Result(BindingPointToFieldAndOffset bindpoint_to_data_in)
+    : bindpoint_to_data(std::move(bindpoint_to_data_in)) {}
+TextureBuiltinsFromUniform::Result::Result(const Result&) = default;
+TextureBuiltinsFromUniform::Result::~Result() = default;
+
+}  // namespace tint::ast::transform
diff --git a/src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform.h b/src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform.h
new file mode 100644
index 0000000..0db043e
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform.h
@@ -0,0 +1,119 @@
+// Copyright 2023 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_TINT_LANG_WGSL_AST_TRANSFORM_TEXTURE_BUILTINS_FROM_UNIFORM_H_
+#define SRC_TINT_LANG_WGSL_AST_TRANSFORM_TEXTURE_BUILTINS_FROM_UNIFORM_H_
+
+#include <unordered_map>
+#include <unordered_set>
+
+#include "src/tint/api/common/binding_point.h"
+#include "src/tint/api/options/texture_builtins_from_uniform.h"
+#include "src/tint/lang/wgsl/ast/transform/transform.h"
+
+// Forward declarations
+namespace tint {
+class CloneContext;
+}  // namespace tint
+
+namespace tint::ast::transform {
+
+/// TextureBuiltinsFromUniform is a transform that implements calls to textureNumLevels() and
+/// textureNumSamples() by retrieving the texture information from a uniform buffer, as those
+/// builtin functions are not available in some version of GLSL.
+///
+/// The generated uniform buffer will have the form:
+/// ```
+/// struct internal_uniform {
+///  texture_builtin_value_0 : u32,
+/// };
+///
+/// @group(0) @binding(0) var tex : texture_2d<f32>;
+/// ```
+/// The binding group and number used for this uniform buffer are provided via
+/// the `Config` transform input.
+///
+/// The transform coverts the texture builtins calls into values lookup from the internal
+/// buffer. If the texture is a function parameter instead of a global variable, this transform
+/// also takes care of adding extra paramters and arguments to these functions and their callsites.
+///
+/// This transform must run before `CombineSamplers` transform so that the binding point of the
+/// original texture object can be preserved.
+class TextureBuiltinsFromUniform final : public Castable<TextureBuiltinsFromUniform, Transform> {
+  public:
+    /// Constructor
+    TextureBuiltinsFromUniform();
+    /// Destructor
+    ~TextureBuiltinsFromUniform() override;
+
+    /// Configuration options for the TextureBuiltinsFromUniform transform.
+    struct Config final : public Castable<Config, Data> {
+        /// Constructor
+        /// @param ubo_bp the binding point to use for the generated uniform buffer.
+        explicit Config(BindingPoint ubo_bp);
+
+        /// Copy constructor
+        Config(const Config&);
+
+        /// Copy assignment
+        /// @return this Config
+        Config& operator=(const Config&);
+
+        /// Destructor
+        ~Config() override;
+
+        /// The binding point to use for the generated uniform buffer.
+        BindingPoint ubo_binding;
+    };
+
+    /// Information produced about what the transform did.
+    /// If there were no calls to the textureNumLevels() or textureNumSamples() builtin, then no
+    /// Result will be emitted.
+    struct Result final : public Castable<Result, Data> {
+        /// Using for shorter names
+        /// Records the field and the byte offset of the data to push in the internal uniform
+        /// buffer.
+        using FieldAndOffset = TextureBuiltinsFromUniformOptions::FieldAndOffset;
+        /// Maps from binding point to data entry with the information to populate the data.
+        using BindingPointToFieldAndOffset =
+            TextureBuiltinsFromUniformOptions::BindingPointToFieldAndOffset;
+
+        /// Constructor
+        /// @param bindpoint_to_data_in mapping from binding points of global texture variables to
+        /// the byte offsets and data types needed to be pushed into the internal uniform buffer.
+        explicit Result(BindingPointToFieldAndOffset bindpoint_to_data_in);
+
+        /// Copy constructor
+        Result(const Result&);
+
+        /// Destructor
+        ~Result() override;
+
+        /// A map of global texture variable binding point to the byte offset and data type to push
+        /// into the internal uniform buffer.
+        BindingPointToFieldAndOffset bindpoint_to_data;
+    };
+
+    /// @copydoc Transform::Apply
+    ApplyResult Apply(const Program* program,
+                      const DataMap& inputs,
+                      DataMap& outputs) const override;
+
+  private:
+    struct State;
+};
+
+}  // namespace tint::ast::transform
+
+#endif  // SRC_TINT_LANG_WGSL_AST_TRANSFORM_TEXTURE_BUILTINS_FROM_UNIFORM_H_
diff --git a/src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform_test.cc b/src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform_test.cc
new file mode 100644
index 0000000..21185e8
--- /dev/null
+++ b/src/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform_test.cc
@@ -0,0 +1,687 @@
+// Copyright 2023 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/tint/lang/wgsl/ast/transform/texture_builtins_from_uniform.h"
+
+#include <utility>
+
+#include "src/tint/lang/wgsl/ast/transform/helper_test.h"
+
+namespace tint::ast::transform {
+namespace {
+
+using TextureBuiltinsFromUniformTest = TransformTest;
+
+TEST_F(TextureBuiltinsFromUniformTest, ShouldRunEmptyModule) {
+    auto* src = R"()";
+
+    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+
+    DataMap data;
+    data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
+
+    EXPECT_FALSE(ShouldRun<TextureBuiltinsFromUniform>(src, data));
+}
+
+TEST_F(TextureBuiltinsFromUniformTest, ShouldRunNoTextureNumLevels) {
+    auto* src = R"(
+@group(0) @binding(0) var t : texture_2d<f32>;
+
+@compute @workgroup_size(1)
+fn main() {
+  _ = textureDimensions(t);
+}
+)";
+
+    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+
+    DataMap data;
+    data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
+
+    EXPECT_FALSE(ShouldRun<TextureBuiltinsFromUniform>(src, data));
+}
+
+TEST_F(TextureBuiltinsFromUniformTest, ShouldRunWithTextureNumLevels) {
+    auto* src = R"(
+@group(0) @binding(0) var t : texture_2d<f32>;
+
+@compute @workgroup_size(1)
+fn main() {
+  var len : u32 = textureNumLevels(t);
+}
+)";
+
+    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+
+    DataMap data;
+    data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
+
+    EXPECT_TRUE(ShouldRun<TextureBuiltinsFromUniform>(src, data));
+}
+
+TEST_F(TextureBuiltinsFromUniformTest, Error_MissingTransformData) {
+    auto* src = R"(
+@group(0) @binding(0) var t : texture_2d<f32>;
+
+@compute @workgroup_size(1)
+fn main() {
+  var len : u32 = textureNumLevels(t);
+}
+)";
+
+    auto* expect =
+        "error: missing transform data for tint::ast::transform::TextureBuiltinsFromUniform";
+
+    auto got = Run<TextureBuiltinsFromUniform>(src);
+
+    EXPECT_EQ(expect, str(got));
+}
+
+TEST_F(TextureBuiltinsFromUniformTest, BasicTextureNumLevels) {
+    auto* src = R"(
+@group(0) @binding(0) var t : texture_2d<f32>;
+
+@compute @workgroup_size(1)
+fn main() {
+  var len : u32 = textureNumLevels(t);
+}
+)";
+
+    auto* expect = R"(
+struct tint_symbol {
+  texture_builtin_value_0 : u32,
+}
+
+@group(0) @binding(30) var<uniform> tint_symbol_1 : tint_symbol;
+
+@group(0) @binding(0) var t : texture_2d<f32>;
+
+@compute @workgroup_size(1)
+fn main() {
+  var len : u32 = tint_symbol_1.texture_builtin_value_0;
+}
+)";
+
+    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+
+    DataMap data;
+    data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
+
+    auto got = Run<TextureBuiltinsFromUniform>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
+    ASSERT_NE(val, nullptr);
+    // Note: Using the following EXPECT_EQ directly on BindingPointToFieldAndOffset seems to cause
+    // compiler to hang. EXPECT_EQ(
+    //     TextureBuiltinsFromUniformOptions::BindingPointToFieldAndOffset{
+    //         {BindgPoint{0u, 0u},
+    //          std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 0u)}},
+    //     val->bindpoint_to_data);
+    EXPECT_EQ(1u, val->bindpoint_to_data.size());
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 0u),
+              val->bindpoint_to_data.at(BindingPoint{0, 0}));
+}
+
+TEST_F(TextureBuiltinsFromUniformTest, BasicTextureNumSamples) {
+    auto* src = R"(
+@group(0) @binding(0) var t : texture_multisampled_2d<f32>;
+
+@compute @workgroup_size(1)
+fn main() {
+  var samples : u32 = textureNumSamples(t);
+}
+)";
+
+    auto* expect = R"(
+struct tint_symbol {
+  texture_builtin_value_0 : u32,
+}
+
+@group(0) @binding(30) var<uniform> tint_symbol_1 : tint_symbol;
+
+@group(0) @binding(0) var t : texture_multisampled_2d<f32>;
+
+@compute @workgroup_size(1)
+fn main() {
+  var samples : u32 = tint_symbol_1.texture_builtin_value_0;
+}
+)";
+
+    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+
+    DataMap data;
+    data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
+
+    auto got = Run<TextureBuiltinsFromUniform>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
+    ASSERT_NE(val, nullptr);
+    EXPECT_EQ(1u, val->bindpoint_to_data.size());
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumSamples, 0u),
+              val->bindpoint_to_data.at(BindingPoint{0, 0}));
+}
+
+TEST_F(TextureBuiltinsFromUniformTest, SameBuiltinCalledMultipleTimes) {
+    auto* src = R"(
+@group(0) @binding(0) var tex : texture_2d<f32>;
+
+@compute @workgroup_size(1)
+fn main() {
+  var len : u32 = textureNumLevels(tex);
+  len = textureNumLevels(tex);
+}
+)";
+
+    auto* expect = R"(
+struct tint_symbol {
+  texture_builtin_value_0 : u32,
+}
+
+@group(0) @binding(30) var<uniform> tint_symbol_1 : tint_symbol;
+
+@group(0) @binding(0) var tex : texture_2d<f32>;
+
+@compute @workgroup_size(1)
+fn main() {
+  var len : u32 = tint_symbol_1.texture_builtin_value_0;
+  len = tint_symbol_1.texture_builtin_value_0;
+}
+)";
+
+    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+
+    DataMap data;
+    data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
+
+    auto got = Run<TextureBuiltinsFromUniform>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
+    ASSERT_NE(val, nullptr);
+    EXPECT_EQ(1u, val->bindpoint_to_data.size());
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 0u),
+              val->bindpoint_to_data.at(BindingPoint{0, 0}));
+}
+
+TEST_F(TextureBuiltinsFromUniformTest, SameBuiltinCalledMultipleTimesTextureNumSamples) {
+    auto* src = R"(
+@group(0) @binding(0) var tex : texture_multisampled_2d<f32>;
+
+@compute @workgroup_size(1)
+fn main() {
+  var len : u32 = textureNumSamples(tex);
+  len = textureNumSamples(tex);
+}
+)";
+
+    auto* expect = R"(
+struct tint_symbol {
+  texture_builtin_value_0 : u32,
+}
+
+@group(0) @binding(30) var<uniform> tint_symbol_1 : tint_symbol;
+
+@group(0) @binding(0) var tex : texture_multisampled_2d<f32>;
+
+@compute @workgroup_size(1)
+fn main() {
+  var len : u32 = tint_symbol_1.texture_builtin_value_0;
+  len = tint_symbol_1.texture_builtin_value_0;
+}
+)";
+
+    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+
+    DataMap data;
+    data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
+
+    auto got = Run<TextureBuiltinsFromUniform>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
+    ASSERT_NE(val, nullptr);
+    EXPECT_EQ(1u, val->bindpoint_to_data.size());
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumSamples, 0u),
+              val->bindpoint_to_data.at(BindingPoint{0, 0}));
+}
+
+TEST_F(TextureBuiltinsFromUniformTest, TextureAsFunctionParameterBasic) {
+    auto* src = R"(
+@group(0) @binding(0) var tex : texture_2d<f32>;
+
+fn f(t: texture_2d<f32>) -> u32 {
+  return textureNumLevels(t);
+}
+
+@compute @workgroup_size(1)
+fn main() {
+  var len : u32 = f(tex);
+}
+)";
+
+    auto* expect = R"(
+struct tint_symbol_1 {
+  texture_builtin_value_0 : u32,
+}
+
+@group(0) @binding(30) var<uniform> tint_symbol_2 : tint_symbol_1;
+
+@group(0) @binding(0) var tex : texture_2d<f32>;
+
+fn f(t : texture_2d<f32>, tint_symbol : u32) -> u32 {
+  return tint_symbol;
+}
+
+@compute @workgroup_size(1)
+fn main() {
+  var len : u32 = f(tex, tint_symbol_2.texture_builtin_value_0);
+}
+)";
+
+    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+
+    DataMap data;
+    data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
+
+    auto got = Run<TextureBuiltinsFromUniform>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
+    ASSERT_NE(val, nullptr);
+    EXPECT_EQ(1u, val->bindpoint_to_data.size());
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 0u),
+              val->bindpoint_to_data.at(BindingPoint{0, 0}));
+}
+
+TEST_F(TextureBuiltinsFromUniformTest, TextureAsFunctionParameterUsedTwice) {
+    auto* src = R"(
+@group(0) @binding(0) var tex : texture_2d<f32>;
+
+fn f(t: texture_2d<f32>) -> u32 {
+  var len = textureNumLevels(t);
+  len += textureNumLevels(t);
+  return len;
+}
+
+@compute @workgroup_size(1)
+fn main() {
+  var len : u32 = f(tex);
+}
+)";
+
+    auto* expect = R"(
+struct tint_symbol_1 {
+  texture_builtin_value_0 : u32,
+}
+
+@group(0) @binding(30) var<uniform> tint_symbol_2 : tint_symbol_1;
+
+@group(0) @binding(0) var tex : texture_2d<f32>;
+
+fn f(t : texture_2d<f32>, tint_symbol : u32) -> u32 {
+  var len = tint_symbol;
+  len += tint_symbol;
+  return len;
+}
+
+@compute @workgroup_size(1)
+fn main() {
+  var len : u32 = f(tex, tint_symbol_2.texture_builtin_value_0);
+}
+)";
+
+    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+
+    DataMap data;
+    data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
+
+    auto got = Run<TextureBuiltinsFromUniform>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
+    ASSERT_NE(val, nullptr);
+    EXPECT_EQ(1u, val->bindpoint_to_data.size());
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 0u),
+              val->bindpoint_to_data.at(BindingPoint{0, 0}));
+}
+
+TEST_F(TextureBuiltinsFromUniformTest, TextureAsFunctionParameterMultipleParameters) {
+    auto* src = R"(
+@group(0) @binding(0) var tex1 : texture_2d<f32>;
+@group(0) @binding(1) var tex2 : texture_2d<f32>;
+@group(0) @binding(2) var tex3 : texture_2d<f32>;
+
+fn f(t1: texture_2d<f32>, t2: texture_2d<f32>, t3: texture_2d<f32>) -> u32 {
+  return textureNumLevels(t1) + textureNumLevels(t2) + textureNumLevels(t3);
+}
+
+@compute @workgroup_size(1)
+fn main() {
+  var len : u32 = f(tex1, tex2, tex3);
+}
+)";
+
+    auto* expect = R"(
+struct tint_symbol_3 {
+  texture_builtin_value_0 : u32,
+  texture_builtin_value_1 : u32,
+  texture_builtin_value_2 : u32,
+}
+
+@group(0) @binding(30) var<uniform> tint_symbol_4 : tint_symbol_3;
+
+@group(0) @binding(0) var tex1 : texture_2d<f32>;
+
+@group(0) @binding(1) var tex2 : texture_2d<f32>;
+
+@group(0) @binding(2) var tex3 : texture_2d<f32>;
+
+fn f(t1 : texture_2d<f32>, t2 : texture_2d<f32>, t3 : texture_2d<f32>, tint_symbol : u32, tint_symbol_1 : u32, tint_symbol_2 : u32) -> u32 {
+  return ((tint_symbol + tint_symbol_1) + tint_symbol_2);
+}
+
+@compute @workgroup_size(1)
+fn main() {
+  var len : u32 = f(tex1, tex2, tex3, tint_symbol_4.texture_builtin_value_0, tint_symbol_4.texture_builtin_value_1, tint_symbol_4.texture_builtin_value_2);
+}
+)";
+
+    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+
+    DataMap data;
+    data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
+
+    auto got = Run<TextureBuiltinsFromUniform>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
+    ASSERT_NE(val, nullptr);
+    EXPECT_EQ(3u, val->bindpoint_to_data.size());
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 0u),
+              val->bindpoint_to_data.at(BindingPoint{0, 0}));
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 4u),
+              val->bindpoint_to_data.at(BindingPoint{0, 1}));
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 8u),
+              val->bindpoint_to_data.at(BindingPoint{0, 2}));
+}
+
+TEST_F(TextureBuiltinsFromUniformTest, TextureAsFunctionParameterNested) {
+    auto* src = R"(
+@group(0) @binding(0) var tex : texture_2d<f32>;
+
+fn f2(tt: texture_2d<f32>) -> u32 {
+  return textureNumLevels(tt);
+}
+
+fn f1(t: texture_2d<f32>) -> u32 {
+  return f2(t);
+}
+
+@compute @workgroup_size(1)
+fn main() {
+  var len : u32 = f1(tex);
+}
+)";
+
+    auto* expect = R"(
+struct tint_symbol_2 {
+  texture_builtin_value_0 : u32,
+}
+
+@group(0) @binding(30) var<uniform> tint_symbol_3 : tint_symbol_2;
+
+@group(0) @binding(0) var tex : texture_2d<f32>;
+
+fn f2(tt : texture_2d<f32>, tint_symbol : u32) -> u32 {
+  return tint_symbol;
+}
+
+fn f1(t : texture_2d<f32>, tint_symbol_1 : u32) -> u32 {
+  return f2(t, tint_symbol_1);
+}
+
+@compute @workgroup_size(1)
+fn main() {
+  var len : u32 = f1(tex, tint_symbol_3.texture_builtin_value_0);
+}
+)";
+
+    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+
+    DataMap data;
+    data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
+
+    auto got = Run<TextureBuiltinsFromUniform>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
+    ASSERT_NE(val, nullptr);
+    EXPECT_EQ(1u, val->bindpoint_to_data.size());
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 0u),
+              val->bindpoint_to_data.at(BindingPoint{0, 0}));
+}
+
+TEST_F(TextureBuiltinsFromUniformTest, TextureAsFunctionParameterMixed) {
+    auto* src = R"(
+@group(0) @binding(0) var tex0 : texture_2d<f32>;
+@group(0) @binding(1) var tex1 : texture_2d<f32>;
+@group(0) @binding(2) var tex2 : texture_2d<f32>;
+@group(0) @binding(3) var tex3 : texture_2d<f32>;
+@group(0) @binding(4) var tex4 : texture_2d_array<f32>; // unused for textureNumLevels
+
+fn f_nested(t1: texture_2d<f32>, t2: texture_2d<f32>) -> u32 {
+  return textureNumLevels(t1) + textureNumLevels(t2);
+}
+
+fn f1(a: u32, t: texture_2d<f32>) -> u32 {
+  return a + f_nested(t, tex1) + textureNumLevels(tex3);
+}
+
+@compute @workgroup_size(1)
+fn main() {
+  _ = textureNumLayers(tex4);
+  _ = f1(9u, tex0);
+  _ = f_nested(tex2, tex2);
+  _ = f_nested(tex1, tex0);
+}
+)";
+
+    auto* expect = R"(
+struct tint_symbol_3 {
+  texture_builtin_value_0 : u32,
+  texture_builtin_value_1 : u32,
+  texture_builtin_value_2 : u32,
+  texture_builtin_value_3 : u32,
+}
+
+@group(0) @binding(30) var<uniform> tint_symbol_4 : tint_symbol_3;
+
+@group(0) @binding(0) var tex0 : texture_2d<f32>;
+
+@group(0) @binding(1) var tex1 : texture_2d<f32>;
+
+@group(0) @binding(2) var tex2 : texture_2d<f32>;
+
+@group(0) @binding(3) var tex3 : texture_2d<f32>;
+
+@group(0) @binding(4) var tex4 : texture_2d_array<f32>;
+
+fn f_nested(t1 : texture_2d<f32>, t2 : texture_2d<f32>, tint_symbol : u32, tint_symbol_1 : u32) -> u32 {
+  return (tint_symbol + tint_symbol_1);
+}
+
+fn f1(a : u32, t : texture_2d<f32>, tint_symbol_2 : u32) -> u32 {
+  return ((a + f_nested(t, tex1, tint_symbol_2, tint_symbol_4.texture_builtin_value_0)) + tint_symbol_4.texture_builtin_value_1);
+}
+
+@compute @workgroup_size(1)
+fn main() {
+  _ = textureNumLayers(tex4);
+  _ = f1(9u, tex0, tint_symbol_4.texture_builtin_value_2);
+  _ = f_nested(tex2, tex2, tint_symbol_4.texture_builtin_value_3, tint_symbol_4.texture_builtin_value_3);
+  _ = f_nested(tex1, tex0, tint_symbol_4.texture_builtin_value_0, tint_symbol_4.texture_builtin_value_2);
+}
+)";
+
+    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+
+    DataMap data;
+    data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
+
+    auto got = Run<TextureBuiltinsFromUniform>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
+    ASSERT_NE(val, nullptr);
+    EXPECT_EQ(4u, val->bindpoint_to_data.size());
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 0u),
+              val->bindpoint_to_data.at(BindingPoint{0, 1}));
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 4u),
+              val->bindpoint_to_data.at(BindingPoint{0, 3}));
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 8u),
+              val->bindpoint_to_data.at(BindingPoint{0, 0}));
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 12u),
+              val->bindpoint_to_data.at(BindingPoint{0, 2}));
+}
+
+TEST_F(TextureBuiltinsFromUniformTest, MultipleTextures) {
+    auto* src = R"(
+@group(0) @binding(0) var t0 : texture_2d<f32>;
+@group(0) @binding(1) var t1 : texture_multisampled_2d<f32>;
+@group(0) @binding(2) var t2 : texture_2d_array<f32>;
+@group(0) @binding(3) var t3 : texture_cube<f32>;
+@group(0) @binding(4) var t4 : texture_depth_2d;
+@group(1) @binding(0) var t5 : texture_depth_multisampled_2d;
+
+@compute @workgroup_size(1)
+fn main() {
+  _ = textureNumLevels(t0);
+  _ = textureNumSamples(t1);
+  _ = textureNumLevels(t2);
+  _ = textureNumLevels(t3);
+  _ = textureNumLevels(t4);
+  _ = textureNumSamples(t5);
+}
+)";
+
+    auto* expect = R"(
+struct tint_symbol {
+  texture_builtin_value_0 : u32,
+  texture_builtin_value_1 : u32,
+  texture_builtin_value_2 : u32,
+  texture_builtin_value_3 : u32,
+  texture_builtin_value_4 : u32,
+  texture_builtin_value_5 : u32,
+}
+
+@group(0) @binding(30) var<uniform> tint_symbol_1 : tint_symbol;
+
+@group(0) @binding(0) var t0 : texture_2d<f32>;
+
+@group(0) @binding(1) var t1 : texture_multisampled_2d<f32>;
+
+@group(0) @binding(2) var t2 : texture_2d_array<f32>;
+
+@group(0) @binding(3) var t3 : texture_cube<f32>;
+
+@group(0) @binding(4) var t4 : texture_depth_2d;
+
+@group(1) @binding(0) var t5 : texture_depth_multisampled_2d;
+
+@compute @workgroup_size(1)
+fn main() {
+  _ = tint_symbol_1.texture_builtin_value_0;
+  _ = tint_symbol_1.texture_builtin_value_1;
+  _ = tint_symbol_1.texture_builtin_value_2;
+  _ = tint_symbol_1.texture_builtin_value_3;
+  _ = tint_symbol_1.texture_builtin_value_4;
+  _ = tint_symbol_1.texture_builtin_value_5;
+}
+)";
+
+    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+
+    DataMap data;
+    data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
+
+    auto got = Run<TextureBuiltinsFromUniform>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
+    ASSERT_NE(val, nullptr);
+    EXPECT_EQ(6u, val->bindpoint_to_data.size());
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 0u),
+              val->bindpoint_to_data.at(BindingPoint{0, 0}));
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumSamples, 4u),
+              val->bindpoint_to_data.at(BindingPoint{0, 1}));
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 8u),
+              val->bindpoint_to_data.at(BindingPoint{0, 2}));
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 12u),
+              val->bindpoint_to_data.at(BindingPoint{0, 3}));
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 16u),
+              val->bindpoint_to_data.at(BindingPoint{0, 4}));
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumSamples, 20u),
+              val->bindpoint_to_data.at(BindingPoint{1, 0}));
+}
+
+TEST_F(TextureBuiltinsFromUniformTest, BindingPointExist) {
+    auto* src = R"(
+struct tint_symbol {
+  foo : array<vec4<u32>, 1u>,
+}
+
+@group(0) @binding(30) var<uniform> tint_symbol_1 : tint_symbol;
+
+@group(0) @binding(0) var t : texture_2d<f32>;
+
+@compute @workgroup_size(1)
+fn main() {
+  var len : u32 = textureNumLevels(t);
+}
+)";
+
+    auto* expect = R"(
+struct tint_symbol {
+  foo : array<vec4<u32>, 1u>,
+  texture_builtin_value_0 : u32,
+}
+
+@group(0) @binding(30) var<uniform> tint_symbol_1 : tint_symbol;
+
+@group(0) @binding(0) var t : texture_2d<f32>;
+
+@compute @workgroup_size(1)
+fn main() {
+  var len : u32 = tint_symbol_1.texture_builtin_value_0;
+}
+)";
+
+    TextureBuiltinsFromUniform::Config cfg({0, 30u});
+
+    DataMap data;
+    data.Add<TextureBuiltinsFromUniform::Config>(std::move(cfg));
+
+    auto got = Run<TextureBuiltinsFromUniform>(src, data);
+
+    EXPECT_EQ(expect, str(got));
+    auto* val = got.data.Get<TextureBuiltinsFromUniform::Result>();
+    ASSERT_NE(val, nullptr);
+    EXPECT_EQ(std::make_pair(TextureBuiltinsFromUniformOptions::Field::TextureNumLevels, 0u),
+              val->bindpoint_to_data.at(BindingPoint{0, 0}));
+}
+
+}  // namespace
+}  // namespace tint::ast::transform
diff --git a/src/tint/utils/BUILD.cmake b/src/tint/utils/BUILD.cmake
index b4c50f5..0f7a6a9 100644
--- a/src/tint/utils/BUILD.cmake
+++ b/src/tint/utils/BUILD.cmake
@@ -35,6 +35,7 @@
 include(utils/reflection/BUILD.cmake)
 include(utils/result/BUILD.cmake)
 include(utils/rtti/BUILD.cmake)
+include(utils/socket/BUILD.cmake)
 include(utils/strconv/BUILD.cmake)
 include(utils/symbol/BUILD.cmake)
 include(utils/text/BUILD.cmake)
diff --git a/src/tint/utils/macros/compiler.h b/src/tint/utils/macros/compiler.h
index f0af760..6d31ca8 100644
--- a/src/tint/utils/macros/compiler.h
+++ b/src/tint/utils/macros/compiler.h
@@ -31,6 +31,7 @@
 #define TINT_DISABLE_WARNING_UNREACHABLE_CODE __pragma(warning(disable : 4702))
 #define TINT_DISABLE_WARNING_WEAK_VTABLES /* currently no-op */
 #define TINT_DISABLE_WARNING_FLOAT_EQUAL  /* currently no-op */
+#define TINT_DISABLE_WARNING_DEPRECATED __pragma(warning(disable : 4996))
 
 // clang-format off
 #define TINT_BEGIN_DISABLE_WARNING(name)     \
@@ -57,6 +58,7 @@
 #define TINT_DISABLE_WARNING_UNREACHABLE_CODE /* currently no-op */
 #define TINT_DISABLE_WARNING_WEAK_VTABLES _Pragma("clang diagnostic ignored \"-Wweak-vtables\"")
 #define TINT_DISABLE_WARNING_FLOAT_EQUAL _Pragma("clang diagnostic ignored \"-Wfloat-equal\"")
+#define TINT_DISABLE_WARNING_DEPRECATED /* currently no-op */
 
 // clang-format off
 #define TINT_BEGIN_DISABLE_WARNING(name)     \
@@ -83,6 +85,7 @@
 #define TINT_DISABLE_WARNING_UNREACHABLE_CODE /* currently no-op */
 #define TINT_DISABLE_WARNING_WEAK_VTABLES     /* currently no-op */
 #define TINT_DISABLE_WARNING_FLOAT_EQUAL      /* currently no-op */
+#define TINT_DISABLE_WARNING_DEPRECATED       /* currently no-op */
 
 // clang-format off
 #define TINT_BEGIN_DISABLE_WARNING(name)     \
diff --git a/src/tint/utils/socket/BUILD.cmake b/src/tint/utils/socket/BUILD.cmake
new file mode 100644
index 0000000..84577fa
--- /dev/null
+++ b/src/tint/utils/socket/BUILD.cmake
@@ -0,0 +1,37 @@
+# Copyright 2023 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.
+
+################################################################################
+# File generated by tools/src/cmd/gen
+# using the template:
+#   tools/src/cmd/gen/build/BUILD.cmake.tmpl
+#
+# Do not modify this file directly
+################################################################################
+
+################################################################################
+# Target:    tint_utils_socket
+# Kind:      lib
+################################################################################
+tint_add_target(tint_utils_socket lib
+  utils/socket/rwmutex.h
+  utils/socket/socket.cc
+  utils/socket/socket.h
+)
+
+if(IS_WIN)
+  tint_target_add_external_dependencies(tint_utils_socket lib
+    "winsock"
+  )
+endif(IS_WIN)
diff --git a/src/tint/utils/socket/BUILD.gn b/src/tint/utils/socket/BUILD.gn
new file mode 100644
index 0000000..7f0bba4
--- /dev/null
+++ b/src/tint/utils/socket/BUILD.gn
@@ -0,0 +1,38 @@
+# Copyright 2023 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.
+
+################################################################################
+# File generated by tools/src/cmd/gen
+# using the template:
+#   tools/src/cmd/gen/build/BUILD.gn.tmpl
+#
+# Do not modify this file directly
+################################################################################
+
+import("../../../../scripts/tint_overrides_with_defaults.gni")
+
+import("${tint_src_dir}/tint.gni")
+
+libtint_source_set("socket") {
+  sources = [
+    "rwmutex.h",
+    "socket.cc",
+    "socket.h",
+  ]
+  deps = []
+
+  if (is_win) {
+    deps += [ "${tint_src_dir}:winsock" ]
+  }
+}
diff --git a/src/tint/utils/socket/rwmutex.h b/src/tint/utils/socket/rwmutex.h
new file mode 100644
index 0000000..9b010ed
--- /dev/null
+++ b/src/tint/utils/socket/rwmutex.h
@@ -0,0 +1,190 @@
+// Copyright 2020 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.
+
+#ifndef SRC_TINT_UTILS_SOCKET_RWMUTEX_H_
+#define SRC_TINT_UTILS_SOCKET_RWMUTEX_H_
+
+#include <condition_variable>
+#include <mutex>
+
+////////////////////////////////////////////////////////////////////////////////
+// RWMutex
+////////////////////////////////////////////////////////////////////////////////
+
+/// A RWMutex is a reader/writer mutual exclusion lock.
+/// The lock can be held by an arbitrary number of readers or a single writer.
+/// Also known as a shared mutex.
+class RWMutex {
+  public:
+    inline RWMutex() = default;
+
+    /// LockReader() locks the mutex for reading.
+    /// Multiple read locks can be held while there are no writer locks.
+    inline void LockReader();
+
+    /// UnlockReader() unlocks the mutex for reading.
+    inline void UnlockReader();
+
+    /// LockWriter() locks the mutex for writing.
+    /// If the lock is already locked for reading or writing, LockWriter blocks
+    /// until the lock is available.
+    inline void LockWriter();
+
+    /// UnlockWriter() unlocks the mutex for writing.
+    inline void UnlockWriter();
+
+  private:
+    RWMutex(const RWMutex&) = delete;
+    RWMutex& operator=(const RWMutex&) = delete;
+
+    int read_locks = 0;
+    int pending_write_locks = 0;
+    std::mutex mutex;
+    std::condition_variable cv;
+};
+
+void RWMutex::LockReader() {
+    std::unique_lock<std::mutex> lock(mutex);
+    read_locks++;
+}
+
+void RWMutex::UnlockReader() {
+    std::unique_lock<std::mutex> lock(mutex);
+    read_locks--;
+    if (read_locks == 0 && pending_write_locks > 0) {
+        cv.notify_one();
+    }
+}
+
+void RWMutex::LockWriter() {
+    std::unique_lock<std::mutex> lock(mutex);
+    if (read_locks > 0) {
+        pending_write_locks++;
+        cv.wait(lock, [&] { return read_locks == 0; });
+        pending_write_locks--;
+    }
+    lock.release();  // Keep lock held
+}
+
+void RWMutex::UnlockWriter() {
+    if (pending_write_locks > 0) {
+        cv.notify_one();
+    }
+    mutex.unlock();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// RLock
+////////////////////////////////////////////////////////////////////////////////
+
+/// RLock is a RAII read lock helper for a RWMutex.
+class RLock {
+  public:
+    /// Constructor.
+    /// Locks `mutex` with a read-lock for the lifetime of the WLock.
+    /// @param mutex the mutex
+    explicit inline RLock(RWMutex& mutex);
+    /// Destructor.
+    /// Unlocks the RWMutex.
+    inline ~RLock();
+
+    /// Move constructor
+    /// @param other the other RLock to move into this RLock.
+    inline RLock(RLock&& other);
+    /// Move assignment operator
+    /// @param other the other RLock to move into this RLock.
+    /// @returns this RLock so calls can be chained
+    inline RLock& operator=(RLock&& other);
+
+  private:
+    RLock(const RLock&) = delete;
+    RLock& operator=(const RLock&) = delete;
+
+    RWMutex* m;
+};
+
+RLock::RLock(RWMutex& mutex) : m(&mutex) {
+    m->LockReader();
+}
+
+RLock::~RLock() {
+    if (m != nullptr) {
+        m->UnlockReader();
+    }
+}
+
+RLock::RLock(RLock&& other) {
+    m = other.m;
+    other.m = nullptr;
+}
+
+RLock& RLock::operator=(RLock&& other) {
+    m = other.m;
+    other.m = nullptr;
+    return *this;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// WLock
+////////////////////////////////////////////////////////////////////////////////
+
+/// WLock is a RAII write lock helper for a RWMutex.
+class WLock {
+  public:
+    /// Constructor.
+    /// Locks `mutex` with a write-lock for the lifetime of the WLock.
+    /// @param mutex the mutex
+    explicit inline WLock(RWMutex& mutex);
+
+    /// Destructor.
+    /// Unlocks the RWMutex.
+    inline ~WLock();
+
+    /// Move constructor
+    /// @param other the other WLock to move into this WLock.
+    inline WLock(WLock&& other);
+    /// Move assignment operator
+    /// @param other the other WLock to move into this WLock.
+    /// @returns this WLock so calls can be chained
+    inline WLock& operator=(WLock&& other);
+
+  private:
+    WLock(const WLock&) = delete;
+    WLock& operator=(const WLock&) = delete;
+
+    RWMutex* m;
+};
+
+WLock::WLock(RWMutex& mutex) : m(&mutex) {
+    m->LockWriter();
+}
+
+WLock::~WLock() {
+    if (m != nullptr) {
+        m->UnlockWriter();
+    }
+}
+
+WLock::WLock(WLock&& other) {
+    m = other.m;
+    other.m = nullptr;
+}
+
+WLock& WLock::operator=(WLock&& other) {
+    m = other.m;
+    other.m = nullptr;
+    return *this;
+}
+
+#endif  // SRC_TINT_UTILS_SOCKET_RWMUTEX_H_
diff --git a/src/tint/utils/socket/socket.cc b/src/tint/utils/socket/socket.cc
new file mode 100644
index 0000000..54cb462
--- /dev/null
+++ b/src/tint/utils/socket/socket.cc
@@ -0,0 +1,317 @@
+// Copyright 2021 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.
+
+#include "src/tint/utils/socket/socket.h"
+
+#include "src/tint/utils/socket/rwmutex.h"
+
+#if defined(_WIN32)
+#include <winsock2.h>
+#include <ws2tcpip.h>
+#else
+#include <netdb.h>
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <sys/select.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <unistd.h>
+#endif
+
+#if defined(_WIN32)
+#include <atomic>
+namespace {
+std::atomic<int> wsa_init_count = {0};
+}  // anonymous namespace
+#else
+#include <fcntl.h>
+namespace {
+using SOCKET = int;
+}  // anonymous namespace
+#endif
+
+namespace {
+constexpr SOCKET InvalidSocket = static_cast<SOCKET>(-1);
+void Init() {
+#if defined(_WIN32)
+    if (wsa_init_count++ == 0) {
+        WSADATA winsock_data;
+        (void)WSAStartup(MAKEWORD(2, 2), &winsock_data);
+    }
+#endif
+}
+
+void Term() {
+#if defined(_WIN32)
+    if (--wsa_init_count == 0) {
+        WSACleanup();
+    }
+#endif
+}
+
+bool SetBlocking(SOCKET s, bool blocking) {
+#if defined(_WIN32)
+    u_long mode = blocking ? 0 : 1;
+    return ioctlsocket(s, FIONBIO, &mode) == NO_ERROR;
+#else
+    auto arg = fcntl(s, F_GETFL, nullptr);
+    if (arg < 0) {
+        return false;
+    }
+    arg = blocking ? (arg & ~O_NONBLOCK) : (arg | O_NONBLOCK);
+    return fcntl(s, F_SETFL, arg) >= 0;
+#endif
+}
+
+bool Errored(SOCKET s) {
+    if (s == InvalidSocket) {
+        return true;
+    }
+    char error = 0;
+    socklen_t len = sizeof(error);
+    getsockopt(s, SOL_SOCKET, SO_ERROR, &error, &len);
+    return error != 0;
+}
+
+class Impl : public Socket {
+  public:
+    static std::shared_ptr<Impl> create(const char* address, const char* port) {
+        Init();
+
+        addrinfo hints = {};
+        hints.ai_family = AF_INET;
+        hints.ai_socktype = SOCK_STREAM;
+        hints.ai_protocol = IPPROTO_TCP;
+        hints.ai_flags = AI_PASSIVE;
+
+        addrinfo* info = nullptr;
+        auto err = getaddrinfo(address, port, &hints, &info);
+#if !defined(_WIN32)
+        if (err) {
+            printf("getaddrinfo(%s, %s) error: %s\n", address, port, gai_strerror(err));
+        }
+#else
+        (void)err;
+#endif
+
+        if (info) {
+            auto socket = ::socket(info->ai_family, info->ai_socktype, info->ai_protocol);
+            auto out = std::make_shared<Impl>(info, socket);
+            out->SetOptions();
+            return out;
+        }
+
+        Term();
+        return nullptr;
+    }
+
+    explicit Impl(SOCKET socket) : info(nullptr), s(socket) {}
+
+    Impl(addrinfo* i, SOCKET socket) : info(i), s(socket) {}
+
+    ~Impl() override {
+        if (info) {
+            freeaddrinfo(info);
+        }
+        Close();
+        Term();
+    }
+
+    template <typename FUNCTION>
+    void Lock(FUNCTION&& f) {
+        RLock l(mutex);
+        f(s, info);
+    }
+
+    void SetOptions() {
+        RLock l(mutex);
+        if (s == InvalidSocket) {
+            return;
+        }
+
+        int enable = 1;
+
+#if !defined(_WIN32)
+        // Prevent sockets lingering after process termination, causing
+        // reconnection issues on the same port.
+        setsockopt(s, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<char*>(&enable), sizeof(enable));
+
+        struct {
+            int l_onoff;  /* linger active */
+            int l_linger; /* how many seconds to linger for */
+        } linger = {false, 0};
+        setsockopt(s, SOL_SOCKET, SO_LINGER, reinterpret_cast<char*>(&linger), sizeof(linger));
+#endif  // !defined(_WIN32)
+
+        // Enable TCP_NODELAY.
+        setsockopt(s, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast<char*>(&enable), sizeof(enable));
+    }
+
+    bool IsOpen() override {
+        {
+            RLock l(mutex);
+            if ((s != InvalidSocket) && !Errored(s)) {
+                return true;
+            }
+        }
+        WLock lock(mutex);
+        s = InvalidSocket;
+        return false;
+    }
+
+    void Close() override {
+        {
+            RLock l(mutex);
+            if (s != InvalidSocket) {
+#if defined(_WIN32)
+                closesocket(s);
+#else
+                ::shutdown(s, SHUT_RDWR);
+#endif
+            }
+        }
+
+        WLock l(mutex);
+        if (s != InvalidSocket) {
+#if !defined(_WIN32)
+            ::close(s);
+#endif
+            s = InvalidSocket;
+        }
+    }
+
+    size_t Read(void* buffer, size_t bytes) override {
+        {
+            RLock lock(mutex);
+            if (s == InvalidSocket) {
+                return 0;
+            }
+            auto len = recv(s, reinterpret_cast<char*>(buffer), bytes, 0);
+            if (len > 0) {
+                return static_cast<size_t>(len);
+            }
+        }
+        // Socket closed or errored
+        WLock lock(mutex);
+        s = InvalidSocket;
+        return 0;
+    }
+
+    bool Write(const void* buffer, size_t bytes) override {
+        RLock lock(mutex);
+        if (s == InvalidSocket) {
+            return false;
+        }
+        if (bytes == 0) {
+            return true;
+        }
+        return ::send(s, reinterpret_cast<const char*>(buffer), bytes, 0) > 0;
+    }
+
+    std::shared_ptr<Socket> Accept() override {
+        std::shared_ptr<Impl> out;
+        Lock([&](SOCKET socket, const addrinfo*) {
+            if (socket != InvalidSocket) {
+                Init();
+                if (auto sock = ::accept(socket, nullptr, nullptr); s >= 0) {
+                    out = std::make_shared<Impl>(sock);
+                    out->SetOptions();
+                }
+            }
+        });
+        return out;
+    }
+
+  private:
+    addrinfo* const info;
+    SOCKET s = InvalidSocket;
+    RWMutex mutex;
+};
+
+}  // anonymous namespace
+
+Socket::~Socket() = default;
+
+std::shared_ptr<Socket> Socket::Listen(const char* address, const char* port) {
+    auto impl = Impl::create(address, port);
+    if (!impl) {
+        return nullptr;
+    }
+    impl->Lock([&](SOCKET socket, const addrinfo* info) {
+        if (bind(socket, info->ai_addr, info->ai_addrlen) != 0) {
+            impl.reset();
+            return;
+        }
+
+        if (listen(socket, 0) != 0) {
+            impl.reset();
+            return;
+        }
+    });
+    return impl;
+}
+
+std::shared_ptr<Socket> Socket::Connect(const char* address,
+                                        const char* port,
+                                        uint32_t timeout_ms) {
+    auto impl = Impl::create(address, port);
+    if (!impl) {
+        return nullptr;
+    }
+
+    std::shared_ptr<Socket> out;
+    impl->Lock([&](SOCKET socket, const addrinfo* info) {
+        if (socket == InvalidSocket) {
+            return;
+        }
+
+        if (timeout_ms == 0) {
+            if (::connect(socket, info->ai_addr, info->ai_addrlen) == 0) {
+                out = impl;
+            }
+            return;
+        }
+
+        if (!SetBlocking(socket, false)) {
+            return;
+        }
+
+        auto res = ::connect(socket, info->ai_addr, info->ai_addrlen);
+        if (res == 0) {
+            if (SetBlocking(socket, true)) {
+                out = impl;
+            }
+        } else {
+            const auto timeout_us = timeout_ms * 1000;
+
+            fd_set fdset;
+            FD_ZERO(&fdset);
+            FD_SET(socket, &fdset);
+
+            timeval tv;
+            tv.tv_sec = timeout_us / 1000000;
+            tv.tv_usec = static_cast<int>(timeout_us - (tv.tv_sec * 1000000));
+            res = select(static_cast<int>(socket + 1), nullptr, &fdset, nullptr, &tv);
+            if (res > 0 && !Errored(socket) && SetBlocking(socket, true)) {
+                out = impl;
+            }
+        }
+    });
+
+    if (!out) {
+        return nullptr;
+    }
+
+    return out->IsOpen() ? out : nullptr;
+}
diff --git a/src/tint/utils/socket/socket.h b/src/tint/utils/socket/socket.h
new file mode 100644
index 0000000..5ca37cc
--- /dev/null
+++ b/src/tint/utils/socket/socket.h
@@ -0,0 +1,71 @@
+// Copyright 2021 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.
+
+#ifndef SRC_TINT_UTILS_SOCKET_SOCKET_H_
+#define SRC_TINT_UTILS_SOCKET_SOCKET_H_
+
+#include <atomic>
+#include <memory>
+
+/// Socket provides an OS abstraction to a TCP socket.
+class Socket {
+  public:
+    /// Connects to the given TCP address and port.
+    /// @param address the target socket address
+    /// @param port the target socket port
+    /// @param timeout_ms the timeout for the connection attempt.
+    ///        If timeout_ms is non-zero and no connection was made before timeout_ms milliseconds,
+    ///        then nullptr is returned.
+    /// @returns the connected Socket, or nullptr on failure
+    static std::shared_ptr<Socket> Connect(const char* address,
+                                           const char* port,
+                                           uint32_t timeout_ms);
+
+    /// Begins listening for connections on the given TCP address and port.
+    /// Call Accept() on the returned Socket to block and wait for a connection.
+    /// @param address the socket address to listen on. Use "localhost" for connections from only
+    ///        this machine, or an empty string to allow connections from any incoming address.
+    /// @param port the socket port to listen on
+    /// @returns the Socket that listens for connections
+    static std::shared_ptr<Socket> Listen(const char* address, const char* port);
+
+    /// Destructor
+    virtual ~Socket();
+
+    /// Attempts to read at most `n` bytes into buffer, returning the actual number of bytes read.
+    /// read() will block until the socket is closed or at least one byte is read.
+    /// @param buffer the output buffer. Must be at least `n` bytes in size.
+    /// @param n the maximum number of bytes to read
+    /// @return the number of bytes read, or 0 if the socket was closed or errored
+    virtual size_t Read(void* buffer, size_t n) = 0;
+
+    /// Writes `n` bytes from buffer into the socket.
+    /// @param buffer the source data buffer. Must be at least `n` bytes in size.
+    /// @param n the number of bytes to read from `buffer`
+    /// @returns true on success, or false if there was an error or the socket was
+    /// closed.
+    virtual bool Write(const void* buffer, size_t n) = 0;
+
+    /// @returns true if the socket has not been closed.
+    virtual bool IsOpen() = 0;
+
+    /// Closes the socket.
+    virtual void Close() = 0;
+
+    /// Blocks for a connection to be made to the listening port, or for the Socket to be closed.
+    /// @returns a pointer to the next established incoming connection
+    virtual std::shared_ptr<Socket> Accept() = 0;
+};
+
+#endif  // SRC_TINT_UTILS_SOCKET_SOCKET_H_