Make RenderBuffer mappable

Adds map() and unmap() functions to RenderBuffer, and changes Mesh::draw() to map and update the vertex buffer instead of making a new one.

Also refactors RenderBuffer to just be a blob of data as opposed to an array of elements of fixed size and type. Removes the type-specific factory methods for creating RenderBuffers in favor of a single one that just takes a buffer type and buffer size.

Diffs=
06a187288 Make RenderBuffer mappable (#5907)

Co-authored-by: Chris Dalton <99840794+csmartdalton@users.noreply.github.com>
diff --git a/.rive_head b/.rive_head
index 5706491..a2ac71a 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-654d4488e936ac8c63930253b83229ef1c82618b
+06a1872884b3b7cf2be58673e5f5246c57c3d39c
diff --git a/include/rive/factory.hpp b/include/rive/factory.hpp
index 354d95f..b5ac3be 100644
--- a/include/rive/factory.hpp
+++ b/include/rive/factory.hpp
@@ -27,9 +27,9 @@
     Factory() {}
     virtual ~Factory() {}
 
-    virtual rcp<RenderBuffer> makeBufferU16(Span<const uint16_t>) = 0;
-    virtual rcp<RenderBuffer> makeBufferU32(Span<const uint32_t>) = 0;
-    virtual rcp<RenderBuffer> makeBufferF32(Span<const float>) = 0;
+    virtual rcp<RenderBuffer> makeRenderBuffer(RenderBufferType,
+                                               RenderBufferFlags,
+                                               size_t sizeInBytes) = 0;
 
     virtual rcp<RenderShader> makeLinearGradient(float sx,
                                                  float sy,
diff --git a/include/rive/renderer.hpp b/include/rive/renderer.hpp
index 46afa23..7ca07dc 100644
--- a/include/rive/renderer.hpp
+++ b/include/rive/renderer.hpp
@@ -5,6 +5,7 @@
 #ifndef _RIVE_RENDERER_HPP_
 #define _RIVE_RENDERER_HPP_
 
+#include "rive/enum_bitset.hpp"
 #include "rive/shapes/paint/color.hpp"
 #include "rive/command_path.hpp"
 #include "rive/layout.hpp"
@@ -27,16 +28,43 @@
 // Helper that computes a matrix to "align" content (source) to fit inside frame (destination).
 Mat2D computeAlignment(Fit, Alignment, const AABB& frame, const AABB& content);
 
-// A render buffer holds an immutable array of values
+enum class RenderBufferType
+{
+    index,
+    vertex,
+};
+
+enum class RenderBufferFlags
+{
+    none = 0,
+    mappedOnceAtInitialization = 1 << 0, // The client will map the buffer exactly one time, before
+                                         // rendering, and will never update it again.
+};
+RIVE_MAKE_ENUM_BITSET(RenderBufferFlags)
+
 class RenderBuffer : public RefCnt<RenderBuffer>
 {
-    const size_t m_Count;
-
 public:
-    RenderBuffer(size_t count);
+    RenderBuffer(RenderBufferType, RenderBufferFlags, size_t sizeInBytes);
     virtual ~RenderBuffer();
 
-    size_t count() const { return m_Count; }
+    RenderBufferType type() const { return m_type; }
+    RenderBufferFlags flags() const { return m_flags; }
+    size_t sizeInBytes() const { return m_sizeInBytes; }
+
+    void* map();
+    void unmap();
+
+protected:
+    virtual void* onMap() = 0;
+    virtual void onUnmap() = 0;
+
+private:
+    const RenderBufferType m_type;
+    const RenderBufferFlags m_flags;
+    const size_t m_sizeInBytes;
+    RIVE_DEBUG_CODE(size_t m_mapCount = 0;)
+    RIVE_DEBUG_CODE(size_t m_unmapCount = 0;)
 };
 
 enum class RenderPaintStyle
@@ -122,6 +150,8 @@
                                rcp<RenderBuffer> vertices_f32,
                                rcp<RenderBuffer> uvCoords_f32,
                                rcp<RenderBuffer> indices_u16,
+                               uint32_t vertexCount,
+                               uint32_t indexCount,
                                BlendMode,
                                float opacity) = 0;
 
diff --git a/include/rive/shapes/mesh.hpp b/include/rive/shapes/mesh.hpp
index a833d0f..80e283d 100644
--- a/include/rive/shapes/mesh.hpp
+++ b/include/rive/shapes/mesh.hpp
@@ -18,6 +18,7 @@
     {};
     std::vector<MeshVertex*> m_Vertices;
     rcp<IndexBuffer> m_IndexBuffer;
+    bool m_VertexRenderBufferDirty = true;
 
     rcp<RenderBuffer> m_IndexRenderBuffer;
     rcp<RenderBuffer> m_VertexRenderBuffer;
diff --git a/include/utils/factory_utils.hpp b/include/utils/factory_utils.hpp
index bc93cf3..5ad69c8 100644
--- a/include/utils/factory_utils.hpp
+++ b/include/utils/factory_utils.hpp
@@ -14,43 +14,32 @@
 //
 class DataRenderBuffer : public RenderBuffer
 {
-    const size_t m_elemSize;
-    std::vector<uint32_t> m_storage; // store 32bits for alignment
-
 public:
-    DataRenderBuffer(const void* src, size_t count, size_t elemSize) :
-        RenderBuffer(count), m_elemSize(elemSize)
+    DataRenderBuffer(RenderBufferType type, RenderBufferFlags flags, size_t sizeInBytes) :
+        RenderBuffer(type, flags, sizeInBytes)
     {
-        const size_t bytes = count * elemSize;
-        m_storage.resize((bytes + 3) >> 2); // round up to next 32bit count
-        memcpy(m_storage.data(), src, bytes);
+        m_storage = malloc(sizeInBytes);
     }
 
-    const float* f32s() const
-    {
-        assert(m_elemSize == sizeof(float));
-        return reinterpret_cast<const float*>(m_storage.data());
-    }
+    ~DataRenderBuffer() { free(m_storage); }
 
-    const uint16_t* u16s() const
-    {
-        assert(m_elemSize == sizeof(uint16_t));
-        return reinterpret_cast<const uint16_t*>(m_storage.data());
-    }
+    const float* f32s() const { return reinterpret_cast<const float*>(m_storage); }
 
-    const Vec2D* vecs() const { return reinterpret_cast<const Vec2D*>(this->f32s()); }
+    const uint16_t* u16s() const { return reinterpret_cast<const uint16_t*>(m_storage); }
 
-    size_t elemSize() const { return m_elemSize; }
+    const Vec2D* vecs() const { return reinterpret_cast<const Vec2D*>(f32s()); }
 
     static const DataRenderBuffer* Cast(const RenderBuffer* buffer)
     {
         return static_cast<const DataRenderBuffer*>(buffer);
     }
 
-    template <typename T> static rcp<RenderBuffer> Make(Span<T> span)
-    {
-        return rcp<RenderBuffer>(new DataRenderBuffer(span.data(), span.size(), sizeof(T)));
-    }
+protected:
+    void* onMap() override { return m_storage; }
+    void onUnmap() override {}
+
+private:
+    void* m_storage;
 };
 
 } // namespace rive
diff --git a/include/utils/no_op_factory.hpp b/include/utils/no_op_factory.hpp
index 3038738..1adcb3d 100644
--- a/include/utils/no_op_factory.hpp
+++ b/include/utils/no_op_factory.hpp
@@ -12,9 +12,7 @@
 
 class NoOpFactory : public Factory
 {
-    rcp<RenderBuffer> makeBufferU16(Span<const uint16_t>) override;
-    rcp<RenderBuffer> makeBufferU32(Span<const uint32_t>) override;
-    rcp<RenderBuffer> makeBufferF32(Span<const float>) override;
+    rcp<RenderBuffer> makeRenderBuffer(RenderBufferType, RenderBufferFlags, size_t) override;
 
     rcp<RenderShader> makeLinearGradient(float sx,
                                          float sy,
diff --git a/include/utils/no_op_renderer.hpp b/include/utils/no_op_renderer.hpp
index 0a0396b..5cedb34 100644
--- a/include/utils/no_op_renderer.hpp
+++ b/include/utils/no_op_renderer.hpp
@@ -23,6 +23,8 @@
                        rcp<RenderBuffer>,
                        rcp<RenderBuffer>,
                        rcp<RenderBuffer>,
+                       uint32_t vertexCount,
+                       uint32_t indexCount,
                        BlendMode,
                        float) override
     {}
diff --git a/skia/renderer/include/cg_factory.hpp b/skia/renderer/include/cg_factory.hpp
index 0e88428..f9e21bd 100644
--- a/skia/renderer/include/cg_factory.hpp
+++ b/skia/renderer/include/cg_factory.hpp
@@ -14,9 +14,7 @@
 class CGFactory : public Factory
 {
 public:
-    rcp<RenderBuffer> makeBufferU16(Span<const uint16_t>) override;
-    rcp<RenderBuffer> makeBufferU32(Span<const uint32_t>) override;
-    rcp<RenderBuffer> makeBufferF32(Span<const float>) override;
+    rcp<RenderBuffer> makeRenderBuffer(RenderBufferType, RenderBufferFlags, size_t) override;
 
     rcp<RenderShader> makeLinearGradient(float sx,
                                          float sy,
diff --git a/skia/renderer/include/cg_renderer.hpp b/skia/renderer/include/cg_renderer.hpp
index 7329e4d..945c2df 100644
--- a/skia/renderer/include/cg_renderer.hpp
+++ b/skia/renderer/include/cg_renderer.hpp
@@ -35,6 +35,8 @@
                        rcp<RenderBuffer> vertices_f32,
                        rcp<RenderBuffer> uvCoords_f32,
                        rcp<RenderBuffer> indices_u16,
+                       uint32_t vertexCount,
+                       uint32_t indexCount,
                        BlendMode,
                        float opacity) override;
 };
diff --git a/skia/renderer/include/skia_factory.hpp b/skia/renderer/include/skia_factory.hpp
index c12722c..c4e2dee 100644
--- a/skia/renderer/include/skia_factory.hpp
+++ b/skia/renderer/include/skia_factory.hpp
@@ -14,9 +14,7 @@
 class SkiaFactory : public Factory
 {
 public:
-    rcp<RenderBuffer> makeBufferU16(Span<const uint16_t>) override;
-    rcp<RenderBuffer> makeBufferU32(Span<const uint32_t>) override;
-    rcp<RenderBuffer> makeBufferF32(Span<const float>) override;
+    rcp<RenderBuffer> makeRenderBuffer(RenderBufferType, RenderBufferFlags, size_t) override;
 
     rcp<RenderShader> makeLinearGradient(float sx,
                                          float sy,
diff --git a/skia/renderer/include/skia_renderer.hpp b/skia/renderer/include/skia_renderer.hpp
index 3fd557b..d397e80 100644
--- a/skia/renderer/include/skia_renderer.hpp
+++ b/skia/renderer/include/skia_renderer.hpp
@@ -28,6 +28,8 @@
                        rcp<RenderBuffer> vertices_f32,
                        rcp<RenderBuffer> uvCoords_f32,
                        rcp<RenderBuffer> indices_u16,
+                       uint32_t vertexCount,
+                       uint32_t indexCount,
                        BlendMode,
                        float opacity) override;
 };
diff --git a/skia/renderer/src/cg_factory.cpp b/skia/renderer/src/cg_factory.cpp
index cefbf3b..89d3007 100644
--- a/skia/renderer/src/cg_factory.cpp
+++ b/skia/renderer/src/cg_factory.cpp
@@ -420,6 +420,8 @@
                                rcp<RenderBuffer> vertices,
                                rcp<RenderBuffer> uvCoords,
                                rcp<RenderBuffer> indices,
+                               uint32_t vertexCount,
+                               uint32_t indexCount,
                                BlendMode blendMode,
                                float opacity)
 {
@@ -432,7 +434,7 @@
 
     auto scale = [sx, sy](Vec2D v) { return Vec2D{v.x * sx, v.y * sy}; };
 
-    auto triangles = indices->count() / 3;
+    auto triangles = indexCount / 3;
     auto ndx = DataRenderBuffer::Cast(indices.get())->u16s();
     auto pts = DataRenderBuffer::Cast(vertices.get())->vecs();
     auto uvs = DataRenderBuffer::Cast(uvCoords.get())->vecs();
@@ -479,19 +481,11 @@
 
 // Factory
 
-rcp<RenderBuffer> CGFactory::makeBufferU16(Span<const uint16_t> data)
+rcp<RenderBuffer> CGFactory::makeRenderBuffer(RenderBufferType type,
+                                              RenderBufferFlags flags,
+                                              size_t sizeInBytes)
 {
-    return DataRenderBuffer::Make(data);
-}
-
-rcp<RenderBuffer> CGFactory::makeBufferU32(Span<const uint32_t> data)
-{
-    return DataRenderBuffer::Make(data);
-}
-
-rcp<RenderBuffer> CGFactory::makeBufferF32(Span<const float> data)
-{
-    return DataRenderBuffer::Make(data);
+    return make_rcp<DataRenderBuffer>(type, flags, sizeInBytes);
 }
 
 rcp<RenderShader> CGFactory::makeLinearGradient(float sx,
diff --git a/skia/renderer/src/skia_factory.cpp b/skia/renderer/src/skia_factory.cpp
index 74a4970..67394d4 100644
--- a/skia/renderer/src/skia_factory.cpp
+++ b/skia/renderer/src/skia_factory.cpp
@@ -158,16 +158,15 @@
                                  rcp<RenderBuffer> vertices,
                                  rcp<RenderBuffer> uvCoords,
                                  rcp<RenderBuffer> indices,
+                                 uint32_t vertexCount,
+                                 uint32_t indexCount,
                                  BlendMode blendMode,
                                  float opacity)
 {
-    // need our vertices and uvs to agree
-    assert(vertices->count() == uvCoords->count());
-    // vertices and uvs are arrays of floats, so we need their counts to be
-    // even, since we treat them as arrays of points
-    assert((vertices->count() & 1) == 0);
-
-    const int vertexCount = vertices->count() >> 1;
+    // need our buffers and counts to agree
+    assert(vertices->sizeInBytes() == vertexCount * sizeof(Vec2D));
+    assert(uvCoords->sizeInBytes() == vertexCount * sizeof(Vec2D));
+    assert(indices->sizeInBytes() == indexCount * sizeof(uint16_t));
 
     SkMatrix scaleM;
 
@@ -177,7 +176,7 @@
     // The local matrix is ignored for drawVertices, so we have to manually scale
     // the UVs to match Skia's convention...
     std::vector<SkPoint> scaledUVs(vertexCount);
-    for (int i = 0; i < vertexCount; ++i)
+    for (uint32_t i = 0; i < vertexCount; ++i)
     {
         scaledUVs[i] = {uvs[i].fX * image->width(), uvs[i].fY * image->height()};
     }
@@ -200,15 +199,13 @@
 
     const SkColor* no_colors = nullptr;
     auto vertexMode = SkVertices::kTriangles_VertexMode;
-    // clang-format off
     auto vt = SkVertices::MakeCopy(vertexMode,
                                    vertexCount,
                                    (const SkPoint*)DataRenderBuffer::Cast(vertices.get())->vecs(),
                                    uvs,
                                    no_colors,
-                                   indices->count(),
+                                   indexCount,
                                    DataRenderBuffer::Cast(indices.get())->u16s());
-    // clang-format on
 
     // The blend mode is ignored if we don't have colors && uvs
     m_Canvas->drawVertices(vt, SkBlendMode::kModulate, paint);
@@ -222,19 +219,11 @@
 
 // Factory
 
-rcp<RenderBuffer> SkiaFactory::makeBufferU16(Span<const uint16_t> data)
+rcp<RenderBuffer> SkiaFactory::makeRenderBuffer(RenderBufferType type,
+                                                RenderBufferFlags flags,
+                                                size_t sizeInBytes)
 {
-    return DataRenderBuffer::Make(data);
-}
-
-rcp<RenderBuffer> SkiaFactory::makeBufferU32(Span<const uint32_t> data)
-{
-    return DataRenderBuffer::Make(data);
-}
-
-rcp<RenderBuffer> SkiaFactory::makeBufferF32(Span<const float> data)
-{
-    return DataRenderBuffer::Make(data);
+    return make_rcp<DataRenderBuffer>(type, flags, sizeInBytes);
 }
 
 rcp<RenderShader> SkiaFactory::makeLinearGradient(float sx,
diff --git a/src/renderer.cpp b/src/renderer.cpp
index b2ed93e..3941fe7 100644
--- a/src/renderer.cpp
+++ b/src/renderer.cpp
@@ -80,10 +80,29 @@
     this->transform(Mat2D(c, s, -s, c, 0, 0));
 }
 
-RenderBuffer::RenderBuffer(size_t count) : m_Count(count) { Counter::update(Counter::kBuffer, 1); }
+RenderBuffer::RenderBuffer(RenderBufferType type, RenderBufferFlags flags, size_t sizeInBytes) :
+    m_type(type), m_flags(flags), m_sizeInBytes(sizeInBytes)
+{
+    Counter::update(Counter::kBuffer, 1);
+}
 
 RenderBuffer::~RenderBuffer() { Counter::update(Counter::kBuffer, -1); }
 
+void* RenderBuffer::map()
+{
+    assert(m_mapCount == 0 || !(m_flags & RenderBufferFlags::mappedOnceAtInitialization));
+    assert(m_mapCount == m_unmapCount);
+    RIVE_DEBUG_CODE(++m_mapCount;)
+    return onMap();
+}
+
+void RenderBuffer::unmap()
+{
+    assert(m_unmapCount + 1 == m_mapCount);
+    RIVE_DEBUG_CODE(++m_unmapCount;)
+    onUnmap();
+}
+
 RenderShader::RenderShader() { Counter::update(Counter::kShader, 1); }
 RenderShader::~RenderShader() { Counter::update(Counter::kShader, -1); }
 
diff --git a/src/shapes/mesh.cpp b/src/shapes/mesh.cpp
index 50ec298..c66fe1d 100644
--- a/src/shapes/mesh.cpp
+++ b/src/shapes/mesh.cpp
@@ -86,7 +86,12 @@
 
 Core* Mesh::clone() const
 {
+    auto factory = artboard()->factory();
     auto clone = static_cast<Mesh*>(MeshBase::clone());
+    clone->m_VertexRenderBufferDirty = true;
+    clone->m_VertexRenderBuffer = factory->makeRenderBuffer(RenderBufferType::vertex,
+                                                            RenderBufferFlags::none,
+                                                            m_Vertices.size() * sizeof(Vec2D));
     clone->m_UVRenderBuffer = m_UVRenderBuffer;
     clone->m_IndexRenderBuffer = m_IndexRenderBuffer;
     return clone;
@@ -96,19 +101,36 @@
 {
     Mat2D uvTransform = renderImage != nullptr ? renderImage->uvTransform() : Mat2D();
 
-    std::vector<float> uv = std::vector<float>(m_Vertices.size() * 2);
-    std::size_t index = 0;
+    auto factory = artboard()->factory();
+    m_VertexRenderBufferDirty = true;
+    m_VertexRenderBuffer = factory->makeRenderBuffer(RenderBufferType::vertex,
+                                                     RenderBufferFlags::none,
+                                                     m_Vertices.size() * sizeof(Vec2D));
 
-    for (auto vertex : m_Vertices)
+    m_UVRenderBuffer = factory->makeRenderBuffer(RenderBufferType::vertex,
+                                                 RenderBufferFlags::mappedOnceAtInitialization,
+                                                 m_Vertices.size() * sizeof(Vec2D));
+    if (m_UVRenderBuffer)
     {
-        Vec2D xformedUV = uvTransform * Vec2D(vertex->u(), vertex->v());
-        uv[index++] = xformedUV.x;
-        uv[index++] = xformedUV.y;
+        float* uv = static_cast<float*>(m_UVRenderBuffer->map());
+        for (auto vertex : m_Vertices)
+        {
+            Vec2D xformedUV = uvTransform * Vec2D(vertex->u(), vertex->v());
+            *uv++ = xformedUV.x;
+            *uv++ = xformedUV.y;
+        }
+        m_UVRenderBuffer->unmap();
     }
 
-    auto factory = artboard()->factory();
-    m_UVRenderBuffer = factory->makeBufferF32(uv);
-    m_IndexRenderBuffer = factory->makeBufferU16(*m_IndexBuffer);
+    m_IndexRenderBuffer = factory->makeRenderBuffer(RenderBufferType::index,
+                                                    RenderBufferFlags::mappedOnceAtInitialization,
+                                                    m_IndexBuffer->size() * sizeof(uint16_t));
+    if (m_IndexRenderBuffer)
+    {
+        void* indexData = m_IndexRenderBuffer->map();
+        memcpy(indexData, m_IndexBuffer->data(), m_IndexRenderBuffer->sizeInBytes());
+        m_IndexRenderBuffer->unmap();
+    }
 }
 
 void Mesh::buildDependencies()
@@ -129,27 +151,22 @@
         {
             skin()->deform({(Vertex**)m_Vertices.data(), m_Vertices.size()});
         }
-        m_VertexRenderBuffer = nullptr;
+        m_VertexRenderBufferDirty = true;
     }
     Super::update(value);
 }
 
 void Mesh::draw(Renderer* renderer, const RenderImage* image, BlendMode blendMode, float opacity)
 {
-    if (m_VertexRenderBuffer == nullptr)
+    if (m_VertexRenderBufferDirty && m_VertexRenderBuffer != nullptr)
     {
-
-        std::vector<float> vertices(m_Vertices.size() * 2);
-        std::size_t index = 0;
+        Vec2D* mappedVertices = reinterpret_cast<Vec2D*>(m_VertexRenderBuffer->map());
         for (auto vertex : m_Vertices)
         {
-            auto translation = vertex->renderTranslation();
-            vertices[index++] = translation.x;
-            vertices[index++] = translation.y;
+            *mappedVertices++ = vertex->renderTranslation();
         }
-
-        auto factory = artboard()->factory();
-        m_VertexRenderBuffer = factory->makeBufferF32(vertices);
+        m_VertexRenderBuffer->unmap();
+        m_VertexRenderBufferDirty = false;
     }
 
     if (skin() == nullptr)
@@ -160,6 +177,8 @@
                             m_VertexRenderBuffer,
                             m_UVRenderBuffer,
                             m_IndexRenderBuffer,
+                            static_cast<uint32_t>(m_Vertices.size()),
+                            static_cast<uint32_t>(m_IndexBuffer->size()),
                             blendMode,
                             opacity);
 }
diff --git a/tess/include/rive/tess/sokol/sokol_factory.hpp b/tess/include/rive/tess/sokol/sokol_factory.hpp
index 2d9812a..598f1f0 100644
--- a/tess/include/rive/tess/sokol/sokol_factory.hpp
+++ b/tess/include/rive/tess/sokol/sokol_factory.hpp
@@ -16,9 +16,7 @@
 public:
     SokolFactory();
 
-    rcp<RenderBuffer> makeBufferU16(Span<const uint16_t>) override;
-    rcp<RenderBuffer> makeBufferU32(Span<const uint32_t>) override;
-    rcp<RenderBuffer> makeBufferF32(Span<const float>) override;
+    rcp<RenderBuffer> makeRenderBuffer(RenderBufferType, RenderBufferFlags, size_t) override;
 
     rcp<RenderShader> makeLinearGradient(float sx,
                                          float sy,
diff --git a/tess/include/rive/tess/sokol/sokol_tess_renderer.hpp b/tess/include/rive/tess/sokol/sokol_tess_renderer.hpp
index 4a6f0d8..463bcc1 100644
--- a/tess/include/rive/tess/sokol/sokol_tess_renderer.hpp
+++ b/tess/include/rive/tess/sokol/sokol_tess_renderer.hpp
@@ -94,6 +94,8 @@
                        rcp<RenderBuffer> vertices_f32,
                        rcp<RenderBuffer> uvCoords_f32,
                        rcp<RenderBuffer> indices_u16,
+                       uint32_t vertexCount,
+                       uint32_t indexCount,
                        BlendMode,
                        float opacity) override;
     void restore() override;
diff --git a/tess/include/rive/tess/tess_renderer.hpp b/tess/include/rive/tess/tess_renderer.hpp
index 1122527..3387ffb 100644
--- a/tess/include/rive/tess/tess_renderer.hpp
+++ b/tess/include/rive/tess/tess_renderer.hpp
@@ -49,6 +49,8 @@
                        rcp<RenderBuffer> vertices_f32,
                        rcp<RenderBuffer> uvCoords_f32,
                        rcp<RenderBuffer> indices_u16,
+                       uint32_t vertexCount,
+                       uint32_t indexCount,
                        BlendMode,
                        float opacity) override;
 };
diff --git a/tess/src/sokol/sokol_tess_renderer.cpp b/tess/src/sokol/sokol_tess_renderer.cpp
index cb8305a..b7fb081 100644
--- a/tess/src/sokol/sokol_tess_renderer.cpp
+++ b/tess/src/sokol/sokol_tess_renderer.cpp
@@ -151,54 +151,76 @@
 
 class SokolBuffer : public RenderBuffer
 {
-private:
-    sg_buffer m_Buffer;
-
 public:
-    SokolBuffer(size_t count, const sg_buffer_desc& desc) :
-        RenderBuffer(count), m_Buffer(sg_make_buffer(desc))
-    {}
-    ~SokolBuffer() { sg_destroy_buffer(m_Buffer); }
+    SokolBuffer(RenderBufferType type, RenderBufferFlags renderBufferFlags, size_t sizeInBytes) :
+        RenderBuffer(type, renderBufferFlags, sizeInBytes), m_mappedMemory(new char[sizeInBytes])
+    {
+        // If the buffer will be immutable, defer creation until the client unmaps for the only time
+        // and we have our initial data.
+        if (!(flags() & RenderBufferFlags::mappedOnceAtInitialization))
+        {
+            m_buffer = sg_make_buffer(makeNoDataBufferDesc());
+        }
+    }
+    ~SokolBuffer() { sg_destroy_buffer(m_buffer); }
 
-    sg_buffer buffer() { return m_Buffer; }
+    sg_buffer buffer()
+    {
+        // In the case of RenderBufferFlags::mappedOnceAtInitialization, the client is expected to
+        // map()/unmap() before we need to access this buffer for rendering.
+        assert(m_buffer.id);
+        return m_buffer;
+    }
+
+protected:
+    void* onMap() override
+    {
+        // An immutable buffer is only mapped once, and then we delete m_mappedMemory.
+        assert(m_mappedMemory);
+        return m_mappedMemory.get();
+    }
+
+    void onUnmap() override
+    {
+        if (flags() & RenderBufferFlags::mappedOnceAtInitialization)
+        {
+            // We are an immutable buffer. Creation was deferred until now.
+            assert(!m_buffer.id);
+            assert(m_mappedMemory);
+            sg_buffer_desc bufferDesc = makeNoDataBufferDesc();
+            bufferDesc.data = {m_mappedMemory.get(), sizeInBytes()};
+            m_buffer = sg_make_buffer(bufferDesc);
+            m_mappedMemory.reset(); // This buffer will never be mapped again.
+        }
+        else
+        {
+            assert(m_buffer.id);
+            sg_update_buffer(m_buffer,
+                             sg_range{.ptr = m_mappedMemory.get(), .size = sizeInBytes()});
+        }
+    }
+
+private:
+    sg_buffer_desc makeNoDataBufferDesc() const
+    {
+        return {
+            .size = sizeInBytes(),
+            .usage = (flags() & RenderBufferFlags::mappedOnceAtInitialization) ? SG_USAGE_IMMUTABLE
+                                                                               : SG_USAGE_STREAM,
+            .type = (type() == RenderBufferType::index) ? SG_BUFFERTYPE_INDEXBUFFER
+                                                        : SG_BUFFERTYPE_VERTEXBUFFER,
+        };
+    };
+
+    sg_buffer m_buffer{};
+    std::unique_ptr<char[]> m_mappedMemory;
 };
 
-rcp<RenderBuffer> SokolFactory::makeBufferU16(Span<const uint16_t> span)
+rcp<RenderBuffer> SokolFactory::makeRenderBuffer(RenderBufferType type,
+                                                 RenderBufferFlags flags,
+                                                 size_t sizeInBytes)
 {
-    return rcp<RenderBuffer>(new SokolBuffer(span.size(),
-                                             (sg_buffer_desc){
-                                                 .type = SG_BUFFERTYPE_INDEXBUFFER,
-                                                 .data =
-                                                     {
-                                                         span.data(),
-                                                         span.size_bytes(),
-                                                     },
-                                             }));
-}
-
-rcp<RenderBuffer> SokolFactory::makeBufferU32(Span<const uint32_t> span)
-{
-    return rcp<RenderBuffer>(new SokolBuffer(span.size(),
-                                             (sg_buffer_desc){
-                                                 .type = SG_BUFFERTYPE_INDEXBUFFER,
-                                                 .data =
-                                                     {
-                                                         span.data(),
-                                                         span.size_bytes(),
-                                                     },
-                                             }));
-}
-rcp<RenderBuffer> SokolFactory::makeBufferF32(Span<const float> span)
-{
-    return rcp<RenderBuffer>(new SokolBuffer(span.size(),
-                                             (sg_buffer_desc){
-                                                 .type = SG_BUFFERTYPE_VERTEXBUFFER,
-                                                 .data =
-                                                     {
-                                                         span.data(),
-                                                         span.size_bytes(),
-                                                     },
-                                             }));
+    return make_rcp<SokolBuffer>(type, flags, sizeInBytes);
 }
 
 sg_pipeline vectorPipeline(sg_shader shader,
@@ -582,6 +604,8 @@
                                       rcp<RenderBuffer> vertices_f32,
                                       rcp<RenderBuffer> uvCoords_f32,
                                       rcp<RenderBuffer> indices_u16,
+                                      uint32_t vertexCount,
+                                      uint32_t indexCount,
                                       BlendMode blendMode,
                                       float opacity)
 {
@@ -600,7 +624,7 @@
 
     sg_apply_bindings(&bind);
     sg_apply_uniforms(SG_SHADERSTAGE_VS, SLOT_vs_params, SG_RANGE_REF(vs_params));
-    sg_draw(0, indices_u16->count(), 1);
+    sg_draw(0, indexCount, 1);
 }
 
 class SokolGradient : public RenderShader
diff --git a/tess/src/tess_renderer.cpp b/tess/src/tess_renderer.cpp
index 773717d..42f697d 100644
--- a/tess/src/tess_renderer.cpp
+++ b/tess/src/tess_renderer.cpp
@@ -41,6 +41,8 @@
                                  rcp<RenderBuffer> vertices_f32,
                                  rcp<RenderBuffer> uvCoords_f32,
                                  rcp<RenderBuffer> indices_u16,
+                                 uint32_t vertexCount,
+                                 uint32_t indexCount,
                                  BlendMode,
                                  float opacity)
-{}
\ No newline at end of file
+{}
diff --git a/utils/no_op_factory.cpp b/utils/no_op_factory.cpp
index b0cb285..d4739a3 100644
--- a/utils/no_op_factory.cpp
+++ b/utils/no_op_factory.cpp
@@ -39,9 +39,10 @@
 };
 } // namespace
 
-rcp<RenderBuffer> NoOpFactory::makeBufferU16(Span<const uint16_t>) { return nullptr; }
-rcp<RenderBuffer> NoOpFactory::makeBufferU32(Span<const uint32_t>) { return nullptr; }
-rcp<RenderBuffer> NoOpFactory::makeBufferF32(Span<const float>) { return nullptr; }
+rcp<RenderBuffer> NoOpFactory::makeRenderBuffer(RenderBufferType, RenderBufferFlags, size_t)
+{
+    return nullptr;
+}
 
 rcp<RenderShader> NoOpFactory::makeLinearGradient(float sx,
                                                   float sy,
diff --git a/vello/src/vello_renderer.cpp b/vello/src/vello_renderer.cpp
index 63c94e7..ffd59af 100644
--- a/vello/src/vello_renderer.cpp
+++ b/vello/src/vello_renderer.cpp
@@ -83,34 +83,28 @@
                                   rcp<RenderBuffer> vertices,
                                   rcp<RenderBuffer> uvCoords,
                                   rcp<RenderBuffer> indices,
+                                  uint32_t vertexCount,
+                                  uint32_t indexCount,
                                   BlendMode blendMode,
                                   float opacity)
 {
     vello_renderer_draw_image_mesh(m_renderer,
                                    static_cast<const VelloImage*>(image)->image(),
                                    DataRenderBuffer::Cast(vertices.get())->vecs(),
-                                   vertices->count(),
+                                   vertexCount,
                                    DataRenderBuffer::Cast(uvCoords.get())->vecs(),
-                                   uvCoords->count(),
+                                   vertexCount,
                                    DataRenderBuffer::Cast(indices.get())->u16s(),
-                                   indices->count(),
+                                   indexCount,
                                    blendMode,
                                    opacity);
 }
 
-rcp<RenderBuffer> VelloFactory::makeBufferU16(Span<const uint16_t> data)
+rcp<RenderBuffer> VelloFactory::makeRenderBuffer(RenderBufferType type,
+                                                 RenderBufferFlags flags,
+                                                 size_t sizeInBytes)
 {
-    return DataRenderBuffer::Make(data);
-}
-
-rcp<RenderBuffer> VelloFactory::makeBufferU32(Span<const uint32_t> data)
-{
-    return DataRenderBuffer::Make(data);
-}
-
-rcp<RenderBuffer> VelloFactory::makeBufferF32(Span<const float> data)
-{
-    return DataRenderBuffer::Make(data);
+    return make_rcp<DataRenderBuffer>(type, flags, sizeInBytes);
 }
 
 rcp<RenderShader> VelloFactory::makeLinearGradient(float sx,
diff --git a/vello/src/vello_renderer.hpp b/vello/src/vello_renderer.hpp
index 5ef84d2..ab8a0c6 100644
--- a/vello/src/vello_renderer.hpp
+++ b/vello/src/vello_renderer.hpp
@@ -184,6 +184,8 @@
                        rcp<RenderBuffer> vertices_f32,
                        rcp<RenderBuffer> uvCoords_f32,
                        rcp<RenderBuffer> indices_u16,
+                       uint32_t vertexCount,
+                       uint32_t indexCount,
                        BlendMode,
                        float opacity) override;
 };
@@ -191,9 +193,7 @@
 class VelloFactory : public Factory
 {
 public:
-    rcp<RenderBuffer> makeBufferU16(Span<const uint16_t>) override;
-    rcp<RenderBuffer> makeBufferU32(Span<const uint32_t>) override;
-    rcp<RenderBuffer> makeBufferF32(Span<const float>) override;
+    rcp<RenderBuffer> makeRenderBuffer(RenderBufferType, RenderBufferFlags, size_t) override;
 
     rcp<RenderShader> makeLinearGradient(float sx,
                                          float sy,