Add coregraphics renderer

CoreGraphics renderer
- new files for the new factory and renderer
- testing hack in skia_host.cpp
- extend TestingWindow to support coregraphics
      - change clear() to be non-const
      - this lets us use coregraphics in gms and goldens

Diffs=
0e9a756b3 Experiment with coregraphics renderer
diff --git a/.rive_head b/.rive_head
index 9b4a9c3..da794fc 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-7ae06e8b84d9d1cb3d97248db66e468d1f2b3db7
+0e9a756b373456564d442a959742b506b08adf44
diff --git a/skia/renderer/include/cg_factory.hpp b/skia/renderer/include/cg_factory.hpp
new file mode 100644
index 0000000..d8534c9
--- /dev/null
+++ b/skia/renderer/include/cg_factory.hpp
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_CG_FACTORY_HPP_
+#define _RIVE_CG_FACTORY_HPP_
+
+#include "rive/factory.hpp"
+#include <vector>
+
+namespace rive {
+
+    class CGFactory : public Factory {
+    public:
+        rcp<RenderBuffer> makeBufferU16(Span<const uint16_t>) override;
+        rcp<RenderBuffer> makeBufferU32(Span<const uint32_t>) override;
+        rcp<RenderBuffer> makeBufferF32(Span<const float>) override;
+
+        rcp<RenderShader> makeLinearGradient(float sx,
+                                             float sy,
+                                             float ex,
+                                             float ey,
+                                             const ColorInt colors[], // [count]
+                                             const float stops[],     // [count]
+                                             size_t count,
+                                             RenderTileMode,
+                                             const Mat2D* localMatrix = nullptr) override;
+
+        rcp<RenderShader> makeRadialGradient(float cx,
+                                             float cy,
+                                             float radius,
+                                             const ColorInt colors[], // [count]
+                                             const float stops[],     // [count]
+                                             size_t count,
+                                             RenderTileMode,
+                                             const Mat2D* localMatrix = nullptr) override;
+
+        std::unique_ptr<RenderPath>
+        makeRenderPath(Span<const Vec2D> points, Span<const PathVerb> verbs, FillRule) override;
+
+        std::unique_ptr<RenderPath> makeEmptyRenderPath() override;
+
+        std::unique_ptr<RenderPaint> makeRenderPaint() override;
+
+        std::unique_ptr<RenderImage> decodeImage(Span<const uint8_t>) override;
+    };
+
+} // namespace rive
+#endif
diff --git a/skia/renderer/include/cg_renderer.hpp b/skia/renderer/include/cg_renderer.hpp
new file mode 100644
index 0000000..e80c734
--- /dev/null
+++ b/skia/renderer/include/cg_renderer.hpp
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_CG_RENDERER_HPP_
+#define _RIVE_CG_RENDERER_HPP_
+
+#include "rive/renderer.hpp"
+
+#if defined(RIVE_BUILD_FOR_OSX)
+#include <ApplicationServices/ApplicationServices.h>
+#elif defined(RIVE_BUILD_FOR_IOS)
+#include <CoreGraphics/CoreGraphics.h>
+#include <ImageIO/ImageIO.h>
+#endif
+
+namespace rive {
+    class CGRenderer : public Renderer {
+    protected:
+        CGContextRef m_ctx;
+
+    public:
+        CGRenderer(CGContextRef ctx, int width, int height);
+        ~CGRenderer() override;
+
+        void save() override;
+        void restore() override;
+        void transform(const Mat2D& transform) override;
+        void clipPath(RenderPath* path) override;
+        void drawPath(RenderPath* path, RenderPaint* paint) override;
+        void drawImage(const RenderImage*, BlendMode, float opacity) override;
+        void drawImageMesh(const RenderImage*,
+                           rcp<RenderBuffer> vertices_f32,
+                           rcp<RenderBuffer> uvCoords_f32,
+                           rcp<RenderBuffer> indices_u16,
+                           BlendMode,
+                           float opacity) override;
+    };
+} // namespace rive
+#endif
diff --git a/skia/renderer/include/mac_utils.hpp b/skia/renderer/include/mac_utils.hpp
index 139a1ba..05408c7 100644
--- a/skia/renderer/include/mac_utils.hpp
+++ b/skia/renderer/include/mac_utils.hpp
@@ -54,30 +54,51 @@
 }
 
 template <typename T> class AutoCF {
-    T m_Obj;
+    T m_obj;
 
 public:
-    AutoCF(T obj = nullptr) : m_Obj(obj) {}
-    ~AutoCF() {
-        if (m_Obj)
-            CFRelease(m_Obj);
+    AutoCF(T obj = nullptr) : m_obj(obj) {}
+    AutoCF(const AutoCF& other) {
+        if (other.m_obj) {
+            CFRetain(other.m_obj);
+        }
+        m_obj = other.m_obj;
     }
-
-    AutoCF(const AutoCF&) = delete;
-    void operator=(const AutoCF&) = delete;
-
-    void reset(T obj) {
-        if (obj != m_Obj) {
-            if (m_Obj) {
-                CFRelease(m_Obj);
-            }
-            m_Obj = obj;
+    AutoCF(AutoCF&& other) {
+        m_obj = other.m_obj;
+        other.m_obj = nullptr;
+    }
+    ~AutoCF() {
+        if (m_obj) {
+            CFRelease(m_obj);
         }
     }
 
-    operator T() const { return m_Obj; }
-    operator bool() const { return m_Obj != nullptr; }
-    T get() const { return m_Obj; }
+    AutoCF& operator=(const AutoCF& other) {
+        if (m_obj != other.m_obj) {
+            if (other.m_obj) {
+                CFRetain(other.m_obj);
+            }
+            if (m_obj) {
+                CFRelease(m_obj);
+            }
+            m_obj = other.m_obj;
+        }
+        return *this;
+    }
+
+    void reset(T obj) {
+        if (obj != m_obj) {
+            if (m_obj) {
+                CFRelease(m_obj);
+            }
+            m_obj = obj;
+        }
+    }
+
+    operator T() const { return m_obj; }
+    operator bool() const { return m_obj != nullptr; }
+    T get() const { return m_obj; }
 };
 
 static inline float find_float(CFDictionaryRef dict, const void* key) {
@@ -110,8 +131,9 @@
 }
 
 namespace rive {
-    CGImageRef DecodeToCGImage(Span<const uint8_t>);
-}
+    AutoCF<CGImageRef> DecodeToCGImage(Span<const uint8_t>);
+    AutoCF<CGImageRef> FlipCGImageInY(AutoCF<CGImageRef>);
+} // namespace rive
 
 #endif
 #endif
diff --git a/skia/renderer/src/cg_factory.cpp b/skia/renderer/src/cg_factory.cpp
new file mode 100644
index 0000000..94c4be8
--- /dev/null
+++ b/skia/renderer/src/cg_factory.cpp
@@ -0,0 +1,476 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "rive/rive_types.hpp"
+
+#ifdef RIVE_BUILD_FOR_APPLE
+
+#include "cg_factory.hpp"
+#include "cg_renderer.hpp"
+#include "mac_utils.hpp"
+
+#if defined(RIVE_BUILD_FOR_OSX)
+#include <ApplicationServices/ApplicationServices.h>
+#elif defined(RIVE_BUILD_FOR_IOS)
+#include <CoreGraphics/CoreGraphics.h>
+#include <ImageIO/ImageIO.h>
+#endif
+
+#include "rive/math/vec2d.hpp"
+#include "rive/shapes/paint/color.hpp"
+
+using namespace rive;
+
+static CGAffineTransform convert(const Mat2D& m) {
+    return CGAffineTransformMake(m[0], m[1], m[2], m[3], m[4], m[5]);
+}
+
+static CGPathDrawingMode convert(FillRule rule) {
+    return (rule == FillRule::nonZero) ? CGPathDrawingMode::kCGPathFill
+                                       : CGPathDrawingMode::kCGPathEOFill;
+}
+
+static CGLineJoin convert(StrokeJoin j) {
+    const CGLineJoin cg[] = {
+        CGLineJoin::kCGLineJoinMiter,
+        CGLineJoin::kCGLineJoinRound,
+        CGLineJoin::kCGLineJoinBevel,
+    };
+    return cg[(unsigned)j];
+}
+
+static CGLineCap convert(StrokeCap c) {
+    const CGLineCap cg[] = {
+        CGLineCap::kCGLineCapButt,
+        CGLineCap::kCGLineCapRound,
+        CGLineCap::kCGLineCapSquare,
+    };
+    return cg[(unsigned)c];
+}
+
+// clang-format off
+static CGBlendMode convert(BlendMode mode) {
+    CGBlendMode cg = kCGBlendModeNormal;
+    switch (mode) {
+        case BlendMode::srcOver: cg = kCGBlendModeNormal; break;
+        case BlendMode::screen: cg = kCGBlendModeScreen; break;
+        case BlendMode::overlay: cg = kCGBlendModeOverlay; break;
+        case BlendMode::darken: cg = kCGBlendModeDarken; break;
+        case BlendMode::lighten: cg = kCGBlendModeLighten; break;
+        case BlendMode::colorDodge: cg = kCGBlendModeColorDodge; break;
+        case BlendMode::colorBurn: cg = kCGBlendModeColorBurn; break;
+        case BlendMode::hardLight: cg = kCGBlendModeHardLight; break;
+        case BlendMode::softLight: cg = kCGBlendModeSoftLight; break;
+        case BlendMode::difference: cg = kCGBlendModeDifference; break;
+        case BlendMode::exclusion: cg = kCGBlendModeExclusion; break;
+        case BlendMode::multiply: cg = kCGBlendModeMultiply; break;
+        case BlendMode::hue: cg = kCGBlendModeHue; break;
+        case BlendMode::saturation: cg = kCGBlendModeSaturation; break;
+        case BlendMode::color: cg = kCGBlendModeColor; break;
+        case BlendMode::luminosity: cg = kCGBlendModeLuminosity; break;
+    }
+    return cg;
+}
+// clang-format on
+
+static void convertColor(ColorInt c, CGFloat rgba[]) {
+    constexpr float kByteToUnit = 1.0f / 255;
+    rgba[0] = colorRed(c) * kByteToUnit;
+    rgba[1] = colorGreen(c) * kByteToUnit;
+    rgba[2] = colorBlue(c) * kByteToUnit;
+    rgba[3] = colorAlpha(c) * kByteToUnit;
+}
+
+class CGRenderPath : public RenderPath {
+private:
+    AutoCF<CGMutablePathRef> m_path = CGPathCreateMutable();
+    CGPathDrawingMode m_fillMode = CGPathDrawingMode::kCGPathFill;
+
+public:
+    CGRenderPath() {}
+
+    CGRenderPath(Span<const Vec2D> pts, Span<const PathVerb> vbs, FillRule rule) {
+        m_fillMode = convert(rule);
+
+        auto p = pts.data();
+        for (auto v : vbs) {
+            switch ((PathVerb)v) {
+                case PathVerb::move:
+                    CGPathMoveToPoint(m_path, nullptr, p[0].x, p[0].y);
+                    p += 1;
+                    break;
+                case PathVerb::line:
+                    CGPathAddLineToPoint(m_path, nullptr, p[0].x, p[0].y);
+                    p += 1;
+                    break;
+                case PathVerb::quad:
+                    CGPathAddQuadCurveToPoint(m_path, nullptr, p[0].x, p[0].y, p[1].x, p[1].y);
+                    p += 2;
+                    break;
+                case PathVerb::cubic:
+                    CGPathAddCurveToPoint(
+                        m_path, nullptr, p[0].x, p[0].y, p[1].x, p[1].y, p[2].x, p[2].y);
+                    p += 3;
+                    break;
+                case PathVerb::close:
+                    CGPathCloseSubpath(m_path);
+                    break;
+            }
+        }
+        assert(p == pts.end());
+    }
+
+    CGPathRef path() const { return m_path.get(); }
+    CGPathDrawingMode drawingMode(bool isStroke) const {
+        return isStroke ? CGPathDrawingMode::kCGPathStroke : m_fillMode;
+    }
+
+    void reset() override { m_path.reset(CGPathCreateMutable()); }
+    void addRenderPath(RenderPath* path, const Mat2D& mx) override {
+        auto transform = convert(mx);
+        CGPathAddPath(m_path, &transform, ((CGRenderPath*)path)->path());
+    }
+    void fillRule(FillRule value) override {
+        m_fillMode = (value == FillRule::nonZero) ? CGPathDrawingMode::kCGPathFill
+                                                  : CGPathDrawingMode::kCGPathEOFill;
+    }
+    void moveTo(float x, float y) override { CGPathMoveToPoint(m_path, nullptr, x, y); }
+    void lineTo(float x, float y) override { CGPathAddLineToPoint(m_path, nullptr, x, y); }
+    void cubicTo(float ox, float oy, float ix, float iy, float x, float y) override {
+        CGPathAddCurveToPoint(m_path, nullptr, ox, oy, ix, iy, x, y);
+    }
+    void close() override { CGPathCloseSubpath(m_path); }
+};
+
+class CGRenderShader : public RenderShader {
+public:
+    CGRenderShader() {}
+
+    virtual void draw(CGContextRef) {}
+};
+
+class CGRenderPaint : public RenderPaint {
+private:
+    bool m_isStroke = false;
+    CGFloat m_rgba[4] = {0, 0, 0, 1};
+    float m_width = 1;
+    CGLineJoin m_join = kCGLineJoinMiter;
+    CGLineCap m_cap = kCGLineCapButt;
+    CGBlendMode m_blend = kCGBlendModeNormal;
+    rcp<RenderShader> m_shader;
+
+public:
+    CGRenderPaint() {}
+
+    bool isStroke() const { return m_isStroke; }
+
+    CGRenderShader* shader() const { return static_cast<CGRenderShader*>(m_shader.get()); }
+
+    void apply(CGContextRef ctx) {
+        if (m_isStroke) {
+            CGContextSetRGBStrokeColor(ctx, m_rgba[0], m_rgba[1], m_rgba[2], m_rgba[3]);
+            CGContextSetLineWidth(ctx, m_width);
+            CGContextSetLineJoin(ctx, m_join);
+            CGContextSetLineCap(ctx, m_cap);
+        } else {
+            CGContextSetRGBFillColor(ctx, m_rgba[0], m_rgba[1], m_rgba[2], m_rgba[3]);
+        }
+        CGContextSetBlendMode(ctx, m_blend);
+    }
+
+    void style(RenderPaintStyle style) override {
+        m_isStroke = (style == RenderPaintStyle::stroke);
+    }
+    void color(ColorInt value) override { convertColor(value, m_rgba); }
+    void thickness(float value) override { m_width = value; }
+    void join(StrokeJoin value) override { m_join = convert(value); }
+    void cap(StrokeCap value) override { m_cap = convert(value); }
+    void blendMode(BlendMode value) override { m_blend = convert(value); }
+    void shader(rcp<RenderShader> sh) override { m_shader = std::move(sh); }
+};
+
+static CGGradientRef convert(const ColorInt colors[], const float stops[], size_t count) {
+    AutoCF space = CGColorSpaceCreateDeviceRGB();
+    std::vector<CGFloat> floats(count * 5); // colors[4] + stops[1]
+    auto c = &floats[0];
+    auto s = &floats[count * 4];
+
+    for (size_t i = 0; i < count; ++i) {
+        convertColor(colors[i], &c[i * 4]);
+    }
+    if (stops) {
+        for (size_t i = 0; i < count; ++i) {
+            s[i] = stops[i];
+        }
+    }
+    return CGGradientCreateWithColorComponents(space, c, s, count);
+}
+
+class CGRadialGradientRenderShader : public CGRenderShader {
+    AutoCF<CGGradientRef> m_grad;
+    CGPoint m_center;
+    CGFloat m_radius;
+
+public:
+    CGRadialGradientRenderShader(float cx,
+                                 float cy,
+                                 float radius,
+                                 const ColorInt colors[],
+                                 const float stops[],
+                                 size_t count) :
+        m_grad(convert(colors, stops, count)) {
+        m_center = CGPointMake(cx, cy);
+        m_radius = radius;
+    }
+
+    void draw(CGContextRef ctx) override {
+        CGGradientDrawingOptions options = 0;
+        CGContextDrawRadialGradient(ctx, m_grad, m_center, 0, m_center, m_radius, options);
+    }
+};
+
+class CGLinearGradientRenderShader : public CGRenderShader {
+    AutoCF<CGGradientRef> m_grad;
+    CGPoint m_start, m_end;
+
+public:
+    CGLinearGradientRenderShader(float sx,
+                                 float sy,
+                                 float ex,
+                                 float ey,
+                                 const ColorInt colors[], // [count]
+                                 const float stops[],     // [count]
+                                 size_t count) :
+        m_grad(convert(colors, stops, count)) {
+        m_start = CGPointMake(sx, sy);
+        m_end = CGPointMake(ex, ey);
+    }
+
+    void draw(CGContextRef ctx) override {
+        CGGradientDrawingOptions options = 0;
+        CGContextDrawLinearGradient(ctx, m_grad, m_start, m_end, options);
+    }
+};
+
+class CGRenderImage : public RenderImage {
+public:
+    AutoCF<CGImageRef> m_image;
+
+    CGRenderImage(const Span<const uint8_t> span) : m_image(FlipCGImageInY(DecodeToCGImage(span))) {
+        if (m_image) {
+            m_Width = CGImageGetWidth(m_image.get());
+            m_Height = CGImageGetHeight(m_image.get());
+        }
+    }
+
+    rcp<RenderShader>
+    makeShader(RenderTileMode tx, RenderTileMode ty, const Mat2D* localMatrix) const override {
+        return rcp<RenderShader>(new CGRenderShader);
+    }
+
+    static CGImageRef Cast(const RenderImage* image) {
+        return reinterpret_cast<const CGRenderImage*>(image)->m_image.get();
+    }
+};
+
+// todo: move this to common place
+class DataRenderBuffer : public RenderBuffer {
+    const size_t m_elemSize;
+    std::vector<uint32_t> m_storage; // store 32bits for alignment
+
+public:
+    DataRenderBuffer(const void* src, size_t count, size_t elemSize) :
+        RenderBuffer(count), m_elemSize(elemSize) {
+        const size_t bytes = count * elemSize;
+        m_storage.resize((bytes + 3) >> 2); // round up to next 32bit count
+        memcpy(m_storage.data(), src, bytes);
+    }
+
+    const float* f32s() const {
+        assert(m_elemSize == sizeof(float));
+        return reinterpret_cast<const float*>(m_storage.data());
+    }
+
+    const uint16_t* u16s() const {
+        assert(m_elemSize == sizeof(uint16_t));
+        return reinterpret_cast<const uint16_t*>(m_storage.data());
+    }
+
+    const Vec2D* vecs() const { return reinterpret_cast<const Vec2D*>(this->f32s()); }
+
+    size_t elemSize() const { return m_elemSize; }
+
+    static const DataRenderBuffer* Cast(const RenderBuffer* buffer) {
+        return static_cast<const DataRenderBuffer*>(buffer);
+    }
+};
+
+template <typename T> rcp<RenderBuffer> make_buffer(Span<T> span) {
+    return rcp<RenderBuffer>(new DataRenderBuffer(span.data(), span.size(), sizeof(T)));
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+CGRenderer::CGRenderer(CGContextRef ctx, int width, int height) : m_ctx(ctx) {
+    CGContextSaveGState(ctx);
+    Mat2D m(1, 0, 0, -1, 0, height);
+    CGContextConcatCTM(ctx, convert(m));
+}
+
+CGRenderer::~CGRenderer() { CGContextRestoreGState(m_ctx); }
+
+void CGRenderer::save() { CGContextSaveGState(m_ctx); }
+
+void CGRenderer::restore() { CGContextRestoreGState(m_ctx); }
+
+void CGRenderer::transform(const Mat2D& m) { CGContextConcatCTM(m_ctx, convert(m)); }
+
+void CGRenderer::drawPath(RenderPath* path, RenderPaint* paint) {
+    auto cgpaint = reinterpret_cast<CGRenderPaint*>(paint);
+    auto cgpath = reinterpret_cast<CGRenderPath*>(path);
+
+    cgpaint->apply(m_ctx);
+
+    CGContextBeginPath(m_ctx);
+    CGContextAddPath(m_ctx, cgpath->path());
+    if (auto sh = cgpaint->shader()) {
+        CGContextSaveGState(m_ctx);
+        CGContextClip(m_ctx);
+        sh->draw(m_ctx);
+        CGContextRestoreGState(m_ctx);
+    } else {
+        CGContextDrawPath(m_ctx, cgpath->drawingMode(cgpaint->isStroke()));
+    }
+}
+
+void CGRenderer::clipPath(RenderPath* path) {
+    auto cgpath = reinterpret_cast<CGRenderPath*>(path);
+
+    CGContextBeginPath(m_ctx);
+    CGContextAddPath(m_ctx, cgpath->path());
+    CGContextClip(m_ctx);
+}
+
+void CGRenderer::drawImage(const RenderImage* image, BlendMode blendMode, float opacity) {
+    auto bounds = CGRectMake(0, 0, image->width(), image->height());
+
+    CGContextSaveGState(m_ctx);
+    CGContextSetAlpha(m_ctx, opacity);
+    CGContextSetBlendMode(m_ctx, convert(blendMode));
+    CGContextDrawImage(m_ctx, bounds, CGRenderImage::Cast(image));
+    CGContextRestoreGState(m_ctx);
+}
+
+static Mat2D basis_matrix(Vec2D p0, Vec2D p1, Vec2D p2) {
+    auto e0 = p1 - p0;
+    auto e1 = p2 - p0;
+    return Mat2D(e0.x, e0.y, e1.x, e1.y, p0.x, p0.y);
+}
+
+void CGRenderer::drawImageMesh(const RenderImage* image,
+                               rcp<RenderBuffer> vertices,
+                               rcp<RenderBuffer> uvCoords,
+                               rcp<RenderBuffer> indices,
+                               BlendMode blendMode,
+                               float opacity) {
+    const float sx = image->width();
+    const float sy = image->height();
+    auto const bounds = CGRectMake(0, 0, sx, sy);
+
+    auto scale = [sx, sy](Vec2D v) { return Vec2D{v.x * sx, v.y * sy}; };
+
+    auto triangles = indices->count() / 3;
+    auto ndx = DataRenderBuffer::Cast(indices.get())->u16s();
+    auto pts = DataRenderBuffer::Cast(vertices.get())->vecs();
+    auto uvs = DataRenderBuffer::Cast(uvCoords.get())->vecs();
+
+    // We use the path to set the clip for each triangle. Since calling
+    // CGContextClip() resets the path, we only need to this once at
+    // the beginning.
+    CGContextBeginPath(m_ctx);
+
+    CGContextSaveGState(m_ctx);
+    CGContextSetAlpha(m_ctx, opacity);
+    CGContextSetBlendMode(m_ctx, convert(blendMode));
+    CGContextSetShouldAntialias(m_ctx, false);
+
+    for (size_t i = 0; i < triangles; ++i) {
+        const auto index0 = *ndx++;
+        const auto index1 = *ndx++;
+        const auto index2 = *ndx++;
+
+        CGContextSaveGState(m_ctx);
+
+        const auto p0 = pts[index0];
+        const auto p1 = pts[index1];
+        const auto p2 = pts[index2];
+        CGContextMoveToPoint(m_ctx, p0.x, p0.y);
+        CGContextAddLineToPoint(m_ctx, p1.x, p1.y);
+        CGContextAddLineToPoint(m_ctx, p2.x, p2.y);
+        CGContextClip(m_ctx);
+
+        const auto v0 = scale(uvs[index0]);
+        const auto v1 = scale(uvs[index1]);
+        const auto v2 = scale(uvs[index2]);
+        auto mx = basis_matrix(p0, p1, p2) * basis_matrix(v0, v1, v2).invertOrIdentity();
+        CGContextConcatCTM(m_ctx, convert(mx));
+        CGContextDrawImage(m_ctx, bounds, CGRenderImage::Cast(image));
+
+        CGContextRestoreGState(m_ctx);
+    }
+
+    CGContextRestoreGState(m_ctx); // restore opacity, antialias, etc.
+}
+
+// Factory
+
+rcp<RenderBuffer> CGFactory::makeBufferU16(Span<const uint16_t> data) { return make_buffer(data); }
+
+rcp<RenderBuffer> CGFactory::makeBufferU32(Span<const uint32_t> data) { return make_buffer(data); }
+
+rcp<RenderBuffer> CGFactory::makeBufferF32(Span<const float> data) { return make_buffer(data); }
+
+rcp<RenderShader> CGFactory::makeLinearGradient(float sx,
+                                                float sy,
+                                                float ex,
+                                                float ey,
+                                                const ColorInt colors[], // [count]
+                                                const float stops[],     // [count]
+                                                size_t count,
+                                                RenderTileMode,
+                                                const Mat2D*) {
+    return rcp<RenderShader>(
+        new CGLinearGradientRenderShader(sx, sy, ex, ey, colors, stops, count));
+}
+
+rcp<RenderShader> CGFactory::makeRadialGradient(float cx,
+                                                float cy,
+                                                float radius,
+                                                const ColorInt colors[], // [count]
+                                                const float stops[],     // [count]
+                                                size_t count,
+                                                RenderTileMode mode,
+                                                const Mat2D* localMatrix) {
+    return rcp<RenderShader>(
+        new CGRadialGradientRenderShader(cx, cy, radius, colors, stops, count));
+}
+
+std::unique_ptr<RenderPath>
+CGFactory::makeRenderPath(Span<const Vec2D> points, Span<const PathVerb> verbs, FillRule fillRule) {
+    return std::make_unique<CGRenderPath>(points, verbs, fillRule);
+}
+
+std::unique_ptr<RenderPath> CGFactory::makeEmptyRenderPath() {
+    return std::make_unique<CGRenderPath>();
+}
+
+std::unique_ptr<RenderPaint> CGFactory::makeRenderPaint() {
+    return std::make_unique<CGRenderPaint>();
+}
+
+std::unique_ptr<RenderImage> CGFactory::decodeImage(Span<const uint8_t> encoded) {
+    return std::make_unique<CGRenderImage>(encoded);
+}
+
+#endif // APPLE
diff --git a/skia/renderer/src/mac_utils.cpp b/skia/renderer/src/mac_utils.cpp
index cddcf4e..893207c 100644
--- a/skia/renderer/src/mac_utils.cpp
+++ b/skia/renderer/src/mac_utils.cpp
@@ -11,7 +11,22 @@
 #include <ImageIO/CGImageSource.h>
 #endif
 
-CGImageRef rive::DecodeToCGImage(rive::Span<const uint8_t> span) {
+AutoCF<CGImageRef> rive::FlipCGImageInY(AutoCF<CGImageRef> image) {
+    if (!image) {
+        return nullptr;
+    }
+
+    auto w = CGImageGetWidth(image);
+    auto h = CGImageGetHeight(image);
+    AutoCF space = CGColorSpaceCreateDeviceRGB();
+    auto info = kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast;
+    AutoCF ctx = CGBitmapContextCreate(nullptr, w, h, 8, 0, space, info);
+    CGContextConcatCTM(ctx, CGAffineTransformMake(1, 0, 0, -1, 0, h));
+    CGContextDrawImage(ctx, CGRectMake(0, 0, w, h), image);
+    return CGBitmapContextCreateImage(ctx);
+}
+
+AutoCF<CGImageRef> rive::DecodeToCGImage(rive::Span<const uint8_t> span) {
     AutoCF data = CFDataCreate(nullptr, span.data(), span.size());
     if (!data) {
         printf("CFDataCreate failed\n");
@@ -24,10 +39,9 @@
         return nullptr;
     }
 
-    auto image = CGImageSourceCreateImageAtIndex(source, 0, nullptr);
+    AutoCF image = CGImageSourceCreateImageAtIndex(source, 0, nullptr);
     if (!image) {
         printf("CGImageSourceCreateImageAtIndex failed\n");
-        return nullptr;
     }
     return image;
 }
diff --git a/viewer/src/skia/skia_host.cpp b/viewer/src/skia/skia_host.cpp
index 6f0021c..71abe50 100644
--- a/viewer/src/skia/skia_host.cpp
+++ b/viewer/src/skia/skia_host.cpp
@@ -7,7 +7,13 @@
 
 #ifdef RIVE_RENDERER_SKIA
 
+#ifdef RIVE_BUILD_FOR_APPLE
+#include "cg_skia_factory.hpp"
+static rive::CGSkiaFactory skiaFactory;
+#else
 #include "skia_factory.hpp"
+static rive::SkiaFactory skiaFactory;
+#endif
 #include "skia_renderer.hpp"
 
 #include "include/core/SkSurface.h"
@@ -20,6 +26,33 @@
 sk_sp<SkSurface> makeSkiaSurface(GrDirectContext* context, int width, int height);
 void skiaPresentSurface(sk_sp<SkSurface> surface);
 
+// Experimental flag, until we complete coregraphics_host
+#define TEST_CG_RENDERER
+
+#ifdef TEST_CG_RENDERER
+#include "cg_factory.hpp"
+#include "cg_renderer.hpp"
+#include "mac_utils.hpp"
+static void render_with_cg(SkCanvas* canvas, int w, int h, ViewerContent* content, double elapsed) {
+    // cons up a CGContext
+    auto pixels = SkData::MakeUninitialized(w * h * 4);
+    auto bytes = (uint8_t*)pixels->writable_data();
+    std::fill(bytes, bytes + pixels->size(), 0);
+    AutoCF space = CGColorSpaceCreateDeviceRGB();
+    auto info = kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast;
+    AutoCF ctx = CGBitmapContextCreate(bytes, w, h, 8, w * 4, space, info);
+
+    // Wrap it with our renderer
+    rive::CGRenderer renderer(ctx, w, h);
+    content->handleDraw(&renderer, elapsed);
+    CGContextFlush(ctx);
+
+    // Draw the pixels into the canvas
+    auto img = SkImage::MakeRasterData(SkImageInfo::MakeN32Premul(w, h), pixels, w * 4);
+    canvas->drawImage(img, 0, 0, SkSamplingOptions(SkFilterMode::kNearest), nullptr);
+}
+#endif
+
 class SkiaViewerHost : public ViewerHost {
 public:
     sk_sp<GrDirectContext> m_context;
@@ -59,9 +92,13 @@
         paint.setColor(0xFF161616);
         canvas->drawPaint(paint);
 
-        rive::SkiaRenderer skiaRenderer(canvas);
         if (content) {
+#ifdef TEST_CG_RENDERER
+            render_with_cg(canvas, m_dimensions.width(), m_dimensions.height(), content, elapsed);
+#else
+            rive::SkiaRenderer skiaRenderer(canvas);
             content->handleDraw(&skiaRenderer, elapsed);
+#endif
         }
 
         canvas->flush();
@@ -73,8 +110,12 @@
 std::unique_ptr<ViewerHost> ViewerHost::Make() { return std::make_unique<SkiaViewerHost>(); }
 
 rive::Factory* ViewerHost::Factory() {
-    static rive::SkiaFactory skiaFactory;
+#ifdef TEST_CG_RENDERER
+    static rive::CGFactory gFactory;
+    return &gFactory;
+#else
     return &skiaFactory;
+#endif
 }
 
 #endif