Initial D3D11 backend for PLS

Diffs=
9c1d1e4ff Initial D3D11 backend for PLS (#5534)

Co-authored-by: Chris Dalton <99840794+csmartdalton@users.noreply.github.com>
diff --git a/.rive_head b/.rive_head
index 99b7c41..0b671ab 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-dcf320c64584505acef32ddf5f316fa5cb6d0813
+9c1d1e4ffbafbe4ab9f649b8228f721ab5e836d0
diff --git a/include/rive/pls/buffer_ring.hpp b/include/rive/pls/buffer_ring.hpp
index 01b1b39..149bb49 100644
--- a/include/rive/pls/buffer_ring.hpp
+++ b/include/rive/pls/buffer_ring.hpp
@@ -139,7 +139,7 @@
         submitTexels(bufferIdx, updateWidthInTexels, updateHeight);
     }
 
-    virtual void submitTexels(int textureIdx, size_t width, size_t height) = 0;
+    virtual void submitTexels(int textureIdx, size_t updateWidthInTexels, size_t updateHeight) = 0;
 
     const Format m_format;
     const size_t m_widthInItems;
diff --git a/include/rive/pls/d3d/d3d11.hpp b/include/rive/pls/d3d/d3d11.hpp
new file mode 100644
index 0000000..762f245
--- /dev/null
+++ b/include/rive/pls/d3d/d3d11.hpp
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2023 Rive
+ */
+
+#pragma once
+
+#define NOMINMAX
+#include <d3d11.h>
+#include <wrl/client.h>
+
+using Microsoft::WRL::ComPtr;
+
+#define VERIFY_OK(CODE)                                                                            \
+    {                                                                                              \
+        HRESULT hr = (CODE);                                                                       \
+        if (hr != S_OK)                                                                            \
+        {                                                                                          \
+            fprintf(stderr, __FILE__ ":%u: D3D error 0x%lx: %s\n", __LINE__, hr, #CODE);           \
+            exit(-1);                                                                              \
+        }                                                                                          \
+    }
diff --git a/include/rive/pls/d3d/pls_render_context_d3d.hpp b/include/rive/pls/d3d/pls_render_context_d3d.hpp
new file mode 100644
index 0000000..1979daf
--- /dev/null
+++ b/include/rive/pls/d3d/pls_render_context_d3d.hpp
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2023 Rive
+ */
+
+#pragma once
+
+#include "rive/pls/d3d/d3d11.hpp"
+#include "rive/pls/pls_render_context.hpp"
+#include <map>
+
+namespace rive::pls
+{
+class PLSRenderTargetD3D;
+
+// D3D backend implementation of PLSRenderTarget.
+class PLSRenderTargetD3D : public PLSRenderTarget
+{
+public:
+    ~PLSRenderTargetD3D() override {}
+
+    void setTargetTexture(ID3D11Device*, ComPtr<ID3D11Texture2D> tex);
+    ID3D11Texture2D* targetTexture() const { return m_targetTexture.Get(); }
+
+private:
+    friend class PLSRenderContextD3D;
+
+    PLSRenderTargetD3D(ID3D11Device*, size_t width, size_t height);
+
+    ComPtr<ID3D11Texture2D> m_targetTexture;
+    ComPtr<ID3D11Texture2D> m_coverageTexture;
+    ComPtr<ID3D11Texture2D> m_originalDstColorTexture;
+    ComPtr<ID3D11Texture2D> m_clipTexture;
+
+    ComPtr<ID3D11UnorderedAccessView> m_targetUAV;
+    ComPtr<ID3D11UnorderedAccessView> m_coverageUAV;
+    ComPtr<ID3D11UnorderedAccessView> m_originalDstColorUAV;
+    ComPtr<ID3D11UnorderedAccessView> m_clipUAV;
+};
+
+// D3D backend implementation of PLSRenderContext.
+class PLSRenderContextD3D : public PLSRenderContext
+{
+public:
+    PLSRenderContextD3D(ComPtr<ID3D11Device>, ComPtr<ID3D11DeviceContext>, bool isIntel);
+
+    rcp<PLSRenderTargetD3D> makeRenderTarget(size_t width, size_t height);
+
+private:
+    std::unique_ptr<BufferRingImpl> makeVertexBufferRing(size_t capacity,
+                                                         size_t itemSizeInBytes) override;
+
+    std::unique_ptr<TexelBufferRing> makeTexelBufferRing(TexelBufferRing::Format,
+                                                         Renderable,
+                                                         size_t widthInItems,
+                                                         size_t height,
+                                                         size_t texelsPerItem,
+                                                         int textureIdx,
+                                                         TexelBufferRing::Filter) override;
+
+    std::unique_ptr<BufferRingImpl> makeUniformBufferRing(size_t capacity,
+                                                          size_t itemSizeInBytes) override;
+
+    void allocateTessellationTexture(size_t height) override;
+
+    void onFlush(FlushType,
+                 LoadAction,
+                 size_t gradSpanCount,
+                 size_t gradSpansHeight,
+                 size_t tessVertexSpanCount,
+                 size_t tessDataHeight,
+                 bool needsClipBuffer) override;
+
+    void setPipelineLayoutAndShaders(DrawType, const ShaderFeatures&);
+
+    const bool m_isIntel;
+
+    ComPtr<ID3D11Device> m_gpu;
+    ComPtr<ID3D11DeviceContext> m_gpuContext;
+    ComPtr<ID3D11Texture2D> m_tessTexture;
+    ComPtr<ID3D11ShaderResourceView> m_tessTextureSRV;
+    ComPtr<ID3D11RenderTargetView> m_tessTextureRTV;
+
+    ComPtr<ID3D11RasterizerState> m_rasterState;
+    ComPtr<ID3D11RasterizerState> m_debugWireframeState;
+
+    ComPtr<ID3D11InputLayout> m_colorRampLayout;
+    ComPtr<ID3D11VertexShader> m_colorRampVertexShader;
+    ComPtr<ID3D11PixelShader> m_colorRampPixelShader;
+
+    ComPtr<ID3D11InputLayout> m_tessellateLayout;
+    ComPtr<ID3D11VertexShader> m_tessellateVertexShader;
+    ComPtr<ID3D11PixelShader> m_tessellatePixelShader;
+
+    struct DrawVertexShader
+    {
+        ComPtr<ID3D11InputLayout> layout;
+        ComPtr<ID3D11VertexShader> shader;
+    };
+    std::map<uint32_t, DrawVertexShader> m_drawVertexShaders;
+    std::map<uint32_t, ComPtr<ID3D11PixelShader>> m_drawPixelShaders;
+
+    ComPtr<ID3D11Buffer> m_patchVertexBuffer;
+    ComPtr<ID3D11Buffer> m_patchIndexBuffer;
+
+    struct PerDrawUniforms
+    {
+        PerDrawUniforms(uint32_t baseInstance_) : baseInstance(baseInstance_) {}
+        uint32_t baseInstance;
+        uint32_t pad0;
+        uint32_t pad1;
+        uint32_t pad2;
+    };
+    static_assert(sizeof(PerDrawUniforms) == 16);
+
+    ComPtr<ID3D11Buffer> m_perDrawUniforms;
+
+    ComPtr<ID3D11SamplerState> m_gradSampler;
+};
+} // namespace rive::pls
diff --git a/include/rive/pls/gl/pls_render_context_gl.hpp b/include/rive/pls/gl/pls_render_context_gl.hpp
index d04ad89..2098c47 100644
--- a/include/rive/pls/gl/pls_render_context_gl.hpp
+++ b/include/rive/pls/gl/pls_render_context_gl.hpp
@@ -93,8 +93,8 @@
     class PLSImplWebGL;
     class PLSImplRWTexture;
 
-    static std::unique_ptr<PLSImpl> MakePLSImplEXTNative();
-    static std::unique_ptr<PLSImpl> MakePLSImplFramebufferFetch(GLExtensions);
+    static std::unique_ptr<PLSImpl> MakePLSImplEXTNative(const GLExtensions&);
+    static std::unique_ptr<PLSImpl> MakePLSImplFramebufferFetch(const GLExtensions&);
     static std::unique_ptr<PLSImpl> MakePLSImplWebGL();
     static std::unique_ptr<PLSImpl> MakePLSImplRWTexture();
 
@@ -129,6 +129,7 @@
                                                          size_t itemSizeInBytes) override;
 
     std::unique_ptr<TexelBufferRing> makeTexelBufferRing(TexelBufferRing::Format,
+                                                         Renderable,
                                                          size_t widthInItems,
                                                          size_t height,
                                                          size_t texelsPerItem,
diff --git a/include/rive/pls/metal/pls_render_context_metal.h b/include/rive/pls/metal/pls_render_context_metal.h
index afaf3f8..ef3ef48 100644
--- a/include/rive/pls/metal/pls_render_context_metal.h
+++ b/include/rive/pls/metal/pls_render_context_metal.h
@@ -34,6 +34,7 @@
                                                          size_t itemSizeInBytes) override;
 
     std::unique_ptr<TexelBufferRing> makeTexelBufferRing(TexelBufferRing::Format,
+                                                         Renderable,
                                                          size_t widthInItems,
                                                          size_t height,
                                                          size_t texelsPerItem,
diff --git a/include/rive/pls/pls.hpp b/include/rive/pls/pls.hpp
index eea399e..83ce78d 100644
--- a/include/rive/pls/pls.hpp
+++ b/include/rive/pls/pls.hpp
@@ -339,6 +339,7 @@
     uint32_t segmentCounts;      // [joinSegmentCount, polarSegmentCount, parametricSegmentCount]
     uint32_t contourIDWithFlags; // flags | contourID
 };
+static_assert(sizeof(TessVertexSpan) == sizeof(float) * 16);
 
 // Per-vertex data for shaders that draw triangles.
 struct TriangleVertex
@@ -350,7 +351,7 @@
     Vec2D point;
     int32_t weight_pathID; // [(weight << 16]
 };
-static_assert(sizeof(TriangleVertex) == 3 * sizeof(float));
+static_assert(sizeof(TriangleVertex) == sizeof(float) * 3);
 
 // Once all curves in a contour have been tessellated, we render the tessellated vertices in
 // "patches" (aka specific instanced geometry).
@@ -404,17 +405,16 @@
 constexpr static uint32_t kMidpointFanPatchIndexCount =
     kMidpointFanPatchSegmentSpan * 6 /*AA outer ramp*/ +
     (kMidpointFanPatchSegmentSpan - 1) * 3 /*Curve fan*/ + 3 /*Triangle from path midpoint*/;
-constexpr static uint32_t kMidpointFanPatchIndexOffset = 0;
-static_assert(kMidpointFanPatchIndexOffset % 4 == 0);
+constexpr static uint32_t kMidpointFanPatchBaseIndex = 0;
+static_assert((kMidpointFanPatchBaseIndex * sizeof(uint16_t)) % 4 == 0);
 constexpr static uint32_t kOuterCurvePatchVertexCount =
     (kOuterCurvePatchSegmentSpan + 1) * 3 /*AA center ramp with bowtie*/ +
     kOuterCurvePatchSegmentSpan /*Curve fan*/;
 constexpr static uint32_t kOuterCurvePatchIndexCount =
     kOuterCurvePatchSegmentSpan * 12 /*AA center ramp with bowtie*/ +
     (kOuterCurvePatchSegmentSpan - 2) * 3 /*Curve fan*/;
-constexpr static uint32_t kOuterCurvePatchIndexOffset =
-    kMidpointFanPatchIndexCount * sizeof(uint16_t);
-static_assert(kOuterCurvePatchIndexOffset % 4 == 0);
+constexpr static uint32_t kOuterCurvePatchBaseIndex = kMidpointFanPatchIndexCount;
+static_assert((kOuterCurvePatchBaseIndex * sizeof(uint16_t)) % 4 == 0);
 constexpr static uint32_t kPatchVertexBufferCount =
     kMidpointFanPatchVertexCount + kOuterCurvePatchVertexCount;
 constexpr static uint32_t kPatchIndexBufferCount =
diff --git a/include/rive/pls/pls_render_context.hpp b/include/rive/pls/pls_render_context.hpp
index 5edec76..f1a3916 100644
--- a/include/rive/pls/pls_render_context.hpp
+++ b/include/rive/pls/pls_render_context.hpp
@@ -259,7 +259,14 @@
     virtual std::unique_ptr<BufferRingImpl> makeVertexBufferRing(size_t capacity,
                                                                  size_t itemSizeInBytes) = 0;
 
+    enum class Renderable : bool
+    {
+        no,
+        yes
+    };
+
     virtual std::unique_ptr<TexelBufferRing> makeTexelBufferRing(TexelBufferRing::Format,
+                                                                 Renderable,
                                                                  size_t widthInItems,
                                                                  size_t height,
                                                                  size_t texelsPerItem,
@@ -378,14 +385,14 @@
         }
     }
 
-    constexpr static uintptr_t PatchIndexOffset(DrawType drawType)
+    constexpr static uintptr_t PatchBaseIndex(DrawType drawType)
     {
         switch (drawType)
         {
             case DrawType::midpointFanPatches:
-                return kMidpointFanPatchIndexOffset;
+                return kMidpointFanPatchBaseIndex;
             case DrawType::outerCurvePatches:
-                return kOuterCurvePatchIndexOffset;
+                return kOuterCurvePatchBaseIndex;
             default:
                 RIVE_UNREACHABLE();
         }
diff --git a/out/premake5.lua b/out/premake5.lua
index 173da02..01659e3 100644
--- a/out/premake5.lua
+++ b/out/premake5.lua
@@ -37,7 +37,10 @@
                  RIVE_RUNTIME_DIR .. "/skia/dependencies/glfw/include"}
     flags { "FatalWarnings" }
 
-    files {"../path_fiddle/*.cpp"}
+    files {
+        "../path_fiddle/path_fiddle.cpp",
+        "../path_fiddle/fiddle_context_gl.cpp",
+    }
 
     links {"rive", "rive_pls_renderer", "rive_harfbuzz", "rive_sheenbidi"}
 
@@ -53,15 +56,16 @@
 
     filter "system:windows"
     do
+        files {"../path_fiddle/fiddle_context_d3d.cpp"}
         architecture "x64"
         defines {"RIVE_WINDOWS", "_CRT_SECURE_NO_WARNINGS"}
         libdirs {RIVE_RUNTIME_DIR .. "/skia/dependencies/glfw_build/src/Release"}
-        links {"glfw3", "opengl32"}
+        links {"glfw3", "opengl32", "d3d11", "dxgi", "d3dcompiler"}
     end
 
     filter "system:macosx"
     do
-        files {"../path_fiddle/*.mm"}
+        files {"../path_fiddle/fiddle_context_metal.mm"}
         buildoptions {"-fobjc-arc"}
         links {"glfw3",
                "Cocoa.framework",
diff --git a/out/premake5_pls_renderer.lua b/out/premake5_pls_renderer.lua
index d96670f..abb78aa 100644
--- a/out/premake5_pls_renderer.lua
+++ b/out/premake5_pls_renderer.lua
@@ -156,6 +156,7 @@
     filter "system:windows"
     do
         architecture "x64"
+        files {"../renderer/d3d/*.cpp"}
     end
 
     if os.host() == 'macosx'
diff --git a/path_fiddle/fiddle_context.hpp b/path_fiddle/fiddle_context.hpp
index 47d699d..99ab6c8 100644
--- a/path_fiddle/fiddle_context.hpp
+++ b/path_fiddle/fiddle_context.hpp
@@ -10,6 +10,7 @@
 
 constexpr static int kZoomWindowWidth = 70, kZoomWindowHeight = 42, kZoomWindowScale = 16;
 
+extern bool g_preferIntelGPU;
 extern struct GLFWwindow* g_window;
 extern bool g_wireframe;
 extern bool g_disableFill;
@@ -33,4 +34,7 @@
 #ifdef __APPLE__
     static std::unique_ptr<FiddleContext> MakeMetalPLS();
 #endif
+#ifdef _WIN32
+    static std::unique_ptr<FiddleContext> MakeD3DPLS();
+#endif
 };
diff --git a/path_fiddle/fiddle_context_d3d.cpp b/path_fiddle/fiddle_context_d3d.cpp
new file mode 100644
index 0000000..4838079
--- /dev/null
+++ b/path_fiddle/fiddle_context_d3d.cpp
@@ -0,0 +1,147 @@
+#include "fiddle_context.hpp"
+
+#include "rive/pls/pls_factory.hpp"
+#include "rive/pls/pls_renderer.hpp"
+#include "rive/pls/d3d/pls_render_context_d3d.hpp"
+#include "rive/pls/d3d/d3d11.hpp"
+#include <array>
+#include <dxgi1_2.h>
+
+#define GLFW_INCLUDE_NONE
+#define GLFW_EXPOSE_NATIVE_WIN32
+#include "GLFW/glfw3.h"
+#include <GLFW/glfw3native.h>
+
+using namespace rive;
+using namespace rive::pls;
+
+class FiddleContextD3DPLS : public FiddleContext
+{
+public:
+    FiddleContextD3DPLS(ComPtr<IDXGIFactory2> d3dFactory,
+                        ComPtr<ID3D11Device> gpu,
+                        ComPtr<ID3D11DeviceContext> gpuContext,
+                        bool isIntel) :
+        m_d3dFactory(std::move(d3dFactory)),
+        m_gpu(std::move(gpu)),
+        m_gpuContext(std::move(gpuContext)),
+        m_plsContext(new PLSRenderContextD3D(m_gpu, m_gpuContext, isIntel))
+    {}
+
+    float dpiScale() const override { return 1; }
+
+    std::unique_ptr<Factory> makeFactory() override { return std::make_unique<PLSFactory>(); }
+
+    void onSizeChanged(int width, int height) override
+    {
+        m_swapchain.Reset();
+
+        DXGI_SWAP_CHAIN_DESC1 scd{};
+        scd.Width = width;
+        scd.Height = height;
+        scd.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
+        scd.SampleDesc.Count = 1;
+        scd.BufferUsage = DXGI_USAGE_UNORDERED_ACCESS;
+        scd.BufferCount = 2;
+        scd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
+        VERIFY_OK(m_d3dFactory->CreateSwapChainForHwnd(m_gpu.Get(),
+                                                       glfwGetWin32Window(g_window),
+                                                       &scd,
+                                                       NULL,
+                                                       NULL,
+                                                       m_swapchain.GetAddressOf()));
+
+        m_renderTarget = m_plsContext->makeRenderTarget(width, height);
+    }
+
+    void toggleZoomWindow() override {}
+
+    std::unique_ptr<Renderer> makeRenderer(int width, int height) override
+    {
+        return std::make_unique<PLSRenderer>(m_plsContext.get());
+    }
+
+    void begin() override
+    {
+        ComPtr<ID3D11Texture2D> backBuffer;
+        VERIFY_OK(m_swapchain->GetBuffer(0,
+                                         __uuidof(ID3D11Texture2D),
+                                         reinterpret_cast<void**>(backBuffer.GetAddressOf())));
+        m_renderTarget->setTargetTexture(m_gpu.Get(), std::move(backBuffer));
+
+        PLSRenderContext::FrameDescriptor frameDescriptor;
+        frameDescriptor.renderTarget = m_renderTarget;
+        frameDescriptor.clearColor = 0xff404040;
+        frameDescriptor.wireframe = g_wireframe;
+        frameDescriptor.fillsDisabled = g_disableFill;
+        frameDescriptor.strokesDisabled = g_disableStroke;
+        m_plsContext->beginFrame(std::move(frameDescriptor));
+    }
+
+    void end() override
+    {
+        m_plsContext->flush();
+        m_swapchain->Present(0, 0);
+    }
+
+    void shrinkGPUResourcesToFit() final { m_plsContext->shrinkGPUResourcesToFit(); }
+
+private:
+    ComPtr<IDXGIFactory2> m_d3dFactory;
+    ComPtr<ID3D11Device> m_gpu;
+    ComPtr<ID3D11DeviceContext> m_gpuContext;
+    ComPtr<IDXGISwapChain1> m_swapchain;
+    std::unique_ptr<PLSRenderContextD3D> m_plsContext;
+    rcp<PLSRenderTargetD3D> m_renderTarget;
+};
+
+std::unique_ptr<FiddleContext> FiddleContext::MakeD3DPLS()
+{
+    // Create a DXGIFactory object.
+    ComPtr<IDXGIFactory2> factory;
+    VERIFY_OK(CreateDXGIFactory(__uuidof(IDXGIFactory2),
+                                reinterpret_cast<void**>(factory.GetAddressOf())));
+
+    ComPtr<IDXGIAdapter> adapter;
+    DXGI_ADAPTER_DESC adapterDesc{};
+    bool isIntel = false;
+    for (UINT i = 0; factory->EnumAdapters(i, &adapter) != DXGI_ERROR_NOT_FOUND; ++i)
+    {
+        adapter->GetDesc(&adapterDesc);
+        isIntel = adapterDesc.VendorId == 0x163C || adapterDesc.VendorId == 0x8086 ||
+                  adapterDesc.VendorId == 0x8087;
+        if (isIntel == g_preferIntelGPU)
+        {
+            break;
+        }
+    }
+
+    ComPtr<ID3D11Device> gpu;
+    ComPtr<ID3D11DeviceContext> gpuContext;
+    D3D_FEATURE_LEVEL featureLevels[] = {D3D_FEATURE_LEVEL_11_1};
+    VERIFY_OK(D3D11CreateDevice(adapter.Get(),
+                                D3D_DRIVER_TYPE_UNKNOWN,
+                                NULL,
+#ifdef DEBUG
+                                D3D11_CREATE_DEVICE_DEBUG,
+#else
+                                0,
+#endif
+                                featureLevels,
+                                std::size(featureLevels),
+                                D3D11_SDK_VERSION,
+                                gpu.GetAddressOf(),
+                                NULL,
+                                gpuContext.GetAddressOf()));
+    if (!gpu || !gpuContext)
+    {
+        return nullptr;
+    }
+
+    printf("D3D device: %S\n", adapterDesc.Description);
+
+    return std::make_unique<FiddleContextD3DPLS>(std::move(factory),
+                                                 std::move(gpu),
+                                                 std::move(gpuContext),
+                                                 isIntel);
+}
diff --git a/path_fiddle/path_fiddle.cpp b/path_fiddle/path_fiddle.cpp
index 4131c1c..498993e 100644
--- a/path_fiddle/path_fiddle.cpp
+++ b/path_fiddle/path_fiddle.cpp
@@ -22,6 +22,7 @@
 
 using namespace rive;
 
+bool g_preferIntelGPU = false;
 GLFWwindow* g_window = nullptr;
 bool g_wireframe = false;
 bool g_disableFill = false;
@@ -215,10 +216,18 @@
     }
 }
 
-bool metal = false;
-bool angle = false;
 bool skia = false;
 
+enum class API
+{
+    gl,
+    metal,
+    d3d
+};
+
+API api = API::gl;
+bool angle = false;
+
 int lastWidth = 0, lastHeight = 0;
 double fpsLastTime = glfwGetTime();
 int fpsFrames = 0;
@@ -266,17 +275,20 @@
     {
         if (!strcmp(argv[i], "--gl"))
         {
-            metal = false;
+            api = API::gl;
         }
         else if (!strcmp(argv[i], "--metal"))
         {
-            metal = true;
+            api = API::metal;
+        }
+        else if (!strcmp(argv[i], "--d3d"))
+        {
+            api = API::d3d;
         }
         else if (!strcmp(argv[i], "--skia"))
         {
             skia = true;
         }
-#ifdef RIVE_DESKTOP_GL
         else if (!strcmp(argv[i], "--angle_gl"))
         {
             glfwInitHint(GLFW_ANGLE_PLATFORM_TYPE, GLFW_ANGLE_PLATFORM_TYPE_OPENGL);
@@ -297,7 +309,10 @@
             glfwInitHint(GLFW_ANGLE_PLATFORM_TYPE, GLFW_ANGLE_PLATFORM_TYPE_METAL);
             angle = true;
         }
-#endif
+        else if (!strcmp(argv[i], "--intel") || !strcmp(argv[i], "-i"))
+        {
+            g_preferIntelGPU = true;
+        }
         else if (sscanf(argv[i], "-a%i", &s_animation))
         {}
         else if (sscanf(argv[i], "-h%i", &s_horzRepeat))
@@ -324,23 +339,30 @@
 
     glfwWindowHint(GLFW_SAMPLES, 0);
     glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
-    if (metal)
+    switch (api)
     {
-        glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
-        glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GLFW_TRUE);
+        case API::metal:
+        case API::d3d:
+            glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
+            glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GLFW_TRUE);
+            break;
+        case API::gl:
+            if (angle)
+            {
+                glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API);
+                glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
+                glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
+                glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
+                glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
+            }
+            else
+            {
+                glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
+            }
+            break;
     }
-    else if (angle)
-    {
-        glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API);
-        glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
-        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
-        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
-        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
-    }
-    else
-    {
-        glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API);
-    }
+    glfwWindowHint(GLFW_FOCUS_ON_SHOW, GLFW_TRUE);
+    glfwWindowHint(GLFW_FLOATING, GLFW_TRUE);
     g_window = glfwCreateWindow(1600, 1600, "Rive Renderer", nullptr, nullptr);
     if (!g_window)
     {
@@ -354,27 +376,40 @@
     glfwSetCursorPosCallback(g_window, mousemove_callback);
     glfwSetKeyCallback(g_window, key_callback);
     glfwMakeContextCurrent(g_window);
+    glfwShowWindow(g_window);
 
-    if (metal)
+    switch (api)
     {
-        if (skia)
-        {
-            fprintf(stderr, "Skia not supported on Metal yet.\n");
-            exit(-1);
-        }
+        case API::metal:
+            if (skia)
+            {
+                fprintf(stderr, "Skia not supported on Metal yet.\n");
+                break;
+            }
 #ifdef __APPLE__
-        s_fiddleContext = FiddleContext::MakeMetalPLS();
+            s_fiddleContext = FiddleContext::MakeMetalPLS();
 #endif
-    }
-    else if (skia)
-    {
+            break;
+        case API::d3d:
+            if (skia)
+            {
+                fprintf(stderr, "Skia not supported on d3d yet.\n");
+                break;
+            }
+#ifdef _WIN32
+            s_fiddleContext = FiddleContext::MakeD3DPLS();
+#endif
+            break;
+        case API::gl:
+            if (skia)
+            {
 #ifdef RIVE_SKIA
-        s_fiddleContext = FiddleContext::MakeGLSkia();
+                s_fiddleContext = FiddleContext::MakeGLSkia();
 #endif
-    }
-    else
-    {
-        s_fiddleContext = FiddleContext::MakeGLPLS();
+                break;
+            }
+            s_fiddleContext = FiddleContext::MakeGLPLS();
+            break;
     }
     if (!s_fiddleContext)
     {
diff --git a/renderer/d3d/pls_render_context_d3d.cpp b/renderer/d3d/pls_render_context_d3d.cpp
new file mode 100644
index 0000000..c2f89c8
--- /dev/null
+++ b/renderer/d3d/pls_render_context_d3d.cpp
@@ -0,0 +1,830 @@
+/*
+ * Copyright 2023 Rive
+ */
+
+#include "rive/pls/d3d/pls_render_context_d3d.hpp"
+
+#include <D3DCompiler.h>
+#include <sstream>
+
+#include "../out/obj/generated/advanced_blend.glsl.hpp"
+#include "../out/obj/generated/color_ramp.glsl.hpp"
+#include "../out/obj/generated/common.glsl.hpp"
+#include "../out/obj/generated/draw.glsl.hpp"
+#include "../out/obj/generated/hlsl.glsl.hpp"
+#include "../out/obj/generated/tessellate.glsl.hpp"
+
+constexpr static UINT kPatchVertexDataSlot = 0;
+constexpr static UINT kTriangleVertexDataSlot = 1;
+
+namespace rive::pls
+{
+static DXGI_FORMAT d3d_format(TexelBufferRing::Format format)
+{
+
+    switch (format)
+    {
+        case TexelBufferRing::Format::rgba32f:
+            return DXGI_FORMAT_R32G32B32A32_FLOAT;
+        case TexelBufferRing::Format::rgba32ui:
+            return DXGI_FORMAT_R32G32B32A32_UINT;
+        case TexelBufferRing::Format::rgba8:
+            return DXGI_FORMAT_R8G8B8A8_UNORM;
+    }
+}
+
+static ComPtr<ID3D11Texture2D> make_simple_2d_texture(ID3D11Device* gpu,
+                                                      DXGI_FORMAT format,
+                                                      size_t width,
+                                                      size_t height,
+                                                      UINT bindFlags)
+{
+    D3D11_TEXTURE2D_DESC desc{};
+    desc.Width = width;
+    desc.Height = height;
+    desc.MipLevels = 1;
+    desc.ArraySize = 1;
+    desc.Format = format;
+    desc.SampleDesc.Count = 1;
+    desc.Usage = D3D11_USAGE_DEFAULT;
+    desc.BindFlags = bindFlags;
+    desc.CPUAccessFlags = 0;
+    desc.MiscFlags = 0;
+
+    ComPtr<ID3D11Texture2D> tex;
+    VERIFY_OK(gpu->CreateTexture2D(&desc, NULL, tex.GetAddressOf()));
+    return tex;
+}
+
+static ComPtr<ID3D11ShaderResourceView> make_simple_2d_srv(ID3D11Device* gpu,
+                                                           ID3D11Texture2D* tex2D,
+                                                           DXGI_FORMAT format)
+{
+    D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
+    srvDesc.Format = format;
+    srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
+    srvDesc.Texture2D.MostDetailedMip = 0;
+    srvDesc.Texture2D.MipLevels = 1;
+
+    ComPtr<ID3D11ShaderResourceView> srv;
+    VERIFY_OK(gpu->CreateShaderResourceView(tex2D, &srvDesc, srv.GetAddressOf()));
+    return srv;
+}
+
+static ComPtr<ID3D11RenderTargetView> make_simple_2d_rtv(ID3D11Device* gpu,
+                                                         ID3D11Texture2D* tex2D,
+                                                         DXGI_FORMAT format)
+{
+    D3D11_RENDER_TARGET_VIEW_DESC rtDesc;
+    rtDesc.Format = format;
+    rtDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
+    rtDesc.Texture2D.MipSlice = 0;
+
+    ComPtr<ID3D11RenderTargetView> rtv;
+    VERIFY_OK(gpu->CreateRenderTargetView(tex2D, &rtDesc, &rtv));
+    return rtv;
+}
+
+static ComPtr<ID3D11UnorderedAccessView> make_simple_2d_uav(ID3D11Device* gpu,
+                                                            ID3D11Texture2D* tex,
+                                                            DXGI_FORMAT format)
+{
+    D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc{};
+    uavDesc.Format = format;
+    uavDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2D;
+
+    ComPtr<ID3D11UnorderedAccessView> uav;
+    VERIFY_OK(gpu->CreateUnorderedAccessView(tex, &uavDesc, uav.GetAddressOf()));
+    return uav;
+}
+
+static ComPtr<ID3DBlob> compile_source_to_blob(ID3D11Device* gpu,
+                                               const char* shaderTypeDefineName,
+                                               const std::string& commonSource,
+                                               const char* entrypoint,
+                                               const char* target)
+{
+    std::ostringstream source;
+    source << "#define " << shaderTypeDefineName << '\n';
+    source << commonSource;
+
+    const std::string& sourceStr = source.str();
+    ComPtr<ID3DBlob> blob;
+    ComPtr<ID3DBlob> errors;
+    HRESULT hr = D3DCompile(sourceStr.c_str(),
+                            sourceStr.length(),
+                            nullptr,
+                            nullptr,
+                            nullptr,
+                            entrypoint,
+                            target,
+                            D3DCOMPILE_ENABLE_STRICTNESS,
+                            0,
+                            &blob,
+                            &errors);
+    if (errors && errors->GetBufferPointer())
+    {
+        fprintf(stderr, "Errors or warnings compiling shader.\n");
+        int l = 1;
+        std::stringstream stream(sourceStr);
+        std::string lineStr;
+        while (std::getline(stream, lineStr, '\n'))
+        {
+            fprintf(stderr, "%4i| %s\n", l++, lineStr.c_str());
+        }
+        fprintf(stderr, "%s\n", reinterpret_cast<char*>(errors->GetBufferPointer()));
+        exit(-1);
+    }
+    if (FAILED(hr))
+    {
+        fprintf(stderr, "Failed to compile shader.\n");
+        exit(-1);
+    }
+    return blob;
+}
+
+PLSRenderContextD3D::PLSRenderContextD3D(ComPtr<ID3D11Device> gpu,
+                                         ComPtr<ID3D11DeviceContext> gpuContext,
+                                         bool isIntel) :
+    PLSRenderContext(PlatformFeatures()), m_isIntel(isIntel), m_gpu(gpu), m_gpuContext(gpuContext)
+{
+    D3D11_RASTERIZER_DESC rasterDesc;
+    rasterDesc.FillMode = D3D11_FILL_SOLID;
+    rasterDesc.CullMode = D3D11_CULL_NONE;
+    rasterDesc.FrontCounterClockwise = FALSE; // FrontCounterClockwise must be FALSE in order to
+                                              // match the winding sense of interior triangulations.
+    rasterDesc.DepthBias = 0;
+    rasterDesc.SlopeScaledDepthBias = 0;
+    rasterDesc.DepthBiasClamp = 0;
+    rasterDesc.DepthClipEnable = FALSE;
+    rasterDesc.ScissorEnable = FALSE;
+    rasterDesc.MultisampleEnable = FALSE;
+    rasterDesc.AntialiasedLineEnable = FALSE;
+    VERIFY_OK(m_gpu->CreateRasterizerState(&rasterDesc, m_rasterState.GetAddressOf()));
+    m_gpuContext->RSSetState(m_rasterState.Get());
+
+    // Make one more raster state for wireframe, for debugging purposes.
+    rasterDesc.FillMode = D3D11_FILL_WIREFRAME;
+    VERIFY_OK(m_gpu->CreateRasterizerState(&rasterDesc, m_debugWireframeState.GetAddressOf()));
+
+    // Compile the shaders that render gradient color ramps.
+    {
+        std::ostringstream s;
+        s << glsl::hlsl << '\n';
+        s << glsl::common << '\n';
+        s << glsl::color_ramp << '\n';
+        ComPtr<ID3DBlob> vertexBlob = compile_source_to_blob(m_gpu.Get(),
+                                                             GLSL_VERTEX,
+                                                             s.str().c_str(),
+                                                             GLSL_colorRampVertexMain,
+                                                             "vs_5_0");
+        ComPtr<ID3DBlob> pixelBlob = compile_source_to_blob(m_gpu.Get(),
+                                                            GLSL_FRAGMENT,
+                                                            s.str().c_str(),
+                                                            GLSL_colorRampFragmentMain,
+                                                            "ps_5_0");
+        D3D11_INPUT_ELEMENT_DESC spanDesc =
+            {GLSL_a_span, 0, DXGI_FORMAT_R32G32B32A32_UINT, 0, 0, D3D11_INPUT_PER_INSTANCE_DATA, 1};
+        VERIFY_OK(m_gpu->CreateInputLayout(&spanDesc,
+                                           1,
+                                           vertexBlob->GetBufferPointer(),
+                                           vertexBlob->GetBufferSize(),
+                                           &m_colorRampLayout));
+        VERIFY_OK(m_gpu->CreateVertexShader(vertexBlob->GetBufferPointer(),
+                                            vertexBlob->GetBufferSize(),
+                                            nullptr,
+                                            &m_colorRampVertexShader));
+        VERIFY_OK(m_gpu->CreatePixelShader(pixelBlob->GetBufferPointer(),
+                                           pixelBlob->GetBufferSize(),
+                                           nullptr,
+                                           &m_colorRampPixelShader));
+    }
+
+    // Compile the tessellation shaders.
+    {
+        std::ostringstream s;
+        s << glsl::hlsl << '\n';
+        s << glsl::common << '\n';
+        s << glsl::tessellate << '\n';
+        ComPtr<ID3DBlob> vertexBlob = compile_source_to_blob(m_gpu.Get(),
+                                                             GLSL_VERTEX,
+                                                             s.str().c_str(),
+                                                             GLSL_tessellateVertexMain,
+                                                             "vs_5_0");
+        ComPtr<ID3DBlob> pixelBlob = compile_source_to_blob(m_gpu.Get(),
+                                                            GLSL_FRAGMENT,
+                                                            s.str().c_str(),
+                                                            GLSL_tessellateFragmentMain,
+                                                            "ps_5_0");
+        D3D11_INPUT_ELEMENT_DESC attribsDesc[] = {{GLSL_a_p0p1_,
+                                                   0,
+                                                   DXGI_FORMAT_R32G32B32A32_FLOAT,
+                                                   0,
+                                                   D3D11_APPEND_ALIGNED_ELEMENT,
+                                                   D3D11_INPUT_PER_INSTANCE_DATA,
+                                                   1},
+                                                  {GLSL_a_p2p3_,
+                                                   0,
+                                                   DXGI_FORMAT_R32G32B32A32_FLOAT,
+                                                   0,
+                                                   D3D11_APPEND_ALIGNED_ELEMENT,
+                                                   D3D11_INPUT_PER_INSTANCE_DATA,
+                                                   1},
+                                                  {GLSL_a_joinTangent,
+                                                   0,
+                                                   DXGI_FORMAT_R32G32B32A32_FLOAT,
+                                                   0,
+                                                   D3D11_APPEND_ALIGNED_ELEMENT,
+                                                   D3D11_INPUT_PER_INSTANCE_DATA,
+                                                   1},
+                                                  {GLSL_a_args,
+                                                   0,
+                                                   DXGI_FORMAT_R32G32B32A32_UINT,
+                                                   0,
+                                                   D3D11_APPEND_ALIGNED_ELEMENT,
+                                                   D3D11_INPUT_PER_INSTANCE_DATA,
+                                                   1}};
+        VERIFY_OK(m_gpu->CreateInputLayout(attribsDesc,
+                                           std::size(attribsDesc),
+                                           vertexBlob->GetBufferPointer(),
+                                           vertexBlob->GetBufferSize(),
+                                           &m_tessellateLayout));
+        VERIFY_OK(m_gpu->CreateVertexShader(vertexBlob->GetBufferPointer(),
+                                            vertexBlob->GetBufferSize(),
+                                            nullptr,
+                                            &m_tessellateVertexShader));
+        VERIFY_OK(m_gpu->CreatePixelShader(pixelBlob->GetBufferPointer(),
+                                           pixelBlob->GetBufferSize(),
+                                           nullptr,
+                                           &m_tessellatePixelShader));
+    }
+
+    // Set up the path patch rendering buffers.
+    PatchVertex patchVertices[kPatchVertexBufferCount];
+    uint16_t patchIndices[kPatchIndexBufferCount];
+    GeneratePatchBufferData(patchVertices, patchIndices);
+
+    {
+        D3D11_BUFFER_DESC vertexBufferDesc{};
+        vertexBufferDesc.ByteWidth = sizeof(patchVertices);
+        vertexBufferDesc.Usage = D3D11_USAGE_IMMUTABLE;
+        vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
+        vertexBufferDesc.StructureByteStride = sizeof(PatchVertex);
+
+        D3D11_SUBRESOURCE_DATA vertexDataDesc{};
+        vertexDataDesc.pSysMem = patchVertices;
+
+        VERIFY_OK(gpu->CreateBuffer(&vertexBufferDesc,
+                                    &vertexDataDesc,
+                                    m_patchVertexBuffer.GetAddressOf()));
+    }
+
+    {
+        D3D11_BUFFER_DESC indexBufferDesc{};
+        indexBufferDesc.ByteWidth = sizeof(patchIndices);
+        indexBufferDesc.Usage = D3D11_USAGE_IMMUTABLE;
+        indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;
+
+        D3D11_SUBRESOURCE_DATA indexDataDesc{};
+        indexDataDesc.pSysMem = patchIndices;
+
+        VERIFY_OK(
+            gpu->CreateBuffer(&indexBufferDesc, &indexDataDesc, m_patchIndexBuffer.GetAddressOf()));
+    }
+
+    // Create a buffer for the uniforms that get updated every draw.
+    {
+        D3D11_BUFFER_DESC desc{};
+        desc.ByteWidth = sizeof(PerDrawUniforms);
+        desc.Usage = D3D11_USAGE_DEFAULT;
+        desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
+        desc.StructureByteStride = sizeof(PerDrawUniforms);
+        VERIFY_OK(gpu->CreateBuffer(&desc, nullptr, m_perDrawUniforms.GetAddressOf()));
+    }
+
+    // Create a sampler for the gradient texture.
+    D3D11_SAMPLER_DESC gradSamplerDesc;
+    gradSamplerDesc.Filter = D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT;
+    gradSamplerDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
+    gradSamplerDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
+    gradSamplerDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
+    gradSamplerDesc.MipLODBias = 0.0f;
+    gradSamplerDesc.MaxAnisotropy = 1;
+    gradSamplerDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
+    gradSamplerDesc.MinLOD = 0;
+    gradSamplerDesc.MaxLOD = 0;
+    VERIFY_OK(m_gpu->CreateSamplerState(&gradSamplerDesc, m_gradSampler.GetAddressOf()));
+    m_gpuContext->PSSetSamplers(0, 1, m_gradSampler.GetAddressOf());
+}
+
+class BufferRingD3D : public BufferRingShadowImpl
+{
+public:
+    BufferRingD3D(ID3D11Device* gpu,
+                  ComPtr<ID3D11DeviceContext> gpuContext,
+                  size_t capacity,
+                  size_t itemSizeInBytes,
+                  UINT bindFlags) :
+        BufferRingShadowImpl(capacity, itemSizeInBytes), m_gpuContext(gpuContext)
+    {
+        D3D11_BUFFER_DESC desc{};
+        desc.ByteWidth = itemSizeInBytes * capacity;
+        desc.Usage = D3D11_USAGE_DEFAULT;
+        desc.BindFlags = bindFlags;
+        desc.CPUAccessFlags = 0;
+        desc.StructureByteStride = itemSizeInBytes;
+
+        for (size_t i = 0; i < kBufferRingSize; ++i)
+        {
+            VERIFY_OK(gpu->CreateBuffer(&desc, nullptr, m_buffers[i].GetAddressOf()));
+        }
+    }
+
+    ID3D11Buffer* submittedBuffer() const { return m_buffers[submittedBufferIdx()].Get(); }
+
+protected:
+    void onUnmapAndSubmitBuffer(int bufferIdx, size_t bytesWritten)
+    {
+        if (bytesWritten == capacity() * itemSizeInBytes())
+        {
+            // Constant buffers don't allow partial updates, so special-case the event where we
+            // update the entire buffer.
+            m_gpuContext
+                ->UpdateSubresource(m_buffers[bufferIdx].Get(), 0, NULL, shadowBuffer(), 0, 0);
+        }
+        else
+        {
+            D3D11_BOX box;
+            box.left = 0;
+            box.right = bytesWritten;
+            box.top = 0;
+            box.bottom = 1;
+            box.front = 0;
+            box.back = 1;
+            m_gpuContext
+                ->UpdateSubresource(m_buffers[bufferIdx].Get(), 0, &box, shadowBuffer(), 0, 0);
+        }
+    }
+
+    ComPtr<ID3D11DeviceContext> m_gpuContext;
+    ComPtr<ID3D11Buffer> m_buffers[kBufferRingSize];
+};
+
+std::unique_ptr<BufferRingImpl> PLSRenderContextD3D::makeVertexBufferRing(size_t capacity,
+                                                                          size_t itemSizeInBytes)
+{
+    return std::make_unique<BufferRingD3D>(m_gpu.Get(),
+                                           m_gpuContext,
+                                           capacity,
+                                           itemSizeInBytes,
+                                           D3D11_BIND_VERTEX_BUFFER);
+}
+
+std::unique_ptr<BufferRingImpl> PLSRenderContextD3D::makeUniformBufferRing(size_t capacity,
+                                                                           size_t itemSizeInBytes)
+{
+    return std::make_unique<BufferRingD3D>(m_gpu.Get(),
+                                           m_gpuContext,
+                                           capacity,
+                                           itemSizeInBytes,
+                                           D3D11_BIND_CONSTANT_BUFFER);
+}
+
+class TexelBufferD3D : public TexelBufferRing
+{
+public:
+    TexelBufferD3D(ID3D11Device* gpu,
+                   ComPtr<ID3D11DeviceContext> gpuContext,
+                   Format format,
+                   bool renderable,
+                   size_t widthInItems,
+                   size_t height,
+                   size_t texelsPerItem) :
+        TexelBufferRing(format, widthInItems, height, texelsPerItem), m_gpuContext(gpuContext)
+    {
+        DXGI_FORMAT formatD3D = d3d_format(format);
+        UINT bindFlags = D3D11_BIND_SHADER_RESOURCE;
+        if (renderable)
+        {
+            bindFlags |= D3D11_BIND_RENDER_TARGET;
+        }
+        for (size_t i = 0; i < kBufferRingSize; ++i)
+        {
+            m_textures[i] =
+                make_simple_2d_texture(gpu, formatD3D, widthInTexels(), height, bindFlags);
+            m_srvs[i] = make_simple_2d_srv(gpu, m_textures[i].Get(), formatD3D);
+            if (renderable)
+            {
+                m_rtvs[i] = make_simple_2d_rtv(gpu, m_textures[i].Get(), formatD3D);
+            }
+        }
+    }
+
+    ID3D11ShaderResourceView* submittedSRV() const { return m_srvs[submittedBufferIdx()].Get(); }
+    ID3D11RenderTargetView* submittedRenderTargetView() const
+    {
+        return m_rtvs[submittedBufferIdx()].Get();
+    }
+
+private:
+    void submitTexels(int textureIdx, size_t updateWidthInTexels, size_t updateHeight) override
+    {
+        D3D11_BOX box;
+        box.left = 0;
+        box.right = updateWidthInTexels;
+        box.top = 0;
+        box.bottom = updateHeight;
+        box.front = 0;
+        box.back = 1;
+        m_gpuContext->UpdateSubresource(m_textures[textureIdx].Get(),
+                                        0,
+                                        &box,
+                                        shadowBuffer(),
+                                        widthInItems() * itemSizeInBytes(),
+                                        0);
+    }
+
+    ComPtr<ID3D11DeviceContext> m_gpuContext;
+    ComPtr<ID3D11Texture2D> m_textures[kBufferRingSize];
+    ComPtr<ID3D11ShaderResourceView> m_srvs[kBufferRingSize];
+    ComPtr<ID3D11RenderTargetView> m_rtvs[kBufferRingSize];
+};
+
+std::unique_ptr<TexelBufferRing> PLSRenderContextD3D::makeTexelBufferRing(
+    TexelBufferRing::Format format,
+    Renderable renderable,
+    size_t widthInItems,
+    size_t height,
+    size_t texelsPerItem,
+    int textureIdx,
+    TexelBufferRing::Filter)
+{
+    return std::make_unique<TexelBufferD3D>(m_gpu.Get(),
+                                            m_gpuContext,
+                                            format,
+                                            renderable == Renderable::yes,
+                                            widthInItems,
+                                            height,
+                                            texelsPerItem);
+}
+
+PLSRenderTargetD3D::PLSRenderTargetD3D(ID3D11Device* gpu, size_t width, size_t height) :
+    PLSRenderTarget(width, height)
+{
+    m_coverageTexture = make_simple_2d_texture(gpu,
+                                               DXGI_FORMAT_R32_UINT,
+                                               width,
+                                               height,
+                                               D3D11_BIND_UNORDERED_ACCESS);
+    m_originalDstColorTexture = make_simple_2d_texture(gpu,
+                                                       DXGI_FORMAT_R8G8B8A8_UNORM,
+                                                       width,
+                                                       height,
+                                                       D3D11_BIND_UNORDERED_ACCESS);
+    m_clipTexture = make_simple_2d_texture(gpu,
+                                           DXGI_FORMAT_R32_UINT,
+                                           width,
+                                           height,
+                                           D3D11_BIND_UNORDERED_ACCESS);
+
+    m_coverageUAV = make_simple_2d_uav(gpu, m_coverageTexture.Get(), DXGI_FORMAT_R32_UINT);
+    m_originalDstColorUAV =
+        make_simple_2d_uav(gpu, m_originalDstColorTexture.Get(), DXGI_FORMAT_R8G8B8A8_UNORM);
+    m_clipUAV = make_simple_2d_uav(gpu, m_clipTexture.Get(), DXGI_FORMAT_R32_UINT);
+}
+
+void PLSRenderTargetD3D::setTargetTexture(ID3D11Device* gpu, ComPtr<ID3D11Texture2D> tex)
+{
+#ifdef DEBUG
+    D3D11_TEXTURE2D_DESC desc;
+    tex->GetDesc(&desc);
+    assert(desc.Width == width());
+    assert(desc.Height == height());
+    assert(desc.Format == DXGI_FORMAT_R8G8B8A8_UNORM);
+#endif
+    m_targetTexture = std::move(tex);
+    m_targetUAV = make_simple_2d_uav(gpu, m_targetTexture.Get(), DXGI_FORMAT_R8G8B8A8_UNORM);
+}
+
+rcp<PLSRenderTargetD3D> PLSRenderContextD3D::makeRenderTarget(size_t width, size_t height)
+{
+    return rcp(new PLSRenderTargetD3D(m_gpu.Get(), width, height));
+}
+
+void PLSRenderContextD3D::allocateTessellationTexture(size_t height)
+{
+    m_tessTexture = make_simple_2d_texture(m_gpu.Get(),
+                                           DXGI_FORMAT_R32G32B32A32_UINT,
+                                           kTessTextureWidth,
+                                           height,
+                                           D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE);
+    m_tessTextureSRV =
+        make_simple_2d_srv(m_gpu.Get(), m_tessTexture.Get(), DXGI_FORMAT_R32G32B32A32_UINT);
+    m_tessTextureRTV =
+        make_simple_2d_rtv(m_gpu.Get(), m_tessTexture.Get(), DXGI_FORMAT_R32G32B32A32_UINT);
+}
+
+void PLSRenderContextD3D::setPipelineLayoutAndShaders(DrawType drawType,
+                                                      const ShaderFeatures& shaderFeatures)
+{
+    uint32_t vertexShaderKey = ShaderUniqueKey(SourceType::vertexOnly, drawType, shaderFeatures);
+    auto vertexEntry = m_drawVertexShaders.find(vertexShaderKey);
+
+    uint32_t pixelShaderKey = ShaderUniqueKey(SourceType::wholeProgram, drawType, shaderFeatures);
+    auto pixelEntry = m_drawPixelShaders.find(pixelShaderKey);
+
+    if (vertexEntry == m_drawVertexShaders.end() || pixelEntry == m_drawPixelShaders.end())
+    {
+        std::ostringstream s;
+        uint64_t shaderFeatureDefines =
+            shaderFeatures.getPreprocessorDefines(SourceType::wholeProgram);
+        if (drawType == DrawType::interiorTriangulation)
+        {
+            s << "#define " << GLSL_DRAW_INTERIOR_TRIANGLES << '\n';
+        }
+        if (shaderFeatureDefines & ShaderFeatures::PreprocessorDefines::ENABLE_ADVANCED_BLEND)
+        {
+            s << "#define " << GLSL_ENABLE_ADVANCED_BLEND << '\n';
+        }
+        if (shaderFeatureDefines & ShaderFeatures::PreprocessorDefines::ENABLE_PATH_CLIPPING)
+        {
+            s << "#define " << GLSL_ENABLE_PATH_CLIPPING << '\n';
+        }
+        if (shaderFeatureDefines & ShaderFeatures::PreprocessorDefines::ENABLE_EVEN_ODD)
+        {
+            s << "#define " << GLSL_ENABLE_EVEN_ODD << '\n';
+        }
+        if (shaderFeatureDefines & ShaderFeatures::PreprocessorDefines::ENABLE_HSL_BLEND_MODES)
+        {
+            s << "#define " << GLSL_ENABLE_HSL_BLEND_MODES << '\n';
+        }
+        s << "#define " << GLSL_ENABLE_BASE_INSTANCE_POLYFILL << '\n';
+        s << glsl::hlsl << '\n';
+        s << glsl::common << '\n';
+        if (shaderFeatures.programFeatures.blendTier != BlendTier::srcOver)
+        {
+            s << glsl::advanced_blend << '\n';
+        }
+        s << glsl::draw << '\n';
+
+        const std::string shader = s.str();
+
+        if (vertexEntry == m_drawVertexShaders.end())
+        {
+            DrawVertexShader drawVertexShader;
+            ComPtr<ID3DBlob> blob = compile_source_to_blob(m_gpu.Get(),
+                                                           GLSL_VERTEX,
+                                                           shader.c_str(),
+                                                           GLSL_drawVertexMain,
+                                                           "vs_5_0");
+            D3D11_INPUT_ELEMENT_DESC layoutDesc;
+            switch (drawType)
+            {
+                case DrawType::midpointFanPatches:
+                case DrawType::outerCurvePatches:
+                    layoutDesc = {GLSL_a_patchVertexData,
+                                  0,
+                                  DXGI_FORMAT_R32G32B32A32_FLOAT,
+                                  kPatchVertexDataSlot,
+                                  0,
+                                  D3D11_INPUT_PER_VERTEX_DATA,
+                                  0};
+                    break;
+                case DrawType::interiorTriangulation:
+                    layoutDesc = {GLSL_a_triangleVertex,
+                                  0,
+                                  DXGI_FORMAT_R32G32B32_FLOAT,
+                                  kTriangleVertexDataSlot,
+                                  0,
+                                  D3D11_INPUT_PER_VERTEX_DATA,
+                                  0};
+                    break;
+            }
+            VERIFY_OK(m_gpu->CreateInputLayout(&layoutDesc,
+                                               1,
+                                               blob->GetBufferPointer(),
+                                               blob->GetBufferSize(),
+                                               &drawVertexShader.layout));
+            VERIFY_OK(m_gpu->CreateVertexShader(blob->GetBufferPointer(),
+                                                blob->GetBufferSize(),
+                                                nullptr,
+                                                &drawVertexShader.shader));
+            vertexEntry = m_drawVertexShaders.insert({vertexShaderKey, drawVertexShader}).first;
+        }
+
+        if (pixelEntry == m_drawPixelShaders.end())
+        {
+            ComPtr<ID3D11PixelShader> pixelShader;
+            ComPtr<ID3DBlob> blob = compile_source_to_blob(m_gpu.Get(),
+                                                           GLSL_FRAGMENT,
+                                                           shader.c_str(),
+                                                           GLSL_drawFragmentMain,
+                                                           "ps_5_0");
+            VERIFY_OK(m_gpu->CreatePixelShader(blob->GetBufferPointer(),
+                                               blob->GetBufferSize(),
+                                               nullptr,
+                                               &pixelShader));
+            pixelEntry = m_drawPixelShaders.insert({pixelShaderKey, pixelShader}).first;
+        }
+    }
+
+    m_gpuContext->IASetInputLayout(vertexEntry->second.layout.Get());
+    m_gpuContext->VSSetShader(vertexEntry->second.shader.Get(), NULL, 0);
+    m_gpuContext->PSSetShader(pixelEntry->second.Get(), NULL, 0);
+}
+
+static ID3D11Buffer* submitted_buffer(const BufferRingImpl* bufferRing)
+{
+    return static_cast<const BufferRingD3D*>(bufferRing)->submittedBuffer();
+}
+
+static ID3D11ShaderResourceView* submitted_srv(const TexelBufferRing* texelBufferRing)
+{
+    return static_cast<const TexelBufferD3D*>(texelBufferRing)->submittedSRV();
+}
+
+static ID3D11RenderTargetView* submitted_rtv(const TexelBufferRing* texelBufferRing)
+{
+    return static_cast<const TexelBufferD3D*>(texelBufferRing)->submittedRenderTargetView();
+}
+
+void PLSRenderContextD3D::onFlush(FlushType flushType,
+                                  LoadAction loadAction,
+                                  size_t gradSpanCount,
+                                  size_t gradSpansHeight,
+                                  size_t tessVertexSpanCount,
+                                  size_t tessDataHeight,
+                                  bool needsClipBuffer)
+{
+    auto renderTarget =
+        static_cast<const PLSRenderTargetD3D*>(frameDescriptor().renderTarget.get());
+
+    constexpr static UINT kZero[4]{};
+    if (loadAction == LoadAction::clear)
+    {
+        float clearColor4f[4];
+        UnpackColorToRGBA32F(frameDescriptor().clearColor, clearColor4f);
+        m_gpuContext->ClearUnorderedAccessViewFloat(renderTarget->m_targetUAV.Get(), clearColor4f);
+    }
+    m_gpuContext->ClearUnorderedAccessViewUint(renderTarget->m_coverageUAV.Get(), kZero);
+    if (needsClipBuffer)
+    {
+        m_gpuContext->ClearUnorderedAccessViewUint(renderTarget->m_clipUAV.Get(), kZero);
+    }
+
+    ID3D11Buffer* cbuffers[] = {submitted_buffer(uniformBufferRing()), m_perDrawUniforms.Get()};
+    m_gpuContext->VSSetConstantBuffers(0, std::size(cbuffers), cbuffers);
+
+    // Render the complex color ramps to the gradient texture.
+    if (gradSpanCount > 0)
+    {
+        ID3D11Buffer* gradSpanBuffer = submitted_buffer(gradSpanBufferRing());
+        UINT gradStride = sizeof(GradientSpan);
+        UINT gradOffset = 0;
+        m_gpuContext->IASetVertexBuffers(0, 1, &gradSpanBuffer, &gradStride, &gradOffset);
+        m_gpuContext->IASetInputLayout(m_colorRampLayout.Get());
+        m_gpuContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
+
+        m_gpuContext->VSSetShader(m_colorRampVertexShader.Get(), NULL, 0);
+
+        D3D11_VIEWPORT viewport = {0,
+                                   static_cast<float>(gradTextureRowsForSimpleRamps()),
+                                   static_cast<float>(kGradTextureWidth),
+                                   static_cast<float>(gradSpansHeight),
+                                   0,
+                                   1};
+        m_gpuContext->RSSetViewports(1, &viewport);
+
+        m_gpuContext->PSSetShaderResources(0, 0, NULL);
+        m_gpuContext->PSSetShader(m_colorRampPixelShader.Get(), NULL, 0);
+
+        ID3D11RenderTargetView* rtv = submitted_rtv(gradTexelBufferRing());
+        m_gpuContext->OMSetRenderTargets(1, &rtv, NULL);
+
+        m_gpuContext->DrawInstanced(4, gradSpanCount, 0, 0);
+    }
+
+    // Tessellate all curves into vertices in the tessellation texture.
+    if (tessVertexSpanCount > 0)
+    {
+        ID3D11Buffer* tessSpanBuffer = submitted_buffer(tessSpanBufferRing());
+        UINT tessStride = sizeof(TessVertexSpan);
+        UINT tessOffset = 0;
+        m_gpuContext->IASetVertexBuffers(0, 1, &tessSpanBuffer, &tessStride, &tessOffset);
+        m_gpuContext->IASetInputLayout(m_tessellateLayout.Get());
+        m_gpuContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
+
+        m_gpuContext->VSSetShader(m_tessellateVertexShader.Get(), NULL, 0);
+        ID3D11ShaderResourceView* vsTextureViews[] = {NULL,
+                                                      submitted_srv(pathBufferRing()),
+                                                      submitted_srv(contourBufferRing())};
+        static_assert(kTessVertexTextureIdx == 0);
+        static_assert(kPathTextureIdx == 1);
+        static_assert(kContourTextureIdx == 2);
+        m_gpuContext->VSSetShaderResources(0, std::size(vsTextureViews), vsTextureViews);
+
+        D3D11_VIEWPORT viewport =
+            {0, 0, static_cast<float>(kTessTextureWidth), static_cast<float>(tessDataHeight), 0, 1};
+        m_gpuContext->RSSetViewports(1, &viewport);
+
+        m_gpuContext->PSSetShaderResources(0, 0, NULL);
+        m_gpuContext->PSSetShader(m_tessellatePixelShader.Get(), NULL, 0);
+
+        m_gpuContext->OMSetRenderTargets(1, m_tessTextureRTV.GetAddressOf(), NULL);
+
+        m_gpuContext->DrawInstanced(4, tessVertexSpanCount, 0, 0);
+
+        if (m_isIntel)
+        {
+            // FIXME! Intel needs this flush! Driver bug? Find a lighter workaround?
+            m_gpuContext->Flush();
+        }
+    }
+
+    // Execute the DrawList.
+    ID3D11UnorderedAccessView* plsUAVs[] = {renderTarget->m_targetUAV.Get(),
+                                            renderTarget->m_coverageUAV.Get(),
+                                            renderTarget->m_originalDstColorUAV.Get(),
+                                            renderTarget->m_clipUAV.Get()};
+    static_assert(kFramebufferPlaneIdx == 0);
+    static_assert(kCoveragePlaneIdx == 1);
+    static_assert(kOriginalDstColorPlaneIdx == 2);
+    static_assert(kClipPlaneIdx == 3);
+    m_gpuContext->OMSetRenderTargetsAndUnorderedAccessViews(0,
+                                                            NULL,
+                                                            NULL,
+                                                            0,
+                                                            std::size(plsUAVs),
+                                                            plsUAVs,
+                                                            NULL);
+
+    m_gpuContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
+    m_gpuContext->IASetIndexBuffer(m_patchIndexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0);
+
+    ID3D11Buffer* vertexBuffers[] = {m_patchVertexBuffer.Get(),
+                                     submitted_buffer(triangleBufferRing())};
+    static_assert(kPatchVertexDataSlot == 0);
+    static_assert(kTriangleVertexDataSlot == 1);
+    UINT vertexStrides[] = {sizeof(PatchVertex), sizeof(TriangleVertex)};
+    UINT vertexOffsets[] = {0, 0};
+    m_gpuContext->IASetVertexBuffers(0, 2, vertexBuffers, vertexStrides, vertexOffsets);
+
+    ID3D11ShaderResourceView* vertexTextureViews[] = {m_tessTextureSRV.Get(),
+                                                      submitted_srv(pathBufferRing()),
+                                                      submitted_srv(contourBufferRing())};
+    static_assert(kTessVertexTextureIdx == 0);
+    static_assert(kPathTextureIdx == 1);
+    static_assert(kContourTextureIdx == 2);
+    m_gpuContext->VSSetShaderResources(0, std::size(vertexTextureViews), vertexTextureViews);
+
+    D3D11_VIEWPORT viewport = {0,
+                               0,
+                               static_cast<float>(renderTarget->width()),
+                               static_cast<float>(renderTarget->height()),
+                               0,
+                               1};
+    m_gpuContext->RSSetViewports(1, &viewport);
+    if (frameDescriptor().wireframe)
+    {
+        m_gpuContext->RSSetState(m_debugWireframeState.Get());
+    }
+
+    ID3D11ShaderResourceView* gradTextureView = submitted_srv(gradTexelBufferRing());
+    m_gpuContext->PSSetShaderResources(kGradTextureIdx, 1, &gradTextureView);
+
+    for (const DrawList* draw = m_drawList; draw; draw = draw->next)
+    {
+        if (draw->vertexOrInstanceCount == 0)
+        {
+            continue;
+        }
+
+        DrawType drawType = draw->drawType;
+        setPipelineLayoutAndShaders(drawType, draw->shaderFeatures);
+
+        switch (drawType)
+        {
+            case DrawType::midpointFanPatches:
+            case DrawType::outerCurvePatches:
+            {
+                PerDrawUniforms uniforms(draw->baseVertexOrInstance);
+                m_gpuContext->UpdateSubresource(m_perDrawUniforms.Get(), 0, NULL, &uniforms, 0, 0);
+                m_gpuContext->DrawIndexedInstanced(PatchIndexCount(drawType),
+                                                   draw->vertexOrInstanceCount,
+                                                   PatchBaseIndex(drawType),
+                                                   0,
+                                                   draw->baseVertexOrInstance);
+                break;
+            }
+            case DrawType::interiorTriangulation:
+                m_gpuContext->Draw(draw->vertexOrInstanceCount, draw->baseVertexOrInstance);
+                break;
+        }
+    }
+
+    if (frameDescriptor().wireframe)
+    {
+        m_gpuContext->RSSetState(m_rasterState.Get());
+    }
+}
+} // namespace rive::pls
diff --git a/renderer/gl/buffer_ring_gl.hpp b/renderer/gl/buffer_ring_gl.hpp
index 5b5fc8a..6b41f99 100644
--- a/renderer/gl/buffer_ring_gl.hpp
+++ b/renderer/gl/buffer_ring_gl.hpp
@@ -41,7 +41,7 @@
     GLuint submittedTextureID() const { return m_ids[submittedBufferIdx()]; }
 
 protected:
-    void submitTexels(int bufferIdx, size_t width, size_t height) override;
+    void submitTexels(int textureIdx, size_t updateWidthInTexels, size_t updateHeight) override;
 
 private:
     const GLenum m_activeTextureIdx;
diff --git a/renderer/gl/gl_utils.cpp b/renderer/gl/gl_utils.cpp
index baa611c..75e8a11 100644
--- a/renderer/gl/gl_utils.cpp
+++ b/renderer/gl/gl_utils.cpp
@@ -15,9 +15,10 @@
 void CompileAndAttachShader(GLuint program,
                             GLuint type,
                             const char* source,
+                            const GLExtensions& extensions,
                             const char* versionString)
 {
-    CompileAndAttachShader(program, type, nullptr, 0, &source, 1, versionString);
+    CompileAndAttachShader(program, type, nullptr, 0, &source, 1, extensions, versionString);
 }
 
 void CompileAndAttachShader(GLuint program,
@@ -26,17 +27,26 @@
                             size_t numDefines,
                             const char* inputSources[],
                             size_t numInputSources,
+                            const GLExtensions& extensions,
                             const char* versionString)
 {
-    GLuint shader =
-        CompileShader(type, defines, numDefines, inputSources, numInputSources, versionString);
+    GLuint shader = CompileShader(type,
+                                  defines,
+                                  numDefines,
+                                  inputSources,
+                                  numInputSources,
+                                  extensions,
+                                  versionString);
     glAttachShader(program, shader);
     glDeleteShader(shader);
 }
 
-GLuint CompileShader(GLuint type, const char* source, const char* versionString)
+GLuint CompileShader(GLuint type,
+                     const char* source,
+                     const GLExtensions& extensions,
+                     const char* versionString)
 {
-    return CompileShader(type, nullptr, 0, &source, 1, versionString);
+    return CompileShader(type, nullptr, 0, &source, 1, extensions, versionString);
 }
 
 GLuint CompileShader(GLuint type,
@@ -44,6 +54,7 @@
                      size_t numDefines,
                      const char* inputSources[],
                      size_t numInputSources,
+                     const GLExtensions& extensions,
                      const char* versionString)
 {
     std::vector<const char*> sources;
@@ -56,6 +67,10 @@
     {
         sources.push_back("#define " GLSL_FRAGMENT "\n");
     }
+    if (type == GL_VERTEX_SHADER && !extensions.ANGLE_base_vertex_base_instance_shader_builtin)
+    {
+        sources.push_back("#define " GLSL_ENABLE_BASE_INSTANCE_POLYFILL "\n");
+    }
     std::ostringstream definesStream;
     for (size_t i = 0; i < numDefines; ++i)
     {
diff --git a/renderer/gl/gl_utils.hpp b/renderer/gl/gl_utils.hpp
index 5cebfa1..558fa8c 100644
--- a/renderer/gl/gl_utils.hpp
+++ b/renderer/gl/gl_utils.hpp
@@ -13,6 +13,7 @@
 void CompileAndAttachShader(GLuint program,
                             GLuint type,
                             const char* source,
+                            const GLExtensions&,
                             const char* versionString = nullptr);
 
 void CompileAndAttachShader(GLuint program,
@@ -21,10 +22,12 @@
                             size_t numDefines,
                             const char* sources[],
                             size_t numSources,
+                            const GLExtensions&,
                             const char* versionString = nullptr);
 
 [[nodiscard]] GLuint CompileShader(GLuint type,
                                    const char* source,
+                                   const GLExtensions&,
                                    const char* versionString = nullptr);
 
 [[nodiscard]] GLuint CompileShader(GLuint type,
@@ -32,6 +35,7 @@
                                    size_t numDefines,
                                    const char* sources[],
                                    size_t numSources,
+                                   const GLExtensions&,
                                    const char* versionString = nullptr);
 
 void LinkProgram(GLuint program);
diff --git a/renderer/gl/pls_impl_ext_native.cpp b/renderer/gl/pls_impl_ext_native.cpp
index 6bb75a7..94c11d4 100644
--- a/renderer/gl/pls_impl_ext_native.cpp
+++ b/renderer/gl/pls_impl_ext_native.cpp
@@ -33,7 +33,7 @@
     PLSLoadStoreProgram(const PLSLoadStoreProgram&) = delete;
     PLSLoadStoreProgram& operator=(const PLSLoadStoreProgram&) = delete;
 
-    PLSLoadStoreProgram(uint32_t ops, GLuint vertexShader)
+    PLSLoadStoreProgram(uint32_t ops, GLuint vertexShader, const GLExtensions& extensions)
     {
         std::vector<const char*> defines{GLSL_PLS_IMPL_EXT_NATIVE};
         if (ops & loadstoreops::kClearColor)
@@ -66,7 +66,8 @@
                                         defines.data(),
                                         defines.size(),
                                         &source,
-                                        1);
+                                        1,
+                                        extensions);
         glutils::LinkProgram(m_id);
 
         if (ops & loadstoreops::kClearColor)
@@ -88,12 +89,12 @@
 class PLSRenderContextGL::PLSImplEXTNative : public PLSRenderContextGL::PLSImpl
 {
 public:
-    PLSImplEXTNative()
+    PLSImplEXTNative(const GLExtensions& extensions) : m_extensions(extensions)
     {
         // We have to manually implement load/store operations from a shader when using
         // EXT_shader_pixel_local_storage.
         m_plsLoadStoreVertexShader =
-            glutils::CompileShader(GL_VERTEX_SHADER, glsl::pls_load_store_ext);
+            glutils::CompileShader(GL_VERTEX_SHADER, glsl::pls_load_store_ext, extensions);
         glGenVertexArrays(1, &m_plsLoadStoreVAO);
     }
 
@@ -157,7 +158,8 @@
         {
             // Otherwise we have to initialize pixel local storage with a fullscreen draw.
             const PLSLoadStoreProgram& plsProgram =
-                m_plsLoadStorePrograms.try_emplace(ops, ops, m_plsLoadStoreVertexShader)
+                m_plsLoadStorePrograms
+                    .try_emplace(ops, ops, m_plsLoadStoreVertexShader, m_extensions)
                     .first->second;
             context->bindProgram(plsProgram.id());
             if (plsProgram.clearColorUniLocation() >= 0)
@@ -175,7 +177,8 @@
         // the main framebuffer.
         uint32_t ops = loadstoreops::kStoreColor;
         const PLSLoadStoreProgram& plsProgram =
-            m_plsLoadStorePrograms.try_emplace(ops, ops, m_plsLoadStoreVertexShader).first->second;
+            m_plsLoadStorePrograms.try_emplace(ops, ops, m_plsLoadStoreVertexShader, m_extensions)
+                .first->second;
         context->bindProgram(plsProgram.id());
         context->bindVAO(m_plsLoadStoreVAO);
         glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
@@ -186,13 +189,15 @@
     const char* shaderDefineName() const override { return GLSL_PLS_IMPL_EXT_NATIVE; }
 
 private:
+    const GLExtensions m_extensions;
     std::map<uint32_t, PLSLoadStoreProgram> m_plsLoadStorePrograms; // Keyed by loadstoreops.
     GLuint m_plsLoadStoreVertexShader = 0;
     GLuint m_plsLoadStoreVAO = 0;
 };
 
-std::unique_ptr<PLSRenderContextGL::PLSImpl> PLSRenderContextGL::MakePLSImplEXTNative()
+std::unique_ptr<PLSRenderContextGL::PLSImpl> PLSRenderContextGL::MakePLSImplEXTNative(
+    const GLExtensions& extensions)
 {
-    return std::make_unique<PLSImplEXTNative>();
+    return std::make_unique<PLSImplEXTNative>(extensions);
 }
 } // namespace rive::pls
diff --git a/renderer/gl/pls_impl_framebuffer_fetch.cpp b/renderer/gl/pls_impl_framebuffer_fetch.cpp
index 6796b4d..7454cd6 100644
--- a/renderer/gl/pls_impl_framebuffer_fetch.cpp
+++ b/renderer/gl/pls_impl_framebuffer_fetch.cpp
@@ -18,7 +18,7 @@
 class PLSRenderContextGL::PLSImplFramebufferFetch : public PLSRenderContextGL::PLSImpl
 {
 public:
-    PLSImplFramebufferFetch(GLExtensions extensions) : m_extensions(extensions) {}
+    PLSImplFramebufferFetch(const GLExtensions& extensions) : m_extensions(extensions) {}
 
     rcp<PLSRenderTargetGL> wrapGLRenderTarget(GLuint framebufferID,
                                               size_t width,
@@ -125,7 +125,7 @@
 };
 
 std::unique_ptr<PLSRenderContextGL::PLSImpl> PLSRenderContextGL::MakePLSImplFramebufferFetch(
-    GLExtensions extensions)
+    const GLExtensions& extensions)
 {
     return std::make_unique<PLSImplFramebufferFetch>(extensions);
 }
diff --git a/renderer/gl/pls_render_context_gl.cpp b/renderer/gl/pls_render_context_gl.cpp
index 0562f24..1365c57 100644
--- a/renderer/gl/pls_render_context_gl.cpp
+++ b/renderer/gl/pls_render_context_gl.cpp
@@ -59,6 +59,7 @@
                                     0,
                                     colorRampSources,
                                     2,
+                                    m_extensions,
                                     m_shaderVersionString);
     glutils::CompileAndAttachShader(m_colorRampProgram,
                                     GL_FRAGMENT_SHADER,
@@ -66,6 +67,7 @@
                                     0,
                                     colorRampSources,
                                     2,
+                                    m_extensions,
                                     m_shaderVersionString);
     glutils::LinkProgram(m_colorRampProgram);
     glUniformBlockBinding(m_colorRampProgram,
@@ -87,6 +89,7 @@
                                     0,
                                     tessellateSources,
                                     2,
+                                    m_extensions,
                                     m_shaderVersionString);
     glutils::CompileAndAttachShader(m_tessellateProgram,
                                     GL_FRAGMENT_SHADER,
@@ -94,6 +97,7 @@
                                     0,
                                     tessellateSources,
                                     2,
+                                    m_extensions,
                                     m_shaderVersionString);
     glutils::LinkProgram(m_tessellateProgram);
     bindProgram(m_tessellateProgram);
@@ -133,8 +137,6 @@
     glEnableVertexAttribArray(0);
     glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, nullptr);
 
-    glVertexAttribDivisor(3, 1);
-
     glGenVertexArrays(1, &m_interiorTrianglesVAO);
     bindVAO(m_interiorTrianglesVAO);
     glEnableVertexAttribArray(0);
@@ -182,6 +184,7 @@
 
 std::unique_ptr<TexelBufferRing> PLSRenderContextGL::makeTexelBufferRing(
     TexelBufferRing::Format format,
+    Renderable,
     size_t widthInItems,
     size_t height,
     size_t texelsPerItem,
@@ -262,11 +265,6 @@
         {
             defines.push_back(GLSL_ENABLE_HSL_BLEND_MODES);
         }
-        if (shaderType == GL_VERTEX_SHADER &&
-            !context->m_extensions.ANGLE_base_vertex_base_instance_shader_builtin)
-        {
-            defines.push_back(GLSL_BASE_INSTANCE_POLYFILL);
-        }
 
         std::vector<const char*> sources;
         sources.push_back(glsl::common);
@@ -291,6 +289,7 @@
                                       defines.size(),
                                       sources.data(),
                                       sources.size(),
+                                      context->m_extensions,
                                       context->m_shaderVersionString);
     }
 
@@ -459,7 +458,8 @@
                 m_plsImpl->ensureRasterOrderingEnabled(true);
                 bindVAO(m_drawVAO);
                 uint32_t indexCount = PatchIndexCount(drawType);
-                void* indexOffset = reinterpret_cast<void*>(PatchIndexOffset(drawType));
+                uint32_t baseIndex = PatchBaseIndex(drawType);
+                void* indexOffset = reinterpret_cast<void*>(baseIndex * sizeof(uint16_t));
                 if (m_extensions.ANGLE_base_vertex_base_instance_shader_builtin)
                 {
                     glDrawElementsInstancedBaseInstanceEXT(GL_TRIANGLES,
@@ -610,6 +610,12 @@
         // varyings.
         platformFeatures.avoidFlatVaryings = true;
     }
+    if (strstr(rendererString, "Direct3D"))
+    {
+        // This extension is polyfilled on D3D anyway. Just don't use it so we can make sure to test
+        // our own fallback.
+        extensions.ANGLE_base_vertex_base_instance_shader_builtin = false;
+    }
 
 #ifdef RIVE_GLES
     loadGLESExtensions(extensions); // Android doesn't load extension functions for us.
@@ -617,7 +623,7 @@
         (extensions.ARM_shader_framebuffer_fetch || extensions.EXT_shader_framebuffer_fetch))
     {
         return std::unique_ptr<PLSRenderContextGL>(
-            new PLSRenderContextGL(platformFeatures, extensions, MakePLSImplEXTNative()));
+            new PLSRenderContextGL(platformFeatures, extensions, MakePLSImplEXTNative(extensions)));
     }
 
     if (extensions.EXT_shader_framebuffer_fetch)
diff --git a/renderer/metal/buffer_ring_metal.h b/renderer/metal/buffer_ring_metal.h
index ac51158..8024f6e 100644
--- a/renderer/metal/buffer_ring_metal.h
+++ b/renderer/metal/buffer_ring_metal.h
@@ -42,7 +42,7 @@
     id<MTLTexture> submittedTexture() const { return m_textures[submittedBufferIdx()]; }
 
 protected:
-    void submitTexels(int bufferIdx, size_t width, size_t height) override;
+    void submitTexels(int textureIdx, size_t updateWidthInTexels, size_t updateHeight) override;
 
 private:
     id<MTLTexture> m_textures[kBufferRingSize];
diff --git a/renderer/metal/pls_render_context_metal.mm b/renderer/metal/pls_render_context_metal.mm
index adccae0..cf48520 100644
--- a/renderer/metal/pls_render_context_metal.mm
+++ b/renderer/metal/pls_render_context_metal.mm
@@ -245,6 +245,7 @@
 
 std::unique_ptr<TexelBufferRing> PLSRenderContextMetal::makeTexelBufferRing(
     TexelBufferRing::Format format,
+    Renderable,
     size_t widthInItems,
     size_t height,
     size_t texelsPerItem,
@@ -448,7 +449,7 @@
                                     indexCount:PatchIndexCount(drawType)
                                      indexType:MTLIndexTypeUInt16
                                    indexBuffer:m_pathPatchIndexBuffer
-                             indexBufferOffset:PatchIndexOffset(drawType)
+                             indexBufferOffset:PatchBaseIndex(drawType) * sizeof(uint16_t)
                                  instanceCount:draw->vertexOrInstanceCount
                                     baseVertex:0
                                   baseInstance:draw->baseVertexOrInstance];
diff --git a/renderer/pls_render_context.cpp b/renderer/pls_render_context.cpp
index 37011fc..8d79dd7 100644
--- a/renderer/pls_render_context.cpp
+++ b/renderer/pls_render_context.cpp
@@ -223,6 +223,7 @@
     {
         assert(!m_pathBuffer.mapped());
         m_pathBuffer.reset(makeTexelBufferRing(TexelBufferRing::Format::rgba32ui,
+                                               Renderable::no,
                                                kPathTextureWidthInItems,
                                                targetPathTextureHeight,
                                                kPathTexelsPerItem,
@@ -250,6 +251,7 @@
     {
         assert(!m_contourBuffer.mapped());
         m_contourBuffer.reset(makeTexelBufferRing(TexelBufferRing::Format::rgba32ui,
+                                                  Renderable::no,
                                                   kContourTextureWidthInItems,
                                                   targetContourTextureHeight,
                                                   kContourTexelsPerItem,
@@ -285,6 +287,7 @@
     {
         assert(!m_gradTexelBuffer.mapped());
         m_gradTexelBuffer.reset(makeTexelBufferRing(TexelBufferRing::Format::rgba8,
+                                                    Renderable::yes,
                                                     kGradTextureWidthInSimpleRamps,
                                                     targetGradTextureHeight,
                                                     2, // 2 texels per simple ramp.
diff --git a/renderer/shaders/advanced_blend.glsl b/renderer/shaders/advanced_blend.glsl
index 57950e3..791c90d 100644
--- a/renderer/shaders/advanced_blend.glsl
+++ b/renderer/shaders/advanced_blend.glsl
@@ -104,7 +104,7 @@
     half lbase = lumv3(cbase);
     half llum = lumv3(clum);
     half ldiff = llum - lbase;
-    half3 color = cbase + make_half3(ldiff);
+    half3 color = cbase + make_half3(ldiff, ldiff, ldiff);
     return clip_color(color);
 }
 
@@ -125,7 +125,7 @@
     }
     else
     {
-        color = make_half3(0);
+        color = make_half3(0, 0, 0);
     }
     return set_lum(color, clum);
 }
@@ -139,7 +139,7 @@
 {
     // The function f() operates on un-multiplied rgb values and dictates the look of the advanced
     // blend equations.
-    half3 f;
+    half3 f = make_half3(0, 0, 0);
     switch (mode)
     {
         case BLEND_MODE_MULTIPLY:
@@ -149,7 +149,8 @@
             f = src.rgb + dst.rgb - src.rgb * dst.rgb;
             break;
         case BLEND_MODE_OVERLAY:
-            for (ushort i = 0u; i < 3u; ++i)
+        {
+            for (int i = 0; i < 3; ++i)
             {
                 if (dst[i] <= .5)
                     f[i] = 2. * src[i] * dst[i];
@@ -157,6 +158,7 @@
                     f[i] = 1. - 2. * (1. - src[i]) * (1. - dst[i]);
             }
             break;
+        }
         case BLEND_MODE_DARKEN:
             f = min(src.rgb, dst.rgb);
             break;
@@ -166,19 +168,20 @@
         case BLEND_MODE_COLORDODGE:
             // ES3 spec, 4.5.1 Range and Precision: dividing a non-zero by 0 results in the
             // appropriately signed IEEE Inf.
-            f = mix(min(dst.rgb / (1. - src.rgb), make_half3(1)),
-                    make_half3(0),
-                    lessThanEqual(dst.rgb, make_half3(0)));
+            f = mix(min(dst.rgb / (1. - src.rgb), make_half3(1, 1, 1)),
+                    make_half3(0, 0, 0),
+                    lessThanEqual(dst.rgb, make_half3(0, 0, 0)));
             break;
         case BLEND_MODE_COLORBURN:
             // ES3 spec, 4.5.1 Range and Precision: dividing a non-zero by 0 results in the
             // appropriately signed IEEE Inf.
             f = mix(1. - min((1. - dst.rgb) / src.rgb, 1.),
-                    make_half3(1),
-                    greaterThanEqual(dst.rgb, make_half3(1)));
+                    make_half3(1, 1, 1),
+                    greaterThanEqual(dst.rgb, make_half3(1, 1, 1)));
             break;
         case BLEND_MODE_HARDLIGHT:
-            for (ushort i = 0u; i < 3u; ++i)
+        {
+            for (int i = 0; i < 3; ++i)
             {
                 if (src[i] <= .5)
                     f[i] = 2. * src[i] * dst[i];
@@ -186,8 +189,10 @@
                     f[i] = 1. - 2. * (1. - src[i]) * (1. - dst[i]);
             }
             break;
+        }
         case BLEND_MODE_SOFTLIGHT:
-            for (ushort i = 0u; i < 3u; ++i)
+        {
+            for (int i = 0; i < 3; ++i)
             {
                 if (src[i] <= 0.5)
                     f[i] = dst[i] - (1. - 2. * src[i]) * dst[i] * (1. - dst[i]);
@@ -198,6 +203,7 @@
                     f[i] = dst[i] + (2. * src[i] - 1.) * (sqrt(dst[i]) - dst[i]);
             }
             break;
+        }
         case BLEND_MODE_DIFFERENCE:
             f = abs(dst.rgb - src.rgb);
             break;
@@ -208,19 +214,19 @@
         // The HSL blend equations are only well defined when the values of the input color
         // components are in the range [0..1].
         case BLEND_MODE_HUE:
-            src.rgb = clamp(src.rgb, make_half3(0), make_half3(1));
+            src.rgb = clamp(src.rgb, make_half3(0, 0, 0), make_half3(1, 1, 1));
             f = set_lum_sat(src.rgb, dst.rgb, dst.rgb);
             break;
         case BLEND_MODE_SATURATION:
-            src.rgb = clamp(src.rgb, make_half3(0), make_half3(1));
+            src.rgb = clamp(src.rgb, make_half3(0, 0, 0), make_half3(1, 1, 1));
             f = set_lum_sat(dst.rgb, src.rgb, dst.rgb);
             break;
         case BLEND_MODE_COLOR:
-            src.rgb = clamp(src.rgb, make_half3(0), make_half3(1));
+            src.rgb = clamp(src.rgb, make_half3(0, 0, 0), make_half3(1, 1, 1));
             f = set_lum(src.rgb, dst.rgb);
             break;
         case BLEND_MODE_LUMINOSITY:
-            src.rgb = clamp(src.rgb, make_half3(0), make_half3(1));
+            src.rgb = clamp(src.rgb, make_half3(0, 0, 0), make_half3(1, 1, 1));
             f = set_lum(dst.rgb, src.rgb);
             break;
 #endif
@@ -243,6 +249,6 @@
     //     A =          X*p0(As,Ad) +     Y*p1(As,Ad) +     Z*p2(As,Ad)
     //
     // NOTE: (X,Y,Z) always == (1,1,1), so it is ignored in this implementation.
-    return make_half3x4(f, 1, src.rgb, 1, dst.rgb, 1) * p;
+    return MUL(make_half3x4(f, 1, src.rgb, 1, dst.rgb, 1), p);
 }
 #endif
diff --git a/renderer/shaders/color_ramp.glsl b/renderer/shaders/color_ramp.glsl
index 383161c..0d26ebf 100644
--- a/renderer/shaders/color_ramp.glsl
+++ b/renderer/shaders/color_ramp.glsl
@@ -5,53 +5,58 @@
 // This shader draws horizontal color ramps into a gradient texture, which will later be sampled by
 // the renderer for drawing gradients.
 
-VARYING_BLOCK_BEGIN(Varyings)
-NO_PERSPECTIVE VARYING half4 rampColor;
-VARYING_BLOCK_END
-
 #ifdef @VERTEX
 ATTR_BLOCK_BEGIN(Attrs)
-ATTR(0) uint4 span; // [horizontalSpan, y, color0, color1]
+ATTR(0, uint4, @a_span); // [spanX, y, color0, color1]
 ATTR_BLOCK_END
+#endif
+
+VARYING_BLOCK_BEGIN(Varyings)
+NO_PERSPECTIVE VARYING(half4, v_rampColor);
+VARYING_BLOCK_END(_pos)
+
+#ifdef @VERTEX
+VERTEX_TEXTURE_BLOCK_BEGIN(VertexTextures)
+VERTEX_TEXTURE_BLOCK_END
 
 half4 unpackColorInt(uint color)
 {
-    return make_half4((uint4(color) >> uint4(16, 8, 0, 24)) & 0xffu) / 255.;
+    return make_half4((uint4(color, color, color, color) >> uint4(16, 8, 0, 24)) & 0xffu) / 255.;
 }
 
-VERTEX_MAIN(
-#ifdef METAL
-    @colorRampVertexMain,
-    Varyings,
-    varyings,
-    uint VERTEX_ID [[vertex_id]],
-    uint INSTANCE_ID [[instance_id]],
-    constant @Uniforms& uniforms [[buffer(0)]],
-    constant Attrs* attrs [[buffer(1)]]
-#endif
-)
+VERTEX_MAIN(@colorRampVertexMain,
+            @Uniforms,
+            uniforms,
+            Attrs,
+            attrs,
+            Varyings,
+            varyings,
+            VertexTextures,
+            textures,
+            _vertexID,
+            _instanceID,
+            _pos)
 {
-    ATTR_LOAD(uint4, attrs, span, INSTANCE_ID);
-    uint horizontalSpan = span.x;
-    float2 coord = float2(
-        float((VERTEX_ID & 1) == 0 ? horizontalSpan & 0xffffu : horizontalSpan >> 16) / 65536.,
-        float(span.y) + ((VERTEX_ID & 2) == 0 ? .0 : 1.));
-    FLD(varyings, rampColor) = unpackColorInt((VERTEX_ID & 1) == 0 ? span.z : span.w);
-    POSITION.xy = coord * float2(2, uniforms.gradInverseViewportY) - 1.;
-    POSITION.zw = float2(0, 1);
-    EMIT_OFFSCREEN_VERTEX(varyings);
+    ATTR_UNPACK(_instanceID, attrs, @a_span, uint4);
+
+    VARYING_INIT(varyings, v_rampColor, half4);
+
+    float x = float((_vertexID & 1) == 0 ? @a_span.x & 0xffffu : @a_span.x >> 16) / 65536.;
+    float y = float(@a_span.y) + ((_vertexID & 2) == 0 ? .0 : 1.);
+    v_rampColor = unpackColorInt((_vertexID & 1) == 0 ? @a_span.z : @a_span.w);
+    _pos.x = x * 2. - 1.;
+    _pos.y = y * uniforms.gradInverseViewportY - 1.;
+    _pos.zw = float2(0, 1);
+
+    VARYING_PACK(varyings, v_rampColor);
+    EMIT_OFFSCREEN_VERTEX(varyings, _pos);
 }
 #endif
 
 #ifdef @FRAGMENT
-FRAG_DATA_TYPE(half4)
-FRAG_DATA_MAIN(
-#ifdef METAL
-    @colorRampFragmentMain,
-    Varyings varyings [[stage_in]]
-#endif
-)
+FRAG_DATA_MAIN(half4, @colorRampFragmentMain, Varyings, varyings)
 {
-    EMIT_FRAG_DATA(FLD(varyings, rampColor));
+    VARYING_UNPACK(varyings, v_rampColor, half4);
+    EMIT_FRAG_DATA(v_rampColor);
 }
 #endif
diff --git a/renderer/shaders/common.glsl b/renderer/shaders/common.glsl
index 77b310d..20f1fe6 100644
--- a/renderer/shaders/common.glsl
+++ b/renderer/shaders/common.glsl
@@ -71,6 +71,8 @@
     return color;
 }
 
+INLINE float2x2 make_float2x2(float4 f) { return float2x2(f.xy, f.zw); }
+
 #ifdef @VERTEX
 UNIFORM_BLOCK_BEGIN(@Uniforms)
 float gradInverseViewportY;
diff --git a/renderer/shaders/draw.glsl b/renderer/shaders/draw.glsl
index b742b84..026be4c 100644
--- a/renderer/shaders/draw.glsl
+++ b/renderer/shaders/draw.glsl
@@ -8,42 +8,38 @@
 #define FAN_VERTEX 1
 #define FAN_MIDPOINT_VERTEX 2
 
-VARYING_BLOCK_BEGIN(Varyings)
-NO_PERSPECTIVE VARYING float4 varying_paint;
-#ifdef @DRAW_INTERIOR_TRIANGLES
-@OPTIONALLY_FLAT VARYING half windingWeight;
-#else
-NO_PERSPECTIVE VARYING half2 edgeDistance;
-#endif
-@OPTIONALLY_FLAT VARYING half pathID;
-#ifdef @ENABLE_PATH_CLIPPING
-@OPTIONALLY_FLAT VARYING half clipID;
-#endif
-#ifdef @ENABLE_ADVANCED_BLEND
-@OPTIONALLY_FLAT VARYING half blendMode;
-#endif
-VARYING_BLOCK_END
-
 #ifdef @VERTEX
-
-// Only used by GL platforms that don't support EXT_base_instance.
-#ifdef @BASE_INSTANCE_POLYFILL
-uniform int @baseInstancePolyfill;
-#endif
-
-VERTEX_TEXTURE_BLOCK_BEGIN(VertexTextures)
-TEXTURE_RGBA32UI(0) @tessVertexTexture;
-TEXTURE_RGBA32UI(1) @pathTexture;
-TEXTURE_RGBA32UI(2) @contourTexture;
-VERTEX_TEXTURE_BLOCK_END
-
 ATTR_BLOCK_BEGIN(Attrs)
 #ifdef @DRAW_INTERIOR_TRIANGLES
-ATTR(0) packed_float3 triangleVertex;
+ATTR(0, packed_float3, @a_triangleVertex);
 #else
-ATTR(0) float4 patchVertexData; // [localVertexID, outset, fillCoverage, vertexType]
+ATTR(0, float4, @a_patchVertexData); // [localVertexID, outset, fillCoverage, vertexType]
 #endif
 ATTR_BLOCK_END
+#endif
+
+VARYING_BLOCK_BEGIN(Varyings)
+NO_PERSPECTIVE VARYING(float4, v_paint);
+#ifdef @DRAW_INTERIOR_TRIANGLES
+@OPTIONALLY_FLAT VARYING(half, v_windingWeight);
+#else
+NO_PERSPECTIVE VARYING(half2, v_edgeDistance);
+#endif
+@OPTIONALLY_FLAT VARYING(half, v_pathID);
+#ifdef @ENABLE_PATH_CLIPPING
+@OPTIONALLY_FLAT VARYING(half, v_clipID);
+#endif
+#ifdef @ENABLE_ADVANCED_BLEND
+@OPTIONALLY_FLAT VARYING(half, v_blendMode);
+#endif
+VARYING_BLOCK_END(_pos)
+
+#ifdef @VERTEX
+VERTEX_TEXTURE_BLOCK_BEGIN(VertexTextures)
+TEXTURE_RGBA32UI(0, @tessVertexTexture);
+TEXTURE_RGBA32UI(1, @pathTexture);
+TEXTURE_RGBA32UI(2, @contourTexture);
+VERTEX_TEXTURE_BLOCK_END
 
 int2 tessTexelCoord(int texelIndex)
 {
@@ -51,47 +47,70 @@
                 texelIndex >> TESS_TEXTURE_WIDTH_LOG2);
 }
 
-float calc_aa_radius(float2x2 matrix, float2 normalized)
+float calc_aa_radius(float2x2 mat, float2 normalized)
 {
 
-    float2 v = matrix * normalized;
+    float2 v = MUL(mat, normalized);
     return (abs(v.x) + abs(v.y)) * (1. / dot(v, v)) * AA_RADIUS;
 }
 
-VERTEX_MAIN(
-#ifdef METAL
-    @drawVertexMain,
-    Varyings,
-    varyings,
-    uint VERTEX_ID [[vertex_id]],
-    uint INSTANCE_ID [[instance_id]],
-    uint BASE_INSTANCE [[base_instance]],
-    constant @Uniforms& uniforms [[buffer(0)]],
-    constant Attrs* attrs [[buffer(1)]],
-    VertexTextures textures
+#ifdef @ENABLE_BASE_INSTANCE_POLYFILL
+// Define a uniform that will supply the base instance if we're on a platform that doesn't provide
+// this as a built-in.
+BASE_INSTANCE_POLYFILL_DECL(@baseInstancePolyfill);
 #endif
-)
+
+VERTEX_MAIN(@drawVertexMain,
+            @Uniforms,
+            uniforms,
+            Attrs,
+            attrs,
+            Varyings,
+            varyings,
+            VertexTextures,
+            textures,
+            _vertexID,
+            _instanceID,
+            _pos)
 {
+#ifdef @DRAW_INTERIOR_TRIANGLES
+    ATTR_UNPACK(_vertexID, attrs, @a_triangleVertex, float3);
+#else
+    ATTR_UNPACK(_vertexID, attrs, @a_patchVertexData, float4);
+#endif
+
+    VARYING_INIT(varyings, v_paint, float4);
+#ifdef @DRAW_INTERIOR_TRIANGLES
+    VARYING_INIT(varyings, v_windingWeight, half);
+#else
+    VARYING_INIT(varyings, v_edgeDistance, half2);
+#endif
+    VARYING_INIT(varyings, v_pathID, half);
+#ifdef @ENABLE_PATH_CLIPPING
+    VARYING_INIT(varyings, v_clipID, half);
+#endif
+#ifdef @ENABLE_ADVANCED_BLEND
+    VARYING_INIT(varyings, v_blendMode, half);
+#endif
+
     bool shouldDiscardVertex = false;
 #ifdef @DRAW_INTERIOR_TRIANGLES
-    ATTR_LOAD(float3, attrs, triangleVertex, VERTEX_ID);
-    uint pathIDBits = floatBitsToUint(triangleVertex.z) & 0xffffu;
+    uint pathIDBits = floatBitsToUint(@a_triangleVertex.z) & 0xffffu;
 #else
     // Unpack patchVertexData.
-    ATTR_LOAD(float4, attrs, patchVertexData, VERTEX_ID);
-    int localVertexID = int(patchVertexData.x);
-    float outset = patchVertexData.y;
-    float fillCoverage = patchVertexData.z;
-    int patchSegmentSpan = floatBitsToInt(patchVertexData.w) >> 2;
-    int vertexType = floatBitsToInt(patchVertexData.w) & 3;
+    int localVertexID = int(@a_patchVertexData.x);
+    float outset = @a_patchVertexData.y;
+    float fillCoverage = @a_patchVertexData.z;
+    int patchSegmentSpan = floatBitsToInt(@a_patchVertexData.w) >> 2;
+    int vertexType = floatBitsToInt(@a_patchVertexData.w) & 3;
 
     // Fetch the tessellation vertex we belong to.
-    int vertexIdx = INSTANCE_ID * patchSegmentSpan + localVertexID;
-#ifdef @BASE_INSTANCE_POLYFILL
-    vertexIdx += baseInstancePolyfill * patchSegmentSpan;
+    int tessVertexIdx = _instanceID * patchSegmentSpan + localVertexID;
+#ifdef @ENABLE_BASE_INSTANCE_POLYFILL
+    tessVertexIdx += @baseInstancePolyfill * patchSegmentSpan;
 #endif
-    int2 tessVertexTexelCoord = tessTexelCoord(vertexIdx);
-    uint4 tessVertexData = TEXEL_FETCH(textures, @tessVertexTexture, tessVertexTexelCoord, 0);
+    int2 tessVertexTexelCoord = tessTexelCoord(tessVertexIdx);
+    uint4 tessVertexData = TEXEL_FETCH(textures, @tessVertexTexture, tessVertexTexelCoord);
     uint contourIDWithFlags = tessVertexData.w;
     bool isClosingVertexOfContour = localVertexID == patchSegmentSpan &&
                                     (contourIDWithFlags & FIRST_VERTEX_OF_CONTOUR_FLAG) != 0u;
@@ -99,14 +118,14 @@
     {
         // The right vertex crossed over into a new contour. Fetch the previous vertex, which will
         // be the final vertex of the contour we're trying to draw.
-        tessVertexTexelCoord = tessTexelCoord(vertexIdx - 1);
-        tessVertexData = TEXEL_FETCH(textures, @tessVertexTexture, tessVertexTexelCoord, 0);
+        tessVertexTexelCoord = tessTexelCoord(tessVertexIdx - 1);
+        tessVertexData = TEXEL_FETCH(textures, @tessVertexTexture, tessVertexTexelCoord);
         contourIDWithFlags = tessVertexData.w;
     }
 
     // Fetch and unpack the contour referenced by the tessellation vertex.
     uint4 contourData =
-        TEXEL_FETCH(textures, @contourTexture, contour_texel_coord(contourIDWithFlags), 0);
+        TEXEL_FETCH(textures, @contourTexture, contour_texel_coord(contourIDWithFlags));
     float2 midpoint = uintBitsToFloat(contourData.xy);
     uint pathIDBits = contourData.z;
     uint vertexIndex0 = contourData.w;
@@ -114,19 +133,18 @@
 
     // Fetch and unpack the path.
     int2 pathTexelCoord = path_texel_coord(pathIDBits);
-    float2x2 matrix =
-        make_float2x2(uintBitsToFloat(TEXEL_FETCH(textures, @pathTexture, pathTexelCoord, 0)));
-    uint4 pathData = TEXEL_FETCH(textures, @pathTexture, pathTexelCoord + int2(1, 0), 0);
+    float2x2 mat =
+        make_float2x2(uintBitsToFloat(TEXEL_FETCH(textures, @pathTexture, pathTexelCoord)));
+    uint4 pathData = TEXEL_FETCH(textures, @pathTexture, pathTexelCoord + int2(1, 0));
     float2 translate = uintBitsToFloat(pathData.xy);
     uint pathParams = pathData.w;
 
 #ifdef @DRAW_INTERIOR_TRIANGLES
     // The vertex position is encoded directly in vertex data when drawing triangles.
-    float2 vertexPosition = matrix * triangleVertex.xy + translate;
+    float2 vertexPosition = MUL(mat, @a_triangleVertex.xy) + translate;
     // When we belong to a non-overlapping interior triangulation, the winding sign and weight are
     // also encoded directly in vertex data.
-    FLD(varyings, windingWeight) =
-        float(floatBitsToInt(triangleVertex.z) >> 16) * sign(determinant(matrix));
+    v_windingWeight = float(floatBitsToInt(@a_triangleVertex.z) >> 16) * sign(determinant(mat));
 #else
     float strokeRadius = uintBitsToFloat(pathData.z);
 
@@ -140,7 +158,7 @@
             // The contour is closed and we are the closing vertex. Wrap back around full circle and
             // re-emit the first tessellation vertex.
             int2 vertexTexelCoord0 = tessTexelCoord(int(vertexIndex0));
-            tessVertexData = TEXEL_FETCH(textures, @tessVertexTexture, vertexTexelCoord0, 0);
+            tessVertexData = TEXEL_FETCH(textures, @tessVertexTexture, vertexTexelCoord0);
             contourIDWithFlags = tessVertexData.w;
         }
     }
@@ -157,24 +175,23 @@
         if ((contourIDWithFlags & RIGHT_JOIN_FLAG) != 0u)
             outset = max(outset, .0);
 
-        float aaRadius = calc_aa_radius(matrix, norm);
-        float globalCoverage = 1.;
+        float aaRadius = calc_aa_radius(mat, norm);
+        half globalCoverage = 1.;
         if (aaRadius > strokeRadius)
         {
-            // The stroke is narrower than the AA ramp. Instead of emitting subpixel geometry,
-            // make the stroke as wide as the AA ramp and apply a global coverage multiplier.
-            globalCoverage = strokeRadius / aaRadius;
+            // The stroke is narrower than the AA ramp. Instead of emitting subpixel geometry, make
+            // the stroke as wide as the AA ramp and apply a global coverage multiplier.
+            globalCoverage = make_half(strokeRadius) / make_half(aaRadius);
             strokeRadius = aaRadius;
         }
 
         // Extend the vertex by half the width of the AA ramp.
-        float2 vertexOffset = norm * (strokeRadius + aaRadius); // Bloat stroke width for AA.
+        float2 vertexOffset = MUL(norm, strokeRadius + aaRadius); // Bloat stroke width for AA.
 
         // Calculate the AA distance to both the outset and inset edges of the stroke. The fragment
         // shader will use whichever is lesser.
         float x = outset * (strokeRadius + aaRadius);
-        FLD(varyings, edgeDistance) =
-            make_half2((1. / (aaRadius * 2.)) * (float2(x, -x) + strokeRadius) + .5);
+        v_edgeDistance = make_half2((1. / (aaRadius * 2.)) * (float2(x, -x) + strokeRadius) + .5);
 
         uint joinType = contourIDWithFlags & JOIN_TYPE_MASK;
         if (joinType != 0u)
@@ -183,8 +200,8 @@
             // the same as the miter line. The first two vertices in the join peek forward to figure
             // out the bisector, and the final two peek backward.
             int peekDir = (contourIDWithFlags & JOIN_TANGENT_0_FLAG) != 0u ? 2 : -2;
-            int2 otherJoinTexelCoord = tessTexelCoord(vertexIdx + peekDir);
-            uint4 otherJoinData = TEXEL_FETCH(textures, @tessVertexTexture, otherJoinTexelCoord, 0);
+            int2 otherJoinTexelCoord = tessTexelCoord(tessVertexIdx + peekDir);
+            uint4 otherJoinData = TEXEL_FETCH(textures, @tessVertexTexture, otherJoinTexelCoord);
             float otherJoinTheta = uintBitsToFloat(otherJoinData.z);
             float joinAngle = abs(otherJoinTheta - theta);
             if (joinAngle > PI)
@@ -193,7 +210,7 @@
             bool isLeftJoin = (contourIDWithFlags & LEFT_JOIN_FLAG) != 0u;
             float bisectTheta = joinAngle * (isTan0 == isLeftJoin ? -.5 : .5) + theta;
             float2 bisector = float2(sin(bisectTheta), -cos(bisectTheta));
-            float bisectAARadius = calc_aa_radius(matrix, bisector);
+            float bisectAARadius = calc_aa_radius(mat, bisector);
 
             // Generalize everything to a "miter-clip", which is proposed in the SVG-2 draft. Bevel
             // joins are converted to miter-clip joins with a miter limit of 1/2 pixel. They
@@ -244,7 +261,7 @@
                     float2 bisectAAOffset = bisector * clipAARadius;
                     float2 k = float2(dot(vertexOffset, vertexOffset),
                                       dot(bisectAAOffset, bisectAAOffset));
-                    vertexOffset = k * inverse(float2x2(vertexOffset, bisectAAOffset));
+                    vertexOffset = MUL(k, inverse(float2x2(vertexOffset, bisectAAOffset)));
                 }
             }
             // The clip distance tells us how to antialias the outer clipped edge. Since joins only
@@ -253,15 +270,15 @@
             float2 pt = abs(outset) * vertexOffset;
             float clipDistance = (clipAARadius - dot(pt, bisector)) / (bisectAARadius * 2.);
             if ((contourIDWithFlags & LEFT_JOIN_FLAG) != 0u)
-                FLD(varyings, edgeDistance).y = clipDistance;
+                v_edgeDistance.y = make_half(clipDistance);
             else
-                FLD(varyings, edgeDistance).x = clipDistance;
+                v_edgeDistance.x = make_half(clipDistance);
         }
 
         // Strokes identify themselves by emitting a negative edgeDistance.
-        FLD(varyings, edgeDistance) *= -globalCoverage;
+        v_edgeDistance *= -globalCoverage;
 
-        postTransformVertexOffset = matrix * (outset * vertexOffset);
+        postTransformVertexOffset = MUL(mat, outset * vertexOffset);
 
         // Throw away the fan triangles since we're a stroke.
         if (vertexType != STROKE_VERTEX)
@@ -274,43 +291,41 @@
             origin = midpoint;
 
         // Offset the vertex for Manhattan AA.
-        postTransformVertexOffset = sign(matrix * (outset * norm)) * AA_RADIUS;
-        FLD(varyings, edgeDistance) = make_half2(fillCoverage, 1);
+        postTransformVertexOffset = sign(MUL(mat, outset * norm)) * AA_RADIUS;
+        v_edgeDistance = make_half2(fillCoverage, 1);
 
         // If we're actually just drawing a triangle, throw away the entire patch except a single
         // fan triangle.
         if ((contourIDWithFlags & RETROFITTED_TRIANGLE_FLAG) != 0u && vertexType != FAN_VERTEX)
             shouldDiscardVertex = true;
     }
-    float2 vertexPosition = matrix * origin + postTransformVertexOffset + translate;
+    float2 vertexPosition = MUL(mat, origin) + postTransformVertexOffset + translate;
 #endif
 
     // Encode the integral pathID as a "half" that we know the hardware will see as a unique value
     // in the fragment shader.
-    FLD(varyings, pathID) =
-        unpackHalf2x16((pathIDBits + MAX_DENORM_F16) * uniforms.pathIDGranularity).r;
+    v_pathID = unpackHalf2x16((pathIDBits + MAX_DENORM_F16) * uniforms.pathIDGranularity).r;
 
     // Indicate even-odd fill rule by making pathID negative.
     if ((pathParams & EVEN_ODD_FLAG) != 0u)
-        FLD(varyings, pathID) = -FLD(varyings, pathID);
+        v_pathID = -v_pathID;
 
     uint paintType = (pathParams >> 20) & 7u;
 #ifdef @ENABLE_PATH_CLIPPING
     uint clipIDBits = (pathParams >> 4) & 0xffffu;
-    FLD(varyings, clipID) =
-        clipIDBits == 0u
-            ? .0
-            : unpackHalf2x16((clipIDBits + MAX_DENORM_F16) * uniforms.pathIDGranularity).r;
+    v_clipID = clipIDBits == 0u
+                   ? .0
+                   : unpackHalf2x16((clipIDBits + MAX_DENORM_F16) * uniforms.pathIDGranularity).r;
     // Negative clipID means to repalce the clip with this clipID.
     if (paintType == CLIP_REPLACE_PAINT_TYPE)
-        FLD(varyings, clipID) = -FLD(varyings, clipID);
+        v_clipID = -v_clipID;
 #endif
 #ifdef @ENABLE_ADVANCED_BLEND
-    FLD(varyings, blendMode) = float(pathParams & 0xfu);
+    v_blendMode = float(pathParams & 0xfu);
 #endif
 
     // Unpack the paint once we have a position.
-    uint4 paintData = TEXEL_FETCH(textures, @pathTexture, pathTexelCoord + int2(2, 0), 0);
+    uint4 paintData = TEXEL_FETCH(textures, @pathTexture, pathTexelCoord + int2(2, 0));
     float4 paint;
     if (paintType != LINEAR_GRADIENT_PAINT_TYPE && paintType != RADIAL_GRADIENT_PAINT_TYPE)
     {
@@ -334,7 +349,7 @@
         paint.b = x0 * (1. / GRAD_TEXTURE_WIDTH) + (.5 / GRAD_TEXTURE_WIDTH);
         if (x1 > x0 + 1.)
             ++paint.b; // x1 must equal GRAD_TEXTURE_WIDTH - 1. Increment paint.b to convey this.
-        float2 localCoord = inverse(matrix) * (vertexPosition - translate);
+        float2 localCoord = MUL(inverse(mat), vertexPosition - translate);
         float3 gradCoeffs = uintBitsToFloat(paintData.yzw);
         if (paintType == LINEAR_GRADIENT_PAINT_TYPE)
         {
@@ -351,40 +366,68 @@
             paint.rg = (localCoord - gradCoeffs.xy) / gradCoeffs.z;
         }
     }
-    FLD(varyings, varying_paint) = paint;
+    v_paint = paint;
 
-    POSITION.xy = vertexPosition * float2(uniforms.renderTargetInverseViewportX,
-                                          -uniforms.renderTargetInverseViewportY) +
-                  float2(-1, 1);
-    POSITION.zw = float2(0, 1);
+    _pos.xy = vertexPosition * float2(uniforms.renderTargetInverseViewportX,
+                                      -uniforms.renderTargetInverseViewportY) +
+              float2(-1, 1);
+    _pos.zw = float2(0, 1);
     if (shouldDiscardVertex)
-        POSITION = float4(uniforms.vertexDiscardValue);
-    EMIT_VERTEX(varyings);
+    {
+        _pos = float4(uniforms.vertexDiscardValue,
+                      uniforms.vertexDiscardValue,
+                      uniforms.vertexDiscardValue,
+                      uniforms.vertexDiscardValue);
+    }
+
+    VARYING_PACK(varyings, v_paint);
+#ifdef @DRAW_INTERIOR_TRIANGLES
+    VARYING_PACK(varyings, v_windingWeight);
+#else
+    VARYING_PACK(varyings, v_edgeDistance);
+#endif
+    VARYING_PACK(varyings, v_pathID);
+#ifdef @ENABLE_PATH_CLIPPING
+    VARYING_PACK(varyings, v_clipID);
+#endif
+#ifdef @ENABLE_ADVANCED_BLEND
+    VARYING_PACK(varyings, v_blendMode);
+#endif
+    EMIT_VERTEX(varyings, _pos);
 }
 #endif
 
 #ifdef @FRAGMENT
 FRAG_TEXTURE_BLOCK_BEGIN(FragmentTextures)
-TEXTURE_RGBA8(3) @gradTexture;
+TEXTURE_RGBA8(3, @gradTexture);
 FRAG_TEXTURE_BLOCK_END
 
+GRADIENT_SAMPLER_DECL(0, gradSampler)
+
 PLS_BLOCK_BEGIN
-PLS_DECL4F(0) framebuffer;
-PLS_DECL2F(1) coverageCountBuffer;
-PLS_DECL4F(2) originalDstColorBuffer;
-PLS_DECL2F(3) clipBuffer;
+PLS_DECL4F(0, framebuffer);
+PLS_DECL2F(1, coverageCountBuffer);
+PLS_DECL4F(2, originalDstColorBuffer);
+PLS_DECL2F(3, clipBuffer);
 PLS_BLOCK_END
 
-#ifdef METAL
-PLS_MAIN(@drawFragmentMain,
-         Varyings varyings [[stage_in]],
-         FragmentTextures textures,
-         bool FRONT_FACING [[front_facing]])
-#else
-PLS_MAIN()
-#endif
+PLS_MAIN(@drawFragmentMain, Varyings, varyings, FragmentTextures, textures, _pos, _clockwise)
 {
-    float4 paint = FLD(varyings, varying_paint);
+    VARYING_UNPACK(varyings, v_paint, float4);
+#ifdef @DRAW_INTERIOR_TRIANGLES
+    VARYING_UNPACK(varyings, v_windingWeight, half);
+#else
+    VARYING_UNPACK(varyings, v_edgeDistance, half2);
+#endif
+    VARYING_UNPACK(varyings, v_pathID, half);
+#ifdef @ENABLE_PATH_CLIPPING
+    VARYING_UNPACK(varyings, v_clipID, half);
+#endif
+#ifdef @ENABLE_ADVANCED_BLEND
+    VARYING_UNPACK(varyings, v_blendMode, half);
+#endif
+
+    float4 paint = v_paint;
     half4 color;
     if (paint.a >= .0)
     {
@@ -403,7 +446,8 @@
                 ? ((GRAD_TEXTURE_WIDTH - .5) / GRAD_TEXTURE_WIDTH) // The ramp spans an entire row.
                 : x0 + (1. / GRAD_TEXTURE_WIDTH);                  // The ramp spans 2 texels.
         float row = -paint.a;
-        color = TEXTURE_SAMPLE(textures, @gradTexture, float2(mix(x0, x1, t), row));
+        float2 gradCoord = float2(mix(x0, x1, t), row);
+        color = make_half4(TEXTURE_SAMPLE(textures, @gradTexture, gradSampler, gradCoord));
     }
 
 #ifndef @DRAW_INTERIOR_TRIANGLES
@@ -416,7 +460,7 @@
     half coverageCount = coverageData.g;
 
     half4 dstColor;
-    if (localPathID != FLD(varyings, pathID))
+    if (localPathID != v_pathID)
     {
         // This is the first fragment from pathID to touch this pixel.
         coverageCount = .0;
@@ -437,34 +481,34 @@
     }
 
 #ifdef @DRAW_INTERIOR_TRIANGLES
-    coverageCount += FLD(varyings, windingWeight);
+    coverageCount += v_windingWeight;
 #else
     // TODO: We may need to just send actual flags instead of using sign(edgeDistance) to identify
     // strokes. Since edgeDistance is interpolated, it can sometimes cross signs.
-    half d = FLD(varyings, edgeDistance).x;
+    half d = v_edgeDistance.x;
     if (d < -1e-4 /*stroke with an intentionally negative edgeDistance*/)
-        coverageCount = min(max(d, FLD(varyings, edgeDistance).y), coverageCount);
-    else if (FRONT_FACING /*clockwise fill*/)
+        coverageCount = min(max(d, v_edgeDistance.y), coverageCount);
+    else if (_clockwise /*clockwise fill*/)
         coverageCount += d;
     else /*counterclockwise fill*/
         coverageCount -= d;
 
     // Save the updated coverage.
-    PLS_STORE2F(coverageCountBuffer, FLD(varyings, pathID), coverageCount);
+    PLS_STORE2F(coverageCountBuffer, v_pathID, coverageCount);
 #endif
 
     // Convert coverageCount to coverage. (Which is min(-edgeDistance) right now for strokes.)
     half coverage = abs(coverageCount);
 #ifdef @ENABLE_EVEN_ODD
-    if (FLD(varyings, pathID) < .0 /*even-odd*/)
+    if (v_pathID < .0 /*even-odd*/)
         coverage = 1. - abs(fract(coverage * .5) * 2. + -1.);
 #endif
     coverage = min(coverage, make_half(1.)); // This also caps stroke coverage, which can be >1.
 
 #ifdef @ENABLE_PATH_CLIPPING
-    if (FLD(varyings, clipID) < .0 /*replace clip*/)
+    if (v_clipID < .0 /*replace clip*/)
     {
-        PLS_STORE2F(clipBuffer, -FLD(varyings, clipID), coverage);
+        PLS_STORE2F(clipBuffer, -v_clipID, coverage);
         PLS_PRESERVE_VALUE(framebuffer);
     }
     else
@@ -472,10 +516,10 @@
     {
 #ifdef @ENABLE_PATH_CLIPPING
         // Apply the clip.
-        if (FLD(varyings, clipID) > .0)
+        if (v_clipID > .0)
         {
             half2 clipData = PLS_LOAD2F(clipBuffer);
-            coverage = FLD(varyings, clipID) == clipData.r ? min(coverage, clipData.g) : .0;
+            coverage = v_clipID == clipData.r ? min(coverage, clipData.g) : .0;
         }
 #endif
         PLS_PRESERVE_VALUE(clipBuffer);
@@ -483,7 +527,7 @@
         // Blend with the framebuffer color.
         color.a *= coverage;
 #ifdef @ENABLE_ADVANCED_BLEND
-        if (FLD(varyings, blendMode) != .0 /*srcOver*/)
+        if (v_blendMode != .0 /*srcOver*/)
         {
 #ifdef @ENABLE_HSL_BLEND_MODES
             color = advanced_hsl_blend(
@@ -492,7 +536,7 @@
 #endif
                 color,
                 unmultiply(dstColor),
-                make_ushort(FLD(varyings, blendMode)));
+                make_ushort(v_blendMode));
         }
         else
 #endif
diff --git a/renderer/shaders/glsl.glsl b/renderer/shaders/glsl.glsl
index 5b6d377..c1254a0 100644
--- a/renderer/shaders/glsl.glsl
+++ b/renderer/shaders/glsl.glsl
@@ -5,6 +5,8 @@
 // This header provides GLSL-specific #defines and declarations that enable our shaders to be
 // compiled on MSL and GLSL both.
 
+#define GLSL
+
 #define float2 vec2
 #define float3 vec3
 #define packed_float3 vec3
@@ -46,15 +48,9 @@
 #define make_ushort4 uvec4
 
 #define float2x2 mat2
-#define float4x2 mat4x2
-#define make_float2x2 mat2
-#define make_float4x2 mat4x2
 #define make_half3x4 mat3x4
 
 #define INLINE
-#define FRONT_FACING gl_FrontFacing
-#define POSITION gl_Position
-#define VERTEX_ID gl_VertexID
 
 #if defined(GL_ANGLE_base_vertex_base_instance_shader_builtin)
 #extension GL_ANGLE_base_vertex_base_instance_shader_builtin : require
@@ -70,23 +66,24 @@
     layout(std140) uniform N                                                                       \
     {
 // clang-format barrier... Otherwise it tries to merge this #define into the above macro...
-#define UNIFORM_BLOCK_END(N)                                                                       \
+#define UNIFORM_BLOCK_END(NAME)                                                                    \
     }                                                                                              \
-    N;
+    NAME;
 
-#define ATTR_BLOCK_BEGIN(N)
-#define ATTR(L) layout(location = L) in
+#define ATTR_BLOCK_BEGIN(NAME)
+#define ATTR(IDX, TYPE, NAME) layout(location = IDX) in TYPE NAME
 #define ATTR_BLOCK_END
 #define ATTR_LOAD(A, B, C, D)
+#define ATTR_UNPACK(ID, attrs, NAME, TYPE)
 
-#define VARYING_BLOCK_BEGIN(N)
 #ifdef @VERTEX
-#define VARYING out
+#define VARYING(TYPE, NAME) out TYPE NAME
 #else
-#define VARYING in
+#define VARYING(TYPE, NAME) in TYPE NAME
 #endif
 #define FLAT flat
-#define VARYING_BLOCK_END
+#define VARYING_BLOCK_BEGIN(NAME)
+#define VARYING_BLOCK_END(_pos)
 
 #if defined(GL_NV_shader_noperspective_interpolation)
 #extension GL_NV_shader_noperspective_interpolation : require
@@ -96,21 +93,22 @@
 #endif
 
 #ifdef @VERTEX
-#define VERTEX_TEXTURE_BLOCK_BEGIN(N)
+#define VERTEX_TEXTURE_BLOCK_BEGIN(NAME)
 #define VERTEX_TEXTURE_BLOCK_END
 #endif
 
 #ifdef @FRAGMENT
-#define FRAG_TEXTURE_BLOCK_BEGIN(N)
+#define FRAG_TEXTURE_BLOCK_BEGIN(NAME)
 #define FRAG_TEXTURE_BLOCK_END
 #endif
 
-#define TEXTURE_RGBA32UI(B) uniform highp usampler2D
-#define TEXTURE_RGBA32F(B) uniform highp sampler2D
-#define TEXTURE_RGBA8(B) uniform mediump sampler2D
+#define TEXTURE_RGBA32UI(IDX, NAME) uniform highp usampler2D NAME
+#define TEXTURE_RGBA32F(IDX, NAME) uniform highp sampler2D NAME
+#define TEXTURE_RGBA8(IDX, NAME) uniform mediump sampler2D NAME
 
-#define TEXEL_FETCH(T, N, C, L) texelFetch(N, C, L)
-#define TEXTURE_SAMPLE(T, N, C) texture(N, C)
+#define TEXEL_FETCH(TEXTURE_BLOCK, NAME, COORD) texelFetch(NAME, COORD, 0)
+#define TEXTURE_SAMPLE(TEXTURE_BLOCK, NAME, SAMPLER_NAME, COORD) texture(NAME, COORD)
+#define GRADIENT_SAMPLER_DECL(IDX, NAME)
 
 // Define macros for implementing pixel local storage based on available extensions.
 #ifdef @PLS_IMPL_WEBGL
@@ -118,8 +116,8 @@
 #extension GL_ANGLE_shader_pixel_local_storage : require
 
 #define PLS_BLOCK_BEGIN
-#define PLS_DECL4F(B) layout(binding = B, rgba8) uniform lowp pixelLocalANGLE
-#define PLS_DECL2F(B) layout(binding = B, r32ui) uniform highp upixelLocalANGLE
+#define PLS_DECL4F(IDX, NAME) layout(binding = IDX, rgba8) uniform lowp pixelLocalANGLE NAME
+#define PLS_DECL2F(IDX, NAME) layout(binding = IDX, r32ui) uniform highp upixelLocalANGLE NAME
 #define PLS_BLOCK_END
 
 #define PLS_LOAD4F(P) pixelLocalLoadANGLE(P)
@@ -145,8 +143,8 @@
 #define PLS_BLOCK_BEGIN                                                                            \
     __pixel_localEXT PLS                                                                           \
     {
-#define PLS_DECL4F(B) layout(rgba8) lowp vec4
-#define PLS_DECL2F(B) layout(rg16f) mediump vec2
+#define PLS_DECL4F(IDX, NAME) layout(rgba8) lowp vec4 NAME
+#define PLS_DECL2F(IDX, NAME) layout(rg16f) mediump vec2 NAME
 #define PLS_BLOCK_END                                                                              \
     }                                                                                              \
     ;
@@ -168,8 +166,8 @@
 #extension GL_EXT_shader_framebuffer_fetch : require
 
 #define PLS_BLOCK_BEGIN
-#define PLS_DECL4F(B) layout(location = B) inout lowp vec4
-#define PLS_DECL2F(B) layout(location = B) inout highp uvec4
+#define PLS_DECL4F(IDX, NAME) layout(location = IDX) inout lowp vec4 NAME
+#define PLS_DECL2F(IDX, NAME) layout(location = IDX) inout highp uvec4 NAME
 #define PLS_BLOCK_END
 
 #define PLS_LOAD4F(P) P
@@ -199,8 +197,8 @@
 #endif
 
 #define PLS_BLOCK_BEGIN
-#define PLS_DECL4F(B) layout(binding = B, rgba8) uniform lowp coherent image2D
-#define PLS_DECL2F(B) layout(binding = B, r32ui) uniform highp coherent uimage2D
+#define PLS_DECL4F(IDX, NAME) layout(binding = IDX, rgba8) uniform lowp coherent image2D NAME
+#define PLS_DECL2F(IDX, NAME) layout(binding = IDX, r32ui) uniform highp coherent uimage2D NAME
 #define PLS_BLOCK_END
 
 #define PLS_LOAD4F(P) imageLoad(P, plsCoord)
@@ -212,29 +210,66 @@
 
 #endif
 
-#define VERTEX_MAIN void main
-#define EMIT_VERTEX(v)
-#define EMIT_OFFSCREEN_VERTEX(v)
-
-#define FRAG_DATA_TYPE(T) out T _fd;
-#define FRAG_DATA_MAIN void main
-#define EMIT_FRAG_DATA(f) _fd = f
-
-#ifdef @PLS_IMPL_RW_TEXTURE
-#define PLS_MAIN()                                                                                 \
+#ifdef @ENABLE_BASE_INSTANCE_POLYFILL
+#define BASE_INSTANCE_POLYFILL_DECL(NAME) uniform int NAME
+// The Qualcomm compiler doesn't like how clang-format handles these lines.
+// clang-format off
+#define VERTEX_MAIN(NAME, Uniforms, uniforms, Attrs, attrs, Varyings, varyings, VertexTextures, textures, _vertexID, _instanceID, _pos) \
     void main()                                                                                    \
     {                                                                                              \
-        highp ivec2 plsCoord = ivec2(floor(gl_FragCoord.xy));
-#define EMIT_PLS }
+        int _vertexID = gl_VertexID;                                                               \
+        int _instanceID = gl_InstanceID;                                                           \
+        vec4 _pos;
+// clang-format on
 #else
-#define PLS_MAIN void main
-#define EMIT_PLS
+#ifdef GL_ANGLE_base_vertex_base_instance_shader_builtin
+#extension GL_ANGLE_base_vertex_base_instance_shader_builtin : require
+#endif
+// The Qualcomm compiler doesn't like how clang-format handles these lines.
+// clang-format off
+#define VERTEX_MAIN(NAME, Uniforms, uniforms, Attrs, attrs, Varyings, varyings, VertexTextures, textures, _vertexID, _instanceID, _pos) \
+    void main()                                                                                    \
+    {                                                                                              \
+        int _vertexID = gl_VertexID;                                                               \
+        int _instanceID = gl_BaseInstance + gl_InstanceID;                                         \
+        vec4 _pos;
+// clang-format on
 #endif
 
-// "FLD" for "field".
-// In Metal, inputs and outputs are accessed from structs.
-// In GLSL 300 es, they have to be global.
-#define FLD(BLOCK, MEMBER) MEMBER
+#define VARYING_INIT(varyings, NAME, TYPE)
+#define VARYING_PACK(varyings, NAME)
+#define VARYING_UNPACK(varyings, NAME, TYPE)
+
+#define EMIT_VERTEX(varyings, _pos)                                                                \
+    }                                                                                              \
+    gl_Position = _pos;
+
+#define EMIT_OFFSCREEN_VERTEX(varyings, _pos)                                                      \
+    }                                                                                              \
+    gl_Position = _pos;
+
+#define FRAG_DATA_MAIN(DATA_TYPE, NAME, Varyings, varyings)                                        \
+    out DATA_TYPE _fd;                                                                             \
+    void main()
+
+#define EMIT_FRAG_DATA(VALUE) _fd = VALUE
+
+#ifdef @PLS_IMPL_RW_TEXTURE
+#define PLS_MAIN(NAME, Varyings, varyings, FragmentTextures, textures, _pos, _clockwise)           \
+    void main()                                                                                    \
+    {                                                                                              \
+        bool _clockwise = gl_FrontFacing;                                                          \
+        highp ivec2 plsCoord = ivec2(floor(gl_FragCoord.xy));
+#else
+#define PLS_MAIN(NAME, Varyings, varyings, FragmentTextures, textures, _pos, _clockwise)           \
+    void main()                                                                                    \
+    {                                                                                              \
+        bool _clockwise = gl_FrontFacing;
+#endif
+
+#define EMIT_PLS }
+
+#define MUL(A, B) ((A) * (B))
 
 precision highp float;
 precision highp int;
diff --git a/renderer/shaders/hlsl.glsl b/renderer/shaders/hlsl.glsl
new file mode 100644
index 0000000..99003c9
--- /dev/null
+++ b/renderer/shaders/hlsl.glsl
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2023 Rive
+ */
+
+// This header provides GLSL-specific #defines and declarations that enable our shaders to be
+// compiled on MSL and GLSL both.
+
+// HLSL warns that it will unroll the loops through r,g,b values in advanced_blend.glsl, but
+// unrolling these loops is exactly what we want.
+#pragma warning(disable : 3550)
+
+// #define native hlsl types if their names are being rewritten.
+#define _ARE_TOKEN_NAMES_PRESERVED
+#ifndef $_ARE_TOKEN_NAMES_PRESERVED
+#define half half
+#define half2 half2
+#define half3 half3
+#define half4 half4
+#define short short
+#define short2 short2
+#define short3 short3
+#define short4 short4
+#define ushort ushort
+#define ushort2 ushort2
+#define ushort3 ushort3
+#define ushort4 ushort4
+#define float2 float2
+#define float3 float3
+#define float4 float4
+#define bool2 bool2
+#define bool3 bool3
+#define bool4 bool4
+#define uint2 uint2
+#define uint3 uint3
+#define uint4 uint4
+#define int2 int2
+#define int3 int3
+#define int4 int4
+#define float4x2 float4x2
+#define ushort ushort
+#define float2x2 float2x2
+#define half3x4 half3x4
+#endif
+
+typedef float3 packed_float3;
+#define make_half float
+#define make_half2 float2
+#define make_half3 float3
+#define make_half4 float4
+
+typedef min16int short;
+#define make_short min16int
+#define make_short2 min16int2
+#define make_short3 min16int3
+#define make_short4 min16int4
+
+typedef min16uint ushort;
+#define make_ushort min16uint
+#define make_ushort2 min16uint2
+#define make_ushort3 min16uint3
+#define make_ushort4 min16uint4
+
+#define make_half3x4 float3x4
+
+#define ATTR_BLOCK_BEGIN(NAME)                                                                     \
+    struct NAME                                                                                    \
+    {
+#define ATTR(IDX, TYPE, NAME) TYPE NAME : NAME
+#define ATTR_BLOCK_END                                                                             \
+    }                                                                                              \
+    ;
+#define ATTR_LOAD(T, A, N, I)
+#define ATTR_UNPACK(ID, attrs, NAME, TYPE) TYPE NAME = attrs.NAME
+
+#define UNIFORM_BLOCK_BEGIN(NAME)                                                                  \
+    cbuffer NAME : register(b0)                                                                    \
+    {                                                                                              \
+        struct                                                                                     \
+        {
+
+#define UNIFORM_BLOCK_END(NAME)                                                                    \
+    }                                                                                              \
+    NAME;                                                                                          \
+    }
+
+#define VARYING_BLOCK_BEGIN(NAME)                                                                  \
+    struct NAME                                                                                    \
+    {
+
+#define NO_PERSPECTIVE noperspective
+#define @OPTIONALLY_FLAT nointerpolation
+#define FLAT nointerpolation
+#define VARYING(TYPE, NAME) TYPE NAME : NAME
+
+#define VARYING_BLOCK_END(_pos)                                                                    \
+    float4 _pos : SV_Position;                                                                     \
+    }                                                                                              \
+    ;
+
+#define VARYING_INIT(varyings, NAME, TYPE) TYPE NAME
+#define VARYING_PACK(varyings, NAME) varyings.NAME = NAME
+#define VARYING_UNPACK(varyings, NAME, TYPE) TYPE NAME = varyings.NAME
+
+#ifdef @VERTEX
+#define VERTEX_TEXTURE_BLOCK_BEGIN(NAME)
+#define VERTEX_TEXTURE_BLOCK_END
+#endif
+
+#ifdef @FRAGMENT
+#define FRAG_TEXTURE_BLOCK_BEGIN(NAME)
+#define FRAG_TEXTURE_BLOCK_END
+#endif
+
+#define TEXTURE_RGBA32UI(IDX, NAME) uniform Texture2D<uint4> NAME : register(t##IDX)
+#define TEXTURE_RGBA32F(IDX, NAME) uniform Texture2D<float4> NAME : register(t##IDX)
+#define TEXTURE_RGBA8(IDX, NAME) uniform Texture2D<unorm float4> NAME : register(t##IDX)
+
+#define TEXEL_FETCH(TEXTURE_BLOCK, NAME, COORD) NAME[COORD]
+
+#define GRADIENT_SAMPLER_DECL(IDX, NAME) SamplerState NAME : register(s##IDX);
+
+#define TEXTURE_SAMPLE(TEXTURE_BLOCK, NAME, SAMPLER_NAME, COORD) NAME.Sample(SAMPLER_NAME, COORD)
+
+#define PLS_INTERLOCK_BEGIN
+#define PLS_INTERLOCK_END
+
+#ifdef @DRAW_INTERIOR_TRIANGLES
+// The interior triangles don't overlap, so therefore don't need rasterizer ordering.
+#define $PLS_TEX2D RWTexture2D
+#else
+#define $PLS_TEX2D RasterizerOrderedTexture2D
+#endif
+
+#define PLS_BLOCK_BEGIN
+#define PLS_DECL4F(IDX, NAME) uniform PLS_TEX2D<unorm float4> NAME : register(u##IDX)
+#define PLS_DECL2F(IDX, NAME) uniform PLS_TEX2D<uint> NAME : register(u##IDX)
+#define PLS_BLOCK_END
+
+#define PLS_LOAD4F(P) P[_plsCoord]
+#define PLS_LOAD2F(P) unpackHalf2x16(P[_plsCoord])
+#define PLS_STORE4F(P, V) P[_plsCoord] = V
+#define PLS_STORE2F(P, X, Y) P[_plsCoord] = packHalf2x16(float2(X, Y))
+
+#define PLS_PRESERVE_VALUE(P)
+
+#ifdef @ENABLE_BASE_INSTANCE_POLYFILL
+#define BASE_INSTANCE_POLYFILL_DECL(NAME)                                                          \
+    cbuffer NAME##_cbuff : register(b1)                                                            \
+    {                                                                                              \
+        uint NAME;                                                                                 \
+        uint NAME##_pad0;                                                                          \
+        uint NAME##_pad1;                                                                          \
+        uint NAME##_pad2;                                                                          \
+    }
+
+#endif
+
+#define VERTEX_MAIN(NAME,                                                                          \
+                    Uniforms,                                                                      \
+                    uniforms,                                                                      \
+                    Attrs,                                                                         \
+                    attrs,                                                                         \
+                    Varyings,                                                                      \
+                    varyings,                                                                      \
+                    VertexTextures,                                                                \
+                    textures,                                                                      \
+                    _vertexID,                                                                     \
+                    _instanceID,                                                                   \
+                    _pos)                                                                          \
+    Varyings NAME(Attrs attrs, uint _vertexID : SV_VertexID, uint _instanceID : SV_InstanceID)     \
+    {                                                                                              \
+        Varyings varyings;                                                                         \
+        float4 _pos;
+
+#define EMIT_VERTEX(varyings, _pos)                                                                \
+    }                                                                                              \
+    varyings._pos = _pos;                                                                          \
+    return varyings;
+
+#define EMIT_OFFSCREEN_VERTEX(varyings, _pos)                                                      \
+    varyings._pos.xzw = _pos.xzw;                                                                  \
+    varyings._pos.y = -_pos.y;                                                                     \
+    return varyings;                                                                               \
+    }
+
+#define FRAG_DATA_MAIN(DATA_TYPE, NAME, Varyings, varyings)                                        \
+    DATA_TYPE NAME(Varyings varyings) : SV_Target                                                  \
+    {
+
+#define EMIT_FRAG_DATA(VALUE)                                                                      \
+    return VALUE;                                                                                  \
+    }
+
+#define PLS_MAIN(NAME, Varyings, varyings, FragmentTextures, textures, _pos, _clockwise)           \
+    [earlydepthstencil] void NAME(Varyings varyings, bool _clockwise : SV_IsFrontFace) {           \
+        int2 _plsCoord = int2(floor(varyings._pos.xy));
+
+#define EMIT_PLS }
+
+#define INLINE inline
+
+#define uintBitsToFloat asfloat
+#define intBitsToFloat asfloat
+#define floatBitsToInt asint
+#define floatBitsToUint asuint
+#define fract frac
+#define mix lerp
+#define inversesqrt rsqrt
+#define notEqual(A, B) ((A) != (B))
+#define lessThanEqual(A, B) ((A) <= (B))
+#define greaterThanEqual(A, B) ((A) >= (B))
+
+// HLSL matrices are stored in row-major order, and therefore transposed from their counterparts
+// in GLSL and Metal. We can work around this entirely by reversing the arguments to mul().
+#define MUL(A, B) mul(B, A)
+
+inline half2 unpackHalf2x16(uint u)
+{
+    uint y = (u >> 16);
+    uint x = u & 0xffffu;
+    return make_half2(f16tof32(x), f16tof32(y));
+}
+
+inline uint packHalf2x16(float2 v)
+{
+    uint x = f32tof16(v.x);
+    uint y = f32tof16(v.y);
+    return (y << 16) | x;
+}
+
+inline float atan(float y, float x) { return atan2(y, x); }
+
+inline float2x2 inverse(float2x2 m)
+{
+    float2x2 adjoint = float2x2(m[1][1], -m[0][1], -m[1][0], m[0][0]);
+    return adjoint * (1. / determinant(m));
+}
diff --git a/renderer/shaders/metal.glsl b/renderer/shaders/metal.glsl
index 1cd6abb..8a147ea 100644
--- a/renderer/shaders/metal.glsl
+++ b/renderer/shaders/metal.glsl
@@ -7,7 +7,9 @@
 
 #define METAL
 
-// Native metal types need a #define. They get rewritten because they are not GLSL keywords.
+// #define native metal types if their names are being rewritten.
+#define _ARE_TOKEN_NAMES_PRESERVED
+#ifndef $_ARE_TOKEN_NAMES_PRESERVED
 #define half half
 #define half2 half2
 #define half3 half3
@@ -37,6 +39,7 @@
 #define ushort ushort
 #define float2x2 float2x2
 #define half3x4 half3x4
+#endif
 
 #define make_half4 half4
 #define make_half3 half3
@@ -50,6 +53,7 @@
 #define notEqual(A, B) ((A) != (B))
 #define lessThanEqual(A, B) ((A) <= (B))
 #define greaterThanEqual(A, B) ((A) >= (B))
+#define MUL(A, B) ((A) * (B))
 #define atan atan2
 #define inversesqrt rsqrt
 
@@ -63,27 +67,31 @@
 #define ATTR_BLOCK_BEGIN(N)                                                                        \
     struct N                                                                                       \
     {
-#define ATTR(LOCATION)
+#define ATTR(IDX, TYPE, NAME) TYPE NAME
 #define ATTR_BLOCK_END                                                                             \
     }                                                                                              \
     ;
-#define ATTR_LOAD(T, A, N, I) T N = A[I].N;
+#define ATTR_UNPACK(ID, attrs, NAME, TYPE) TYPE NAME = attrs[ID].NAME
 
 #define VARYING_BLOCK_BEGIN(N)                                                                     \
     struct N                                                                                       \
     {
-#define VARYING
+#define VARYING(TYPE, NAME) TYPE NAME
 #define FLAT [[flat]]
 #define NO_PERSPECTIVE [[center_no_perspective]]
 // No-persective interpolation appears to break the guarantee that a varying == "x" when all
 // barycentric values also == "x". Using default (perspective-correct) interpolation is also faster
 // than flat on M1.
 #define @OPTIONALLY_FLAT
-#define VARYING_BLOCK_END                                                                          \
+#define VARYING_BLOCK_END(_pos)                                                                    \
     float4 _pos [[position]];                                                                      \
     }                                                                                              \
     ;
 
+#define VARYING_INIT(varyings, NAME, TYPE) thread TYPE& NAME = varyings.NAME
+#define VARYING_PACK(varyings, NAME)
+#define VARYING_UNPACK(varyings, NAME, TYPE) TYPE NAME = varyings.NAME
+
 #define VERTEX_TEXTURE_BLOCK_BEGIN(N)                                                              \
     struct N                                                                                       \
     {
@@ -98,18 +106,20 @@
     }                                                                                              \
     ;
 
-#define TEXTURE_RGBA32UI(B) [[texture(B)]] texture2d<uint>
-#define TEXTURE_RGBA32F(B) [[texture(B)]] texture2d<float>
-#define TEXTURE_RGBA8(B) [[texture(B)]] texture2d<half>
+#define TEXTURE_RGBA32UI(IDX, NAME) [[texture(IDX)]] texture2d<uint> NAME
+#define TEXTURE_RGBA32F(IDX, NAME) [[texture(IDX)]] texture2d<float> NAME
+#define TEXTURE_RGBA8(IDX, NAME) [[texture(IDX)]] texture2d<half> NAME
 
-#define TEXEL_FETCH(T, N, C, L) T.N.read(uint2(C))
-#define TEXTURE_SAMPLE(T, N, C) T.N.sample(sampler(mag_filter::linear, min_filter::linear), C)
+#define TEXEL_FETCH(TEXTURE_BLOCK, NAME, COORD) TEXTURE_BLOCK.NAME.read(uint2(COORD))
+#define GRADIENT_SAMPLER_DECL(IDX, NAME)
+#define TEXTURE_SAMPLE(TEXTURE_BLOCK, NAME, SAMPLER_NAME, COORD)                                   \
+    TEXTURE_BLOCK.NAME.sample(sampler(mag_filter::linear, min_filter::linear), COORD)
 
 #define PLS_BLOCK_BEGIN                                                                            \
     struct PLS                                                                                     \
     {
-#define PLS_DECL4F(B) [[color(B)]] half4
-#define PLS_DECL2F(B) [[color(B)]] half2
+#define PLS_DECL4F(IDX, NAME) [[color(IDX)]] half4 NAME
+#define PLS_DECL2F(IDX, NAME) [[color(IDX)]] half2 NAME
 #define PLS_BLOCK_END                                                                              \
     }                                                                                              \
     ;
@@ -122,47 +132,58 @@
 #define PLS_INTERLOCK_BEGIN
 #define PLS_INTERLOCK_END
 
-// A hack since we use POSITION inside the following #defines...
-#define POSITION POSITION
-
-#define VERTEX_MAIN(F, V, v, ...)                                                                  \
-    __attribute__((visibility("default"))) V vertex F(__VA_ARGS__)                                 \
+#define VERTEX_MAIN(NAME,                                                                          \
+                    Uniforms,                                                                      \
+                    uniforms,                                                                      \
+                    Attrs,                                                                         \
+                    attrs,                                                                         \
+                    Varyings,                                                                      \
+                    varyings,                                                                      \
+                    VertexTextures,                                                                \
+                    textures,                                                                      \
+                    _vertexID,                                                                     \
+                    _instanceID,                                                                   \
+                    _pos)                                                                          \
+    __attribute__((visibility("default"))) Varyings vertex NAME(                                   \
+        uint _vertexID [[vertex_id]],                                                              \
+        uint _instanceID [[instance_id]],                                                          \
+        constant Uniforms& uniforms [[buffer(0)]],                                                 \
+        constant Attrs* attrs [[buffer(1)]],                                                       \
+        VertexTextures textures)                                                                   \
     {                                                                                              \
-        V v;                                                                                       \
-        float4 POSITION;
+        Varyings varyings;                                                                         \
+        float4 _pos;
 
-#define EMIT_VERTEX(v)                                                                             \
-    v._pos = POSITION;                                                                             \
-    return v;                                                                                      \
+#define EMIT_VERTEX(varyings, _pos)                                                                \
+    }                                                                                              \
+    varyings._pos = _pos;                                                                          \
+    return varyings;
+
+#define EMIT_OFFSCREEN_VERTEX(varyings, _pos)                                                      \
+    varyings._pos.xzw = _pos.xzw;                                                                  \
+    varyings._pos.y = -_pos.y;                                                                     \
+    return varyings;                                                                               \
     }
 
-#define EMIT_OFFSCREEN_VERTEX(v)                                                                   \
-    v._pos = POSITION;                                                                             \
-    v._pos.y = -v._pos.y;                                                                          \
-    return v;                                                                                      \
-    }
-
-#define FRAG_DATA_TYPE(T) T
-#define FRAG_DATA_MAIN(F, ...)                                                                     \
-    __attribute__((visibility("default"))) fragment F(__VA_ARGS__)                                 \
+#define FRAG_DATA_MAIN(DATA_TYPE, NAME, Varyings, varyings)                                        \
+    DATA_TYPE __attribute__((visibility("default"))) fragment NAME(Varyings varyings [[stage_in]]) \
     {
-#define EMIT_FRAG_DATA(f)                                                                          \
-    return f;                                                                                      \
+
+#define EMIT_FRAG_DATA(VALUE)                                                                      \
+    return VALUE;                                                                                  \
     }
 
-#define PLS_MAIN(F, ...)                                                                           \
-    __attribute__((visibility("default"))) PLS fragment F(PLS _inpls, __VA_ARGS__)                 \
+#define PLS_MAIN(NAME, Varyings, varyings, FragmentTextures, textures, _pos, _clockwise)           \
+    __attribute__((visibility("default"))) PLS fragment NAME(PLS _inpls,                           \
+                                                             Varyings varyings [[stage_in]],       \
+                                                             bool _clockwise [[front_facing]],     \
+                                                             FragmentTextures textures)            \
     {                                                                                              \
         PLS _pls;
 
 #define EMIT_PLS                                                                                   \
-    return _pls;                                                                                   \
-    }
-
-// "FLD" for "field".
-// In Metal, inputs and outputs are accessed from structs.
-// In GLSL 300 es, they have to be global.
-#define FLD(BLOCK, MEMBER) BLOCK.MEMBER
+    }                                                                                              \
+    return _pls;
 
 using namespace metal;
 
@@ -196,9 +217,6 @@
     return m_ * (1 / det);
 }
 
-inline float2x2 make_float2x2(float4 f) { return float2x2(f.xy, f.zw); }
-inline float4x2 make_float4x2(float4 a, float4 b) { return float4x2(a.xy, a.zw, b.xy, b.zw); }
-
 inline half3 mix(half3 a, half3 b, bool3 c)
 {
     half3 result;
diff --git a/renderer/shaders/minify.py b/renderer/shaders/minify.py
index df0a039..c427ec4 100644
--- a/renderer/shaders/minify.py
+++ b/renderer/shaders/minify.py
@@ -18,6 +18,8 @@
         - No new name includes the '_' character, so internal code can use '_' without fear of
           renaming collisions.
         - GLSL keywords and builtins are not renamed.
+        - Tokens beginning with '@' have their new name exported to a header file.
+        - Tokens beginning with '$' are not renamed, with the exception of removing the leading '$'.
 
 "file.glsl" gets exported to:
     * "outdir/file.exports.h", with #defines for the rewritten names of each identifier that began
@@ -65,7 +67,7 @@
 
 # lexing functions used by PLY.
 def t_DEFINE(tok):
-    r"\#[ \t]*define[ \t]+(?P<id>\@?[A-Za-z_][A-Za-z0-9_]*)(?P<val>(((\\\n|.)(?!\/[\/\*]))*))"
+    r"\#[ \t]*define[ \t]+(?P<id>[\@\$]?[A-Za-z_][A-Za-z0-9_]*)(?P<val>(((\\\n|.)(?!\/[\/\*]))*))"
     tok.define_id = re.match(t_DEFINE.__doc__, tok.value)['id']
     tok.define_val = re.match(t_DEFINE.__doc__, tok.value)['val']
     defines.add(tok.define_id)
@@ -74,7 +76,7 @@
     return tok
 
 def t_IFDEF(tok):
-    r"(?P<tag>\#[ \t]*ifn?def)[ \t]+(?P<id>\@?[A-Za-z_][A-Za-z0-9_]*)"
+    r"(?P<tag>\#[ \t]*ifn?def)[ \t]+(?P<id>[\@\$]?[A-Za-z_][A-Za-z0-9_]*)"
     tok.ifdef_tag = re.match(t_IFDEF.__doc__, tok.value)['tag']
     tok.ifdef_id = re.match(t_IFDEF.__doc__, tok.value)['id']
     parse_id(tok.ifdef_id, tok.lexer.exports)
@@ -122,7 +124,7 @@
     return tok
 
 def t_ID(tok):
-    r"\@?[A-Za-z_][A-Za-z0-9_]*"
+    r"[\@\$]?[A-Za-z_][A-Za-z0-9_]*"
     parse_id(tok.value, tok.lexer.exports)
     return tok
 
@@ -274,6 +276,10 @@
 
     # keywords borrowed from metal, used by metal.glsl
     "using", "namespace", "metal", "inline", "template", "vec", "as_type", "constant",
+
+    # keywords borrowed from hlsl, used by hlsl.glsl
+    "f16tof32", "f32tof16", "atan2", "asfloat", "asint", "asuint", "frac", "lerp", "rsqrt",
+    "typedef", "min16int", "min16uint"
 }
 
 # rgba and stpq get rewritten to xyzw, so we only need to check xyzw here. This way we can keep
@@ -284,14 +290,16 @@
 def is_reserved_keyword(name):
     return name in glsl_reserved\
            or xyzw_pattern.match(name)\
+           or name.startswith("$")\
            or name.startswith("gl_")\
            or name.startswith("__pixel_local")\
            or name.endswith("ANGLE")
 
-# A leading '@' indicates identifier names that should be exported. This method removes it, if it
-# exists.
-def remove_leading_at(name):
-    return name[1:] if name[0] == '@' else name
+# A leading '@' indicates identifier names that should be exported.
+# A leading '$' indicates identifier names that should not be renamed.
+# This method removes both, if they exist.
+def remove_leading_annotation(name):
+    return name[1:] if name[0] == '@' or name[0] == '$' else name
 
 # generates new identifier names to rewrite our variables.
 # exclude '_' from our new names. Internal variables can use '_' to avoid naming collisions.
@@ -313,7 +321,7 @@
 new_names = {}
 def generate_new_names():
     for name,count in sorted(all_id_counts.items(), key=lambda x:x[1], reverse=True):
-        new_name = (remove_leading_at(name)
+        new_name = (remove_leading_annotation(name)
                     if args.human_readable or is_reserved_keyword(name)
                     else generate_new_name())
         new_names[name] = new_name
diff --git a/renderer/shaders/tessellate.glsl b/renderer/shaders/tessellate.glsl
index 04c3aef..4279f53 100644
--- a/renderer/shaders/tessellate.glsl
+++ b/renderer/shaders/tessellate.glsl
@@ -13,35 +13,35 @@
 
 #ifdef @VERTEX
 ATTR_BLOCK_BEGIN(Attrs)
-ATTR(0) float4 attr_p0p1;
-ATTR(1) float4 attr_p2p3;
-ATTR(2) float4 attr_joinTangent;
-ATTR(3) uint4 attr_args; // [x0, x1, y, polarSegmentCount, contourIDWithFlags]
+ATTR(0, float4, @a_p0p1_);
+ATTR(1, float4, @a_p2p3_);
+ATTR(2, float4, @a_joinTangent);
+ATTR(3, uint4, @a_args); // [x0, x1, y, polarSegmentCount, contourIDWithFlags]
 ATTR_BLOCK_END
 #endif
 
 VARYING_BLOCK_BEGIN(Varyings)
-NO_PERSPECTIVE VARYING float4 p0p1;
-NO_PERSPECTIVE VARYING float4 p2p3;
-NO_PERSPECTIVE VARYING float4 args;     // [vertexIdx, totalVertexCount, joinSegmentCount,
-                                        // parametricSegmentCount, radsPerPolarSegment]
-NO_PERSPECTIVE VARYING float3 joinArgs; // [joinTangent, radsPerJoinSegment]
-FLAT VARYING uint contourIDWithFlags;
-VARYING_BLOCK_END
+NO_PERSPECTIVE VARYING(float4, v_p0p1);
+NO_PERSPECTIVE VARYING(float4, v_p2p3);
+NO_PERSPECTIVE VARYING(float4, v_args);     // [vertexIdx, totalVertexCount, joinSegmentCount,
+                                            //  parametricSegmentCount, radsPerPolarSegment]
+NO_PERSPECTIVE VARYING(float3, v_joinArgs); // [joinTangent, radsPerJoinSegment]
+FLAT VARYING(uint, v_contourIDWithFlags);
+VARYING_BLOCK_END(_pos)
 
 // Tangent of the curve at T=0 and T=1.
-float2x2 find_tangents(float4x2 p)
+INLINE float2x2 find_tangents(float2 p0, float2 p1, float2 p2, float2 p3)
 {
     float2x2 t;
-    t[0] = (any(notEqual(p[0], p[1])) ? p[1] : any(notEqual(p[1], p[2])) ? p[2] : p[3]) - p[0];
-    t[1] = p[3] - (any(notEqual(p[3], p[2])) ? p[2] : any(notEqual(p[2], p[1])) ? p[1] : p[0]);
+    t[0] = (any(notEqual(p0, p1)) ? p1 : any(notEqual(p1, p2)) ? p2 : p3) - p0;
+    t[1] = p3 - (any(notEqual(p3, p2)) ? p2 : any(notEqual(p2, p1)) ? p1 : p0);
     return t;
 }
 
 #ifdef @VERTEX
 VERTEX_TEXTURE_BLOCK_BEGIN(VertexTextures)
-TEXTURE_RGBA32UI(1) @pathTexture;
-TEXTURE_RGBA32UI(2) @contourTexture;
+TEXTURE_RGBA32UI(1, @pathTexture);
+TEXTURE_RGBA32UI(2, @contourTexture);
 VERTEX_TEXTURE_BLOCK_END
 
 float cosine_between_vectors(float2 a, float2 b)
@@ -52,46 +52,56 @@
     return (ab_pow2 == .0) ? 1. : clamp(ab_cosTheta * inversesqrt(ab_pow2), -1., 1.);
 }
 
-VERTEX_MAIN(
-#ifdef METAL
-    @tessellateVertexMain,
-    Varyings,
-    varyings,
-    uint VERTEX_ID [[vertex_id]],
-    uint INSTANCE_ID [[instance_id]],
-    constant @Uniforms& uniforms [[buffer(0)]],
-    constant Attrs* attrs [[buffer(1)]],
-    VertexTextures textures
-#endif
-)
+VERTEX_MAIN(@tessellateVertexMain,
+            @Uniforms,
+            uniforms,
+            Attrs,
+            attrs,
+            Varyings,
+            varyings,
+            VertexTextures,
+            textures,
+            _vertexID,
+            _instanceID,
+            _pos)
 {
-    ATTR_LOAD(float4, attrs, attr_p0p1, INSTANCE_ID);
-    ATTR_LOAD(float4, attrs, attr_p2p3, INSTANCE_ID);
-    ATTR_LOAD(uint4, attrs, attr_args, INSTANCE_ID);
-    ATTR_LOAD(float4, attrs, attr_joinTangent, INSTANCE_ID);
+    ATTR_UNPACK(_instanceID, attrs, @a_p0p1_, float4);
+    ATTR_UNPACK(_instanceID, attrs, @a_p2p3_, float4);
+    ATTR_UNPACK(_instanceID, attrs, @a_joinTangent, float4);
+    ATTR_UNPACK(_instanceID, attrs, @a_args, uint4);
 
-    float4x2 p = float4x2(attr_p0p1.xy, attr_p0p1.zw, attr_p2p3.xy, attr_p2p3.zw);
-    float x0 = float(int(attr_args.x << 16) >> 16);
-    float x1 = float(int(attr_args.x) >> 16);
-    float y = float(attr_args.y);
-    float2 coord = float2((VERTEX_ID & 1) == 0 ? x0 : x1, (VERTEX_ID & 2) == 0 ? y : y + 1.);
+    VARYING_INIT(varyings, v_p0p1, float4);
+    VARYING_INIT(varyings, v_p2p3, float4);
+    VARYING_INIT(varyings, v_args, float4);
+    VARYING_INIT(varyings, v_joinArgs, float3);
+    VARYING_INIT(varyings, v_contourIDWithFlags, uint);
 
-    uint parametricSegmentCount = attr_args.z & 0x3ffu;
-    uint polarSegmentCount = (attr_args.z >> 10) & 0x3ffu;
-    uint joinSegmentCount = attr_args.z >> 20;
-    uint outContourIDWithFlags = attr_args.w;
-    if ((outContourIDWithFlags & CULL_EXCESS_TESSELLATION_SEGMENTS_FLAG) != 0u)
+    float2 p0 = @a_p0p1_.xy;
+    float2 p1 = @a_p0p1_.zw;
+    float2 p2 = @a_p2p3_.xy;
+    float2 p3 = @a_p2p3_.zw;
+    float x0 = float(int(@a_args.x << 16) >> 16);
+    float x1 = float(int(@a_args.x) >> 16);
+    float y = float(@a_args.y);
+    float2 coord = float2((_vertexID & 1) == 0 ? x0 : x1, (_vertexID & 2) == 0 ? y : y + 1.);
+
+    uint parametricSegmentCount = @a_args.z & 0x3ffu;
+    uint polarSegmentCount = (@a_args.z >> 10) & 0x3ffu;
+    uint joinSegmentCount = @a_args.z >> 20;
+    uint contourIDWithFlags = @a_args.w;
+    if ((contourIDWithFlags & CULL_EXCESS_TESSELLATION_SEGMENTS_FLAG) != 0u)
     {
         // This span may have more tessellation vertices allocated to it than necessary (e.g.,
         // outerCurve patches all have a fixed patch size, regardless of how many segments the curve
         // actually needs). Re-run Wang's formula to figure out how many segments we actually need,
         // and make any excess segments degenerate by co-locating their vertices at T=0.
         uint pathIDBits =
-            TEXEL_FETCH(textures, @contourTexture, contour_texel_coord(outContourIDWithFlags), 0).z;
-        float2x2 matrix = make_float2x2(
-            uintBitsToFloat(TEXEL_FETCH(textures, @pathTexture, path_texel_coord(pathIDBits), 0)));
-        float2 d0 = matrix * (-2. * p[1] + p[2] + p[0]);
-        float2 d1 = matrix * (-2. * p[2] + p[3] + p[1]);
+            TEXEL_FETCH(textures, @contourTexture, contour_texel_coord(contourIDWithFlags)).z;
+        float2x2 mat = make_float2x2(
+            uintBitsToFloat(TEXEL_FETCH(textures, @pathTexture, path_texel_coord(pathIDBits))));
+        float2 d0 = MUL(mat, -2. * p1 + p2 + p0);
+
+        float2 d1 = MUL(mat, -2. * p2 + p3 + p1);
         float m = max(dot(d0, d0), dot(d1, d1));
         float n = max(ceil(sqrt(.75 * 4. * sqrt(m))), 1.);
         parametricSegmentCount = min(uint(n), parametricSegmentCount);
@@ -100,30 +110,30 @@
     // *vertex* count is equal to the sum of polar and parametric *segment* counts.
     uint totalVertexCount = parametricSegmentCount + polarSegmentCount + joinSegmentCount - 1u;
 
-    float2x2 tangents = find_tangents(p);
+    float2x2 tangents = find_tangents(p0, p1, p2, p3);
     float theta = acos(cosine_between_vectors(tangents[0], tangents[1]));
     float radsPerPolarSegment = theta / float(polarSegmentCount);
     // Adjust sign of radsPerPolarSegment to match the direction the curve turns.
     // NOTE: Since the curve is not allowed to inflect, we can just check F'(.5) x F''(.5).
-    // NOTE: F'(.5) x F''(.5) has the same sign as (p[2] - p[0]) x (p[3] - p[1]).
-    float turn = determinant(float2x2(p[2] - p[0], p[3] - p[1]));
+    // NOTE: F'(.5) x F''(.5) has the same sign as (p2 - p0) x (p3 - p1).
+    float turn = determinant(float2x2(p2 - p0, p3 - p1));
     if (turn == .0) // This is the case for joins and cusps where points are co-located.
         turn = determinant(tangents);
     if (turn < .0)
         radsPerPolarSegment = -radsPerPolarSegment;
 
-    FLD(varyings, p0p1) = float4(p[0], p[1]);
-    FLD(varyings, p2p3) = float4(p[2], p[3]);
-    FLD(varyings, args) = float4(float(totalVertexCount) + coord.x - x1, // vertexIdx
-                                 float(totalVertexCount),                // totalVertexCount
-                                 (joinSegmentCount << 10) | parametricSegmentCount,
-                                 radsPerPolarSegment);
+    v_p0p1 = float4(p0, p1);
+    v_p2p3 = float4(p2, p3);
+    v_args = float4(float(totalVertexCount) + coord.x - x1, // vertexIdx
+                    float(totalVertexCount),                // totalVertexCount
+                    (joinSegmentCount << 10) | parametricSegmentCount,
+                    radsPerPolarSegment);
     if (joinSegmentCount > 1u)
     {
-        float2x2 joinTangents = float2x2(tangents[1], attr_joinTangent.xy);
+        float2x2 joinTangents = float2x2(tangents[1], @a_joinTangent.xy);
         float joinTheta = acos(cosine_between_vectors(joinTangents[0], joinTangents[1]));
         float joinSpan = float(joinSegmentCount);
-        if ((outContourIDWithFlags & (JOIN_TYPE_MASK | EMULATED_STROKE_CAP_FLAG)) ==
+        if ((contourIDWithFlags & (JOIN_TYPE_MASK | EMULATED_STROKE_CAP_FLAG)) ==
             EMULATED_STROKE_CAP_FLAG)
         {
             // Round caps emulated as joins need to emit vertices at T=0 and T=1, unlike normal
@@ -135,41 +145,50 @@
         float radsPerJoinSegment = joinTheta / joinSpan;
         if (determinant(joinTangents) < .0)
             radsPerJoinSegment = -radsPerJoinSegment;
-        FLD(varyings, joinArgs).xy = attr_joinTangent.xy;
-        FLD(varyings, joinArgs).z = radsPerJoinSegment;
+        v_joinArgs.xy = @a_joinTangent.xy;
+        v_joinArgs.z = radsPerJoinSegment;
     }
-    FLD(varyings, contourIDWithFlags) = outContourIDWithFlags;
+    v_contourIDWithFlags = contourIDWithFlags;
 
-    POSITION.xy = coord * float2(2. / TESS_TEXTURE_WIDTH, uniforms.tessInverseViewportY) - 1.;
-    POSITION.zw = float2(0, 1);
-    EMIT_OFFSCREEN_VERTEX(varyings);
+    _pos.xy = coord * float2(2. / TESS_TEXTURE_WIDTH, uniforms.tessInverseViewportY) - 1.;
+    _pos.zw = float2(0, 1);
+
+    VARYING_PACK(varyings, v_p0p1);
+    VARYING_PACK(varyings, v_p2p3);
+    VARYING_PACK(varyings, v_args);
+    VARYING_PACK(varyings, v_joinArgs);
+    VARYING_PACK(varyings, v_contourIDWithFlags);
+    EMIT_OFFSCREEN_VERTEX(varyings, _pos);
 }
 #endif
 
 #ifdef @FRAGMENT
-FRAG_DATA_TYPE(uint4)
-FRAG_DATA_MAIN(
-#ifdef METAL
-    @tessellateFragmentMain,
-    Varyings varyings [[stage_in]]
-#endif
-)
+FRAG_DATA_MAIN(uint4, @tessellateFragmentMain, Varyings, varyings)
 {
-    float4x2 p = make_float4x2(FLD(varyings, p0p1), FLD(varyings, p2p3));
-    float2x2 tangents = find_tangents(p);
+    VARYING_UNPACK(varyings, v_p0p1, float4);
+    VARYING_UNPACK(varyings, v_p2p3, float4);
+    VARYING_UNPACK(varyings, v_args, float4);
+    VARYING_UNPACK(varyings, v_joinArgs, float3);
+    VARYING_UNPACK(varyings, v_contourIDWithFlags, uint);
+
+    float2 p0 = v_p0p1.xy;
+    float2 p1 = v_p0p1.zw;
+    float2 p2 = v_p2p3.xy;
+    float2 p3 = v_p2p3.zw;
+    float2x2 tangents = find_tangents(p0, p1, p2, p3);
     // Colocate any padding vertices at T=0.
-    float vertexIdx = max(floor(FLD(varyings, args).x), .0);
-    float totalVertexCount = FLD(varyings, args).y;
-    uint joinSegmentCount_and_parametricSegmentCount = uint(FLD(varyings, args).z);
+    float vertexIdx = max(floor(v_args.x), .0);
+    float totalVertexCount = v_args.y;
+    uint joinSegmentCount_and_parametricSegmentCount = uint(v_args.z);
     float parametricSegmentCount = float(joinSegmentCount_and_parametricSegmentCount & 0x3ffu);
     float joinSegmentCount = float(joinSegmentCount_and_parametricSegmentCount >> 10);
-    float radsPerPolarSegment = FLD(varyings, args).w;
-    uint outContourIDWithFlags = FLD(varyings, contourIDWithFlags);
+    float radsPerPolarSegment = v_args.w;
+    uint contourIDWithFlags = v_contourIDWithFlags;
     // PLSRenderer sends down the first curve of each contour with the
     // "FIRST_VERTEX_OF_CONTOUR_FLAG" flag. But from here we need to only output this flag for the
     // first *vertex* of that first curve.
     if (vertexIdx != .0)
-        outContourIDWithFlags &= ~FIRST_VERTEX_OF_CONTOUR_FLAG;
+        contourIDWithFlags &= ~FIRST_VERTEX_OF_CONTOUR_FLAG;
 
     // mergedVertexID/mergedSegmentCount are relative to the sub-section of the instance this vertex
     // belongs to (either the curve section that consists of merged polar and parametric segments,
@@ -181,27 +200,27 @@
     if (mergedVertexID <= mergedSegmentCount)
     {
         // We do belong to the curve section. Clear out any stroke join flags.
-        outContourIDWithFlags &= ~JOIN_TYPE_MASK;
+        contourIDWithFlags &= ~JOIN_TYPE_MASK;
     }
     else
     {
         // We actually belong to the join section following the curve. Construct a point-cubic with
         // rotation.
-        p = float4x2(p[3], p[3], p[3], p[3]);
-        tangents = float2x2(tangents[1], FLD(varyings, joinArgs).xy /*joinTangent*/);
+        p0 = p1 = p2 = p3;
+        tangents = float2x2(tangents[1], v_joinArgs.xy /*joinTangent*/);
         parametricSegmentCount = 1.;
         mergedVertexID -= mergedSegmentCount;
         mergedSegmentCount = joinSegmentCount;
-        if ((outContourIDWithFlags & JOIN_TYPE_MASK) != 0u)
+        if ((contourIDWithFlags & JOIN_TYPE_MASK) != 0u)
         {
             // Miter or bevel join vertices snap to either tangents[0] or tangents[1], and get
             // adjusted in the shader that follows.
             if (mergedVertexID < 2.5) // With 5 join segments, this branch will see IDs: 1, 2, 3, 4.
-                outContourIDWithFlags |= JOIN_TANGENT_0_FLAG;
+                contourIDWithFlags |= JOIN_TANGENT_0_FLAG;
             if (mergedVertexID > 1.5 && mergedVertexID < 3.5)
-                outContourIDWithFlags |= JOIN_TANGENT_INNER_FLAG;
+                contourIDWithFlags |= JOIN_TANGENT_INNER_FLAG;
         }
-        else if ((outContourIDWithFlags & EMULATED_STROKE_CAP_FLAG) != 0u)
+        else if ((contourIDWithFlags & EMULATED_STROKE_CAP_FLAG) != 0u)
         {
             // Round caps emulated as joins need to emit vertices at T=0 and T=1, unlike normal
             // round joins. Preserve the same number of vertices (the CPU should have given us two
@@ -211,29 +230,28 @@
             mergedSegmentCount -= 2.;
             mergedVertexID--;
         }
-        radsPerPolarSegment = FLD(varyings, joinArgs).z; // radsPerJoinSegment.
-        outContourIDWithFlags |= radsPerPolarSegment < .0 ? LEFT_JOIN_FLAG : RIGHT_JOIN_FLAG;
+        radsPerPolarSegment = v_joinArgs.z; // radsPerJoinSegment.
+        contourIDWithFlags |= radsPerPolarSegment < .0 ? LEFT_JOIN_FLAG : RIGHT_JOIN_FLAG;
     }
 
     float2 tessCoord;
-    float theta;
+    float theta = .0;
     if (mergedVertexID == .0 || mergedVertexID == mergedSegmentCount ||
-        (outContourIDWithFlags & JOIN_TYPE_MASK) != 0u)
+        (contourIDWithFlags & JOIN_TYPE_MASK) != 0u)
     {
         // Tessellated vertices at the beginning and end of the strip use exact endpoints and
         // tangents. This ensures crack-free seaming between instances.
         bool isTan0 = mergedVertexID < mergedSegmentCount * .5;
-        tessCoord = isTan0 ? p[0] : p[3];
+        tessCoord = isTan0 ? p0 : p3;
         theta = atan2(isTan0 ? tangents[0] : tangents[1]);
     }
-    else if ((outContourIDWithFlags & RETROFITTED_TRIANGLE_FLAG) != 0u)
+    else if ((contourIDWithFlags & RETROFITTED_TRIANGLE_FLAG) != 0u)
     {
         // This cubic should actually be drawn as the single, non-AA triangle: [p0, p1, p3].
         // This is used to squeeze in more rare triangles, like "breadcrumb" triangles from
         // self-intersections on interior triangulation, where it wouldn't be worth it to put them
         // in their own dedicated draw call.
-        tessCoord = p[1];
-        theta = .0;
+        tessCoord = p1;
     }
     else
     {
@@ -255,9 +273,9 @@
             //                                                 |T^2|
             //     Tangent_Direction(T) = dx,dy = |A  2B  C| * |T  |
             //                                    |.   .  .|   |1  |
-            float2 A, B, C = p[1] - p[0];
-            float2 D = p[3] - p[0];
-            float2 E = p[2] - p[1];
+            float2 A, B, C = p1 - p0;
+            float2 D = p3 - p0;
+            float2 E = p2 - p1;
             B = E - C;
             A = -3. * E + D;
             // FIXME(crbug.com/800804,skbug.com/11268): Consider normalizing the exponents in A,B,C
@@ -352,9 +370,9 @@
         }
 
         // Evaluate the cubic at T. Use De Casteljau's for its accuracy and stability.
-        float2 ab = unchecked_mix(p[0], p[1], T);
-        float2 bc = unchecked_mix(p[1], p[2], T);
-        float2 cd = unchecked_mix(p[2], p[3], T);
+        float2 ab = unchecked_mix(p0, p1, T);
+        float2 bc = unchecked_mix(p1, p2, T);
+        float2 cd = unchecked_mix(p2, p3, T);
         float2 abc = unchecked_mix(ab, bc, T);
         float2 bcd = unchecked_mix(bc, cd, T);
         tessCoord = unchecked_mix(abc, bcd, T);
@@ -365,6 +383,6 @@
             theta = atan2(bcd - abc);
     }
 
-    EMIT_FRAG_DATA(uint4(floatBitsToUint(float3(tessCoord, theta)), outContourIDWithFlags));
+    EMIT_FRAG_DATA(uint4(floatBitsToUint(float3(tessCoord, theta)), contourIDWithFlags));
 }
 #endif