Manually track render-objects for leaks
diff --git a/include/rive/renderer.hpp b/include/rive/renderer.hpp
index 5d23576..85559b9 100644
--- a/include/rive/renderer.hpp
+++ b/include/rive/renderer.hpp
@@ -31,7 +31,8 @@
         const size_t m_Count;
 
     public:
-        RenderBuffer(size_t count) : m_Count(count) {}
+        RenderBuffer(size_t count);
+        ~RenderBuffer() override;
 
         size_t count() const { return m_Count; }
     };
@@ -53,10 +54,17 @@
      *  It is common that a shader may be created with a 'localMatrix'. If this is
      *  not null, then it is applied to the shader's domain before the Renderer's CTM.
      */
-    class RenderShader : public RefCnt {};
+    class RenderShader : public RefCnt {
+    public:
+        RenderShader();
+        ~RenderShader() override;
+    };
 
     class RenderPaint {
     public:
+        RenderPaint();
+        virtual ~RenderPaint();
+    
         virtual void style(RenderPaintStyle style) = 0;
         virtual void color(ColorInt value) = 0;
         virtual void thickness(float value) = 0;
@@ -64,8 +72,6 @@
         virtual void cap(StrokeCap value) = 0;
         virtual void blendMode(BlendMode value) = 0;
         virtual void shader(rcp<RenderShader>) = 0;
-
-        virtual ~RenderPaint() {}
     };
 
     class RenderImage {
@@ -74,7 +80,9 @@
         int m_Height = 0;
 
     public:
-        virtual ~RenderImage() {}
+        RenderImage();
+        virtual ~RenderImage();
+
         int width() const { return m_Width; }
         int height() const { return m_Height; }
 
@@ -85,6 +93,9 @@
 
     class RenderPath : public CommandPath {
     public:
+        RenderPath();
+        ~RenderPath() override;
+
         RenderPath* renderPath() override { return this; }
         void addPath(CommandPath* path, const Mat2D& transform) override {
             addRenderPath(path->renderPath(), transform);
diff --git a/skia/viewer/src/main.cpp b/skia/viewer/src/main.cpp
index 16d1096..204e539 100644
--- a/skia/viewer/src/main.cpp
+++ b/skia/viewer/src/main.cpp
@@ -29,6 +29,8 @@
 #include "imgui/backends/imgui_impl_glfw.h"
 #include "imgui/backends/imgui_impl_opengl3.h"
 
+#include "../src/render_counter.hpp"
+
 #include <cmath>
 #include <stdio.h>
 
@@ -39,6 +41,14 @@
 std::unique_ptr<rive::ArtboardInstance> artboardInstance;
 std::unique_ptr<rive::Scene> currentScene;
 
+static void delete_file() {
+    currentScene = nullptr;
+    artboardInstance = nullptr;
+    currentFile = nullptr;
+
+    rive::RenderCounter::globalCounter().dump("After deleting file");
+}
+
 // ImGui wants raw pointers to names, but our public API returns
 // names as strings (by value), so we cache these names each time we
 // load a file
@@ -129,6 +139,8 @@
     if (currentScene) {
         currentScene->inputCount();
     }
+
+    rive::RenderCounter::globalCounter().dump("After loading file");
 }
 
 void initAnimation(int index) {
@@ -153,6 +165,8 @@
         currentScene = artboardInstance->animationAt(index);
         currentScene->inputCount();
     }
+
+    rive::RenderCounter::globalCounter().dump("After loading file");
 }
 
 rive::Mat2D gInverseViewTransform;
@@ -416,6 +430,8 @@
         glfwPollEvents();
     }
 
+    delete_file();
+
     // Cleanup Skia.
     surface = nullptr;
     context = nullptr;
diff --git a/src/render_counter.hpp b/src/render_counter.hpp
new file mode 100644
index 0000000..3481e5e
--- /dev/null
+++ b/src/render_counter.hpp
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_RENDERER_COUNTER_HPP_
+#define _RIVE_RENDERER_COUNTER_HPP_
+
+#include "rive/rive_types.hpp"
+
+namespace rive {
+
+enum RenderCounterType {
+    kBuffer,
+    kPath,
+    kPaint,
+    kShader,
+    kImage,
+
+    kLastCounterType = kImage,
+};
+
+struct RenderCounter {
+    int counts[RenderCounterType::kLastCounterType + 1] = {};
+
+    void update(RenderCounterType ct, int delta) {
+        assert(delta == 1 || delta == -1);
+        counts[ct] += delta;
+    }
+
+    void dump(const char label[] = nullptr) const;
+
+    static RenderCounter& globalCounter();
+};
+
+} // namespace rive
+
+#endif
diff --git a/src/renderer.cpp b/src/renderer.cpp
index 37c3a89..87111ee 100644
--- a/src/renderer.cpp
+++ b/src/renderer.cpp
@@ -67,3 +67,46 @@
     const float c = std::cos(radians);
     this->transform(Mat2D(c, s, -s, c, 0, 0));
 }
+
+/////////////////////////////////////////
+
+#include "../src/render_counter.hpp"
+
+static RenderCounter gCounter;
+
+const char* gCounterNames[] = {
+    "buffer", "path", "paint", "shader", "image",
+};
+
+void RenderCounter::dump(const char label[]) const {
+    if (label == nullptr) {
+        label = "RenderCounters";
+    }
+    printf("%s:", label);
+    for (int i = 0; i <= kLastCounterType; ++i) {
+        printf(" [%s]:%d", gCounterNames[i], counts[i]);
+    }
+    printf("\n");
+}
+
+RenderCounter& RenderCounter::globalCounter() { return gCounter; }
+
+RenderBuffer::RenderBuffer(size_t count) : m_Count(count) {
+    gCounter.update(kBuffer, 1);
+}
+
+RenderBuffer::~RenderBuffer() {
+    gCounter.update(kBuffer, -1);
+}
+
+RenderShader::RenderShader() { gCounter.update(kShader, 1); }
+RenderShader::~RenderShader() { gCounter.update(kShader, -1); }
+
+RenderPaint::RenderPaint() { gCounter.update(kPaint, 1); }
+RenderPaint::~RenderPaint() { gCounter.update(kPaint, -1); }
+
+RenderImage::RenderImage() { gCounter.update(kImage, 1); }
+RenderImage::~RenderImage() { gCounter.update(kImage, -1); }
+
+RenderPath::RenderPath() { gCounter.update(kPath, 1); }
+RenderPath::~RenderPath() { gCounter.update(kPath, -1); }
diff --git a/test/file_test.cpp b/test/file_test.cpp
index 05d0e5e..b8cd48b 100644
--- a/test/file_test.cpp
+++ b/test/file_test.cpp
@@ -8,6 +8,7 @@
 #include <cstdio>
 
 TEST_CASE("file can be read", "[file]") {
+    RenderObjectLeakChecker checker;
     auto file = ReadRiveFile("../../test/assets/two_artboards.riv");
 
     // Default artboard should be named Two.
@@ -18,6 +19,7 @@
 }
 
 TEST_CASE("file with animation can be read", "[file]") {
+    RenderObjectLeakChecker checker;
     auto file = ReadRiveFile("../../test/assets/juice.riv");
 
     auto artboard = file->artboard();
@@ -42,6 +44,7 @@
 }
 
 TEST_CASE("artboards can be counted and accessed via index or name", "[file]") {
+    RenderObjectLeakChecker checker;
     auto file = ReadRiveFile("../../test/assets/dependency_test.riv");
 
     // The artboards caqn be counted
@@ -55,6 +58,7 @@
 }
 
 TEST_CASE("dependencies are as expected", "[file]") {
+    RenderObjectLeakChecker checker;
     // ┌────┐
     // │Blue│
     // └────┘
@@ -112,6 +116,7 @@
 }
 
 TEST_CASE("long name in object is parsed correctly", "[file]") {
+    RenderObjectLeakChecker checker;
     auto file = ReadRiveFile("../../test/assets/long_name.riv");
     auto artboard = file->artboard();
 
diff --git a/test/image_asset_test.cpp b/test/image_asset_test.cpp
index e65d25f..ae0eb4b 100644
--- a/test/image_asset_test.cpp
+++ b/test/image_asset_test.cpp
@@ -12,6 +12,7 @@
 #include <cstdio>
 
 TEST_CASE("image assets loads correctly", "[assets]") {
+    RenderObjectLeakChecker checker;
     auto file = ReadRiveFile("../../test/assets/walle.riv");
 
     auto node = file->artboard()->find("walle");
@@ -42,6 +43,7 @@
 }
 
 TEST_CASE("out of band image assets loads correctly", "[assets]") {
+    RenderObjectLeakChecker checker;
     rive::NoOpFactory gEmptyFactory;
     
     std::string filename = "../../test/assets/out_of_band/walle.riv";
diff --git a/test/rive_file_reader.hpp b/test/rive_file_reader.hpp
index 4294290..1076359 100644
--- a/test/rive_file_reader.hpp
+++ b/test/rive_file_reader.hpp
@@ -4,6 +4,7 @@
 #include <rive/file.hpp>
 #include "rive_testing.hpp"
 #include "no_op_factory.hpp"
+#include "../src/render_counter.hpp"
 
 static inline std::unique_ptr<rive::File>
 ReadRiveFile(const char path[],
@@ -32,4 +33,21 @@
     return file;
 }
 
+class RenderObjectLeakChecker {
+    rive::RenderCounter m_Before;
+
+public:
+    RenderObjectLeakChecker() : m_Before(rive::RenderCounter::globalCounter()) {}
+    ~RenderObjectLeakChecker() {
+        auto after = rive::RenderCounter::globalCounter();
+        for (int i = 0; i <= rive::RenderCounterType::kLastCounterType; ++i) {
+            if (after.counts[i] != m_Before.counts[i]) {
+                m_Before.dump("before");
+                after.dump("after");
+                REQUIRE(false);
+            }
+        }
+    }
+};
+
 #endif