diff --git a/extern/glow b/extern/glow
index 935e9509b82a9d333ad39fa29e543e08ce1905fc..6c99baa6ac43086a5bf94ea37898af9f19cba4fc 160000
--- a/extern/glow
+++ b/extern/glow
@@ -1 +1 @@
-Subproject commit 935e9509b82a9d333ad39fa29e543e08ce1905fc
+Subproject commit 6c99baa6ac43086a5bf94ea37898af9f19cba4fc
diff --git a/extern/glow-extras b/extern/glow-extras
index 1a368d650c787031077975718d6b6b7e1b162c2d..e8dc78bc104a0c0efbbcebd2031fc76053fc587a 160000
--- a/extern/glow-extras
+++ b/extern/glow-extras
@@ -1 +1 @@
-Subproject commit 1a368d650c787031077975718d6b6b7e1b162c2d
+Subproject commit e8dc78bc104a0c0efbbcebd2031fc76053fc587a
diff --git a/extern/polymesh b/extern/polymesh
index b34ab30e148639b208df58b3c406803ce454527b..85dc7a4197ffeca4f4602fbfe735cd83a04d9819 160000
--- a/extern/polymesh
+++ b/extern/polymesh
@@ -1 +1 @@
-Subproject commit b34ab30e148639b208df58b3c406803ce454527b
+Subproject commit 85dc7a4197ffeca4f4602fbfe735cd83a04d9819
diff --git a/extern/typed-geometry b/extern/typed-geometry
index fa16f24845e43a009a488d1dbc5136c4d44cd406..fd57c320949e93a701e533ea436e1f4480e76350 160000
--- a/extern/typed-geometry
+++ b/extern/typed-geometry
@@ -1 +1 @@
-Subproject commit fa16f24845e43a009a488d1dbc5136c4d44cd406
+Subproject commit fd57c320949e93a701e533ea436e1f4480e76350
diff --git a/tests/attributes/raw-attribute-test.cc b/tests/attributes/raw-attribute-test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..4b167fc5dbce2b6f0fc29adbd87378b7182c0633
--- /dev/null
+++ b/tests/attributes/raw-attribute-test.cc
@@ -0,0 +1,99 @@
+#include <doctest.hh>
+
+#include <polymesh/Mesh.hh>
+#include <polymesh/attributes/raw_attribute.hh>
+
+TEST_CASE("RawAttributes.Basics")
+{
+    pm::Mesh m;
+    for (auto i = 0; i < 100; ++i)
+        m.vertices().add();
+
+    auto a_raw = pm::raw_vertex_attribute(m, 4);
+    auto a_int = a_raw.to<int>();
+
+    for (auto v : m.vertices())
+        CHECK(a_int[v] == 0);
+
+    for (auto v : m.vertices())
+    {
+        CHECK(a_raw[v].size() == 4);
+        a_raw[v][0] = std::byte(int(v)); // assumes little endian
+    }
+
+    a_int = a_raw.to<int>();
+    for (auto v : m.vertices())
+        CHECK(a_int[v] == int(v));
+}
+
+TEST_CASE("RawAttributes.Registry")
+{
+    pm::Mesh m;
+    auto ll = low_level_api(m);
+
+    auto ASSERT_ATTR_CNT = [&](int v, int f, int e, int h) {
+        CHECK(ll.vertex_attribute_count() == v);
+        CHECK(ll.face_attribute_count() == f);
+        CHECK(ll.edge_attribute_count() == e);
+        CHECK(ll.halfedge_attribute_count() == h);
+    };
+
+    ASSERT_ATTR_CNT(0, 0, 0, 0);
+
+    {
+        auto a = pm::raw_vertex_attribute(m, 4);
+
+        ASSERT_ATTR_CNT(1, 0, 0, 0);
+
+        {
+            auto b = pm::raw_vertex_attribute(m, 1);
+
+            ASSERT_ATTR_CNT(2, 0, 0, 0);
+
+            auto a0 = pm::raw_face_attribute(m, 4);
+            auto a1 = pm::raw_halfedge_attribute(m, 3);
+            auto a2 = pm::raw_edge_attribute(m, 7);
+
+            ASSERT_ATTR_CNT(2, 1, 1, 1);
+        }
+
+        ASSERT_ATTR_CNT(1, 0, 0, 0);
+    }
+
+    ASSERT_ATTR_CNT(0, 0, 0, 0);
+
+    {
+        auto a = pm::raw_vertex_attribute(m, 5);
+        auto b = pm::raw_vertex_attribute(m, 9);
+
+        ASSERT_ATTR_CNT(2, 0, 0, 0);
+
+        b = a; // copy
+
+        ASSERT_ATTR_CNT(2, 0, 0, 0);
+
+        b = std::move(a); // move
+        CHECK(!a.is_valid());
+
+        ASSERT_ATTR_CNT(1, 0, 0, 0);
+
+        auto c = pm::raw_vertex_attribute(m, 3);
+
+        ASSERT_ATTR_CNT(2, 0, 0, 0);
+
+        pm::raw_vertex_attribute d(m, 11); // "default" ctor
+
+        ASSERT_ATTR_CNT(3, 0, 0, 0);
+
+        pm::raw_vertex_attribute e(std::move(c)); // move c into e
+        CHECK(!c.is_valid());
+
+        ASSERT_ATTR_CNT(3, 0, 0, 0);
+
+        pm::raw_vertex_attribute f(d); // copy d into f
+
+        ASSERT_ATTR_CNT(4, 0, 0, 0);
+    }
+
+    ASSERT_ATTR_CNT(0, 0, 0, 0);
+}