Add a "lite_rtti" utility and use it with Render objects

We need to be more robust if a user tries to use mismatched factories and renderers. This PR creates a simple "lite_rtti" utility and applies it to every Render object. The renderers now abort early instead of crashing if they are given a Render object for the wrong renderer.

Diffs=
c357e7aa7 Add a "lite_rtti" utility and use it with Render objects (#6311)

Co-authored-by: Chris Dalton <99840794+csmartdalton@users.noreply.github.com>
diff --git a/.rive_head b/.rive_head
index 467da65..a54f657 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-53972cc17b532e83ffee7cae7d3420e95f822e8f
+c357e7aa757f23486271df284a5c3aa5643f2ff5
diff --git a/cg_renderer/src/cg_factory.cpp b/cg_renderer/src/cg_factory.cpp
index fc22574..8d4244a 100644
--- a/cg_renderer/src/cg_factory.cpp
+++ b/cg_renderer/src/cg_factory.cpp
@@ -81,7 +81,7 @@
     rgba[3] = colorAlpha(c) * kByteToUnit;
 }
 
-class CGRenderPath : public RenderPath
+class CGRenderPath : public lite_rtti_override<RenderPath, CGRenderPath>
 {
 private:
     AutoCF<CGMutablePathRef> m_path = CGPathCreateMutable();
@@ -156,7 +156,7 @@
     void close() override { CGPathCloseSubpath(m_path); }
 };
 
-class CGRenderShader : public RenderShader
+class CGRenderShader : public lite_rtti_override<RenderShader, CGRenderShader>
 {
 public:
     CGRenderShader() {}
@@ -167,7 +167,7 @@
     virtual void draw(CGContextRef) {}
 };
 
-class CGRenderPaint : public RenderPaint
+class CGRenderPaint : public lite_rtti_override<RenderPaint, CGRenderPaint>
 {
 private:
     bool m_isStroke = false;
@@ -176,7 +176,7 @@
     CGLineJoin m_join = kCGLineJoinMiter;
     CGLineCap m_cap = kCGLineCapButt;
     CGBlendMode m_blend = kCGBlendModeNormal;
-    rcp<RenderShader> m_shader;
+    rcp<CGRenderShader> m_shader;
 
 public:
     CGRenderPaint() {}
@@ -184,7 +184,7 @@
     bool isStroke() const { return m_isStroke; }
     float opacity() const { return m_rgba[3]; }
 
-    CGRenderShader* shader() const { return static_cast<CGRenderShader*>(m_shader.get()); }
+    CGRenderShader* shader() const { return m_shader.get(); }
 
     void apply(CGContextRef ctx)
     {
@@ -211,7 +211,10 @@
     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); }
+    void shader(rcp<RenderShader> sh) override
+    {
+        m_shader = lite_rtti_rcp_cast<CGRenderShader>(std::move(sh));
+    }
     void invalidateStroke() override {}
 };
 
@@ -299,7 +302,7 @@
     }
 };
 
-class CGRenderImage : public RenderImage
+class CGRenderImage : public lite_rtti_override<RenderImage, CGRenderImage>
 {
 public:
     AutoCF<CGImageRef> m_image;
@@ -319,11 +322,6 @@
     {
         CGContextConcatCTM(ctx, CGAffineTransformMake(1, 0, 0, -1, 0, (float)m_Height));
     }
-
-    static const CGRenderImage* Cast(const RenderImage* image)
-    {
-        return static_cast<const CGRenderImage*>(image);
-    }
 };
 
 //////////////////////////////////////////////////////////////////////////
@@ -348,8 +346,8 @@
 
 void CGRenderer::drawPath(RenderPath* path, RenderPaint* paint)
 {
-    auto cgpaint = static_cast<CGRenderPaint*>(paint);
-    auto cgpath = static_cast<CGRenderPath*>(path);
+    LITE_RTTI_CAST_OR_RETURN(cgpaint, CGRenderPaint*, paint);
+    LITE_RTTI_CAST_OR_RETURN(cgpath, CGRenderPath*, path);
 
     cgpaint->apply(m_ctx);
 
@@ -381,7 +379,7 @@
 
 void CGRenderer::clipPath(RenderPath* path)
 {
-    auto cgpath = static_cast<CGRenderPath*>(path);
+    LITE_RTTI_CAST_OR_RETURN(cgpath, CGRenderPath*, path);
 
     CGContextBeginPath(m_ctx);
     CGContextAddPath(m_ctx, cgpath->path());
@@ -390,12 +388,13 @@
 
 void CGRenderer::drawImage(const RenderImage* image, BlendMode blendMode, float opacity)
 {
+    LITE_RTTI_CAST_OR_RETURN(cgimg, const CGRenderImage*, image);
+
     auto bounds = CGRectMake(0, 0, image->width(), image->height());
 
     CGContextSaveGState(m_ctx);
     CGContextSetAlpha(m_ctx, opacity);
     CGContextSetBlendMode(m_ctx, convert(blendMode));
-    auto cgimg = CGRenderImage::Cast(image);
     cgimg->applyLocalMatrix(m_ctx);
     CGContextDrawImage(m_ctx, bounds, cgimg->m_image);
     CGContextRestoreGState(m_ctx);
@@ -417,7 +416,11 @@
                                BlendMode blendMode,
                                float opacity)
 {
-    auto cgimage = CGRenderImage::Cast(image);
+    LITE_RTTI_CAST_OR_RETURN(cgimage, const CGRenderImage*, image);
+    LITE_RTTI_CAST_OR_RETURN(cgindices, DataRenderBuffer*, indices.get());
+    LITE_RTTI_CAST_OR_RETURN(cgvertices, DataRenderBuffer*, vertices.get());
+    LITE_RTTI_CAST_OR_RETURN(cguvcoords, DataRenderBuffer*, uvCoords.get());
+
     auto const localMatrix = cgimage->localM2D();
 
     const float sx = image->width();
@@ -427,9 +430,9 @@
     auto scale = [sx, sy](Vec2D v) { return Vec2D{v.x * sx, v.y * sy}; };
 
     auto triangles = indexCount / 3;
-    auto ndx = DataRenderBuffer::Cast(indices.get())->u16s();
-    auto pts = DataRenderBuffer::Cast(vertices.get())->vecs();
-    auto uvs = DataRenderBuffer::Cast(uvCoords.get())->vecs();
+    auto ndx = cgindices->u16s();
+    auto pts = cgvertices->vecs();
+    auto uvs = cguvcoords->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
diff --git a/include/rive/renderer.hpp b/include/rive/renderer.hpp
index e595435..4b31825 100644
--- a/include/rive/renderer.hpp
+++ b/include/rive/renderer.hpp
@@ -16,6 +16,7 @@
 #include "rive/shapes/paint/blend_mode.hpp"
 #include "rive/shapes/paint/stroke_cap.hpp"
 #include "rive/shapes/paint/stroke_join.hpp"
+#include "utils/lite_rtti.hpp"
 
 #include <cmath>
 #include <stdio.h>
@@ -42,7 +43,7 @@
 };
 RIVE_MAKE_ENUM_BITSET(RenderBufferFlags)
 
-class RenderBuffer : public RefCnt<RenderBuffer>
+class RenderBuffer : public RefCnt<RenderBuffer>, public enable_lite_rtti<RenderBuffer>
 {
 public:
     RenderBuffer(RenderBufferType, RenderBufferFlags, size_t sizeInBytes);
@@ -81,14 +82,14 @@
  *  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<RenderShader>
+class RenderShader : public RefCnt<RenderShader>, public enable_lite_rtti<RenderShader>
 {
 public:
     RenderShader();
     virtual ~RenderShader();
 };
 
-class RenderPaint
+class RenderPaint : public enable_lite_rtti<RenderPaint>
 {
 public:
     RenderPaint();
@@ -104,7 +105,7 @@
     virtual void invalidateStroke() = 0;
 };
 
-class RenderImage : public RefCnt<RenderImage>
+class RenderImage : public RefCnt<RenderImage>, public enable_lite_rtti<RenderImage>
 {
 protected:
     int m_Width = 0;
@@ -121,7 +122,7 @@
     const Mat2D& uvTransform() const { return m_uvTransform; }
 };
 
-class RenderPath : public CommandPath
+class RenderPath : public CommandPath, public enable_lite_rtti<RenderPath>
 {
 public:
     RenderPath();
diff --git a/include/utils/factory_utils.hpp b/include/utils/factory_utils.hpp
index 5ad69c8..27ee8b0 100644
--- a/include/utils/factory_utils.hpp
+++ b/include/utils/factory_utils.hpp
@@ -12,11 +12,11 @@
 
 // Generic subclass of RenderBuffer that just stores the data on the cpu.
 //
-class DataRenderBuffer : public RenderBuffer
+class DataRenderBuffer : public lite_rtti_override<RenderBuffer, DataRenderBuffer>
 {
 public:
     DataRenderBuffer(RenderBufferType type, RenderBufferFlags flags, size_t sizeInBytes) :
-        RenderBuffer(type, flags, sizeInBytes)
+        lite_rtti_override(type, flags, sizeInBytes)
     {
         m_storage = malloc(sizeInBytes);
     }
@@ -29,11 +29,6 @@
 
     const Vec2D* vecs() const { return reinterpret_cast<const Vec2D*>(f32s()); }
 
-    static const DataRenderBuffer* Cast(const RenderBuffer* buffer)
-    {
-        return static_cast<const DataRenderBuffer*>(buffer);
-    }
-
 protected:
     void* onMap() override { return m_storage; }
     void onUnmap() override {}
diff --git a/include/utils/lite_rtti.hpp b/include/utils/lite_rtti.hpp
new file mode 100644
index 0000000..2c1c934
--- /dev/null
+++ b/include/utils/lite_rtti.hpp
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2023 Rive
+ */
+
+// "lite_rtti_cast<T*>()" is a very basic polyfill for "dynamic_cast<T*>()" that can only cast a
+// pointer to its most-derived type. To use it, the base class must derive from enable_lite_rtti,
+// and the subclass must inherit from lite_rtti_override:
+//
+//     class Root : public enable_lite_rtti<Root> {};
+//     class Derived : public lite_rtti_override<Root, Derived> {};
+//     Root* derived = new Derived();
+//     lite_rtti_cast<Derived*>(derived);
+//
+
+#pragma once
+
+#include "rive/refcnt.hpp"
+#include <stdint.h>
+#include <type_traits>
+
+namespace rive
+{
+// Derive type IDs based on the unique address of a static placeholder value.
+template <typename T>
+typename std::enable_if<!std::is_const<T>::value, uintptr_t>::type lite_type_id()
+{
+    static int placeholderForUniqueAddress;
+    return reinterpret_cast<uintptr_t>(&placeholderForUniqueAddress);
+}
+
+// Type IDs for const-qualified types should match their non-const counterparts.
+template <typename T>
+typename std::enable_if<std::is_const<T>::value, uintptr_t>::type lite_type_id()
+{
+    return lite_type_id<typename std::remove_const<T>::type>();
+}
+
+// Enable lite rtti on the root of a class hierarchy.
+template <class Root> class enable_lite_rtti
+{
+public:
+    uintptr_t liteTypeID() const { return m_liteTypeID; }
+
+protected:
+    uintptr_t m_liteTypeID = lite_type_id<Root>();
+};
+
+// Override the lite rtti type ID on subsequent classes of a class hierarchy.
+template <class Base, class Derived> class lite_rtti_override : public Base
+{
+public:
+    lite_rtti_override() { Base::m_liteTypeID = lite_type_id<Derived>(); }
+
+    template <typename... Args>
+    lite_rtti_override(Args&&... args) : Base(std::forward<Args>(args)...)
+    {
+        Base::m_liteTypeID = lite_type_id<Derived>();
+    }
+};
+
+// Like dynamic_cast<>, but can only cast a pointer to its most-derived type.
+template <class U, class T> U lite_rtti_cast(T* t)
+{
+    if (t != nullptr && t->liteTypeID() == lite_type_id<typename std::remove_pointer<U>::type>())
+    {
+        return static_cast<U>(t);
+    }
+    return nullptr;
+}
+
+template <class U, class T> rcp<U> lite_rtti_rcp_cast(rcp<T> t)
+{
+    if (t != nullptr && t->liteTypeID() == lite_type_id<U>())
+    {
+        return static_rcp_cast<U>(t);
+    }
+    return nullptr;
+}
+
+// Different versions of clang-format disagree on how to formate these.
+// clang-format off
+#define LITE_RTTI_CAST_OR_RETURN(NAME, TYPE, POINTER)                                              \
+    auto NAME = rive::lite_rtti_cast<TYPE>(POINTER);                                                     \
+    if (NAME == nullptr)                                                                           \
+        return
+
+#define LITE_RTTI_CAST_OR_BREAK(NAME, TYPE, POINTER)                                               \
+    auto NAME = rive::lite_rtti_cast<TYPE>(POINTER);                                                     \
+    if (NAME == nullptr)                                                                           \
+        break
+
+#define LITE_RTTI_CAST_OR_CONTINUE(NAME, TYPE, POINTER)                                            \
+    auto NAME = rive::lite_rtti_cast<TYPE>(POINTER);                                                     \
+    if (NAME == nullptr)                                                                           \
+        continue
+// clang-format on
+} // namespace rive
diff --git a/skia/renderer/src/skia_factory.cpp b/skia/renderer/src/skia_factory.cpp
index d3ac0d3..f0bc87d 100644
--- a/skia/renderer/src/skia_factory.cpp
+++ b/skia/renderer/src/skia_factory.cpp
@@ -24,7 +24,7 @@
 // skia's has/had bugs in trilerp, so backing down to nearest mip
 const SkSamplingOptions gSampling(SkFilterMode::kLinear, SkMipmapMode::kNearest);
 
-class SkiaRenderPath : public RenderPath
+class SkiaRenderPath : public lite_rtti_override<RenderPath, SkiaRenderPath>
 {
 private:
     SkPath m_Path;
@@ -44,7 +44,7 @@
     virtual void close() override;
 };
 
-class SkiaRenderPaint : public RenderPaint
+class SkiaRenderPaint : public lite_rtti_override<RenderPaint, SkiaRenderPaint>
 {
 private:
     SkPaint m_Paint;
@@ -64,7 +64,7 @@
     void invalidateStroke() override {}
 };
 
-class SkiaRenderImage : public RenderImage
+class SkiaRenderImage : public lite_rtti_override<RenderImage, SkiaRenderImage>
 {
 private:
     sk_sp<SkImage> m_SkImage;
@@ -75,7 +75,7 @@
     sk_sp<SkImage> skImage() const { return m_SkImage; }
 };
 
-class SkiaRenderShader : public RenderShader
+class SkiaRenderShader : public lite_rtti_override<RenderShader, SkiaRenderShader>
 {
 public:
     SkiaRenderShader(sk_sp<SkShader> sh) : shader(std::move(sh)) {}
@@ -88,7 +88,8 @@
 void SkiaRenderPath::rewind() { m_Path.rewind(); }
 void SkiaRenderPath::addRenderPath(RenderPath* path, const Mat2D& transform)
 {
-    m_Path.addPath(static_cast<SkiaRenderPath*>(path)->m_Path, ToSkia::convert(transform));
+    LITE_RTTI_CAST_OR_RETURN(skPath, SkiaRenderPath*, path);
+    m_Path.addPath(skPath->m_Path, ToSkia::convert(transform));
 }
 
 void SkiaRenderPath::moveTo(float x, float y) { m_Path.moveTo(x, y); }
@@ -122,7 +123,7 @@
 
 void SkiaRenderPaint::shader(rcp<RenderShader> rsh)
 {
-    SkiaRenderShader* sksh = (SkiaRenderShader*)rsh.get();
+    SkiaRenderShader* sksh = lite_rtti_cast<SkiaRenderShader*>(rsh.get());
     m_Paint.setShader(sksh ? sksh->shader : nullptr);
 }
 
@@ -134,21 +135,23 @@
 }
 void SkiaRenderer::drawPath(RenderPath* path, RenderPaint* paint)
 {
-    m_Canvas->drawPath(static_cast<SkiaRenderPath*>(path)->path(),
-                       static_cast<SkiaRenderPaint*>(paint)->paint());
+    LITE_RTTI_CAST_OR_RETURN(skPath, SkiaRenderPath*, path);
+    LITE_RTTI_CAST_OR_RETURN(skPaint, SkiaRenderPaint*, paint);
+    m_Canvas->drawPath(skPath->path(), skPaint->paint());
 }
 
 void SkiaRenderer::clipPath(RenderPath* path)
 {
-    m_Canvas->clipPath(static_cast<SkiaRenderPath*>(path)->path(), true);
+    LITE_RTTI_CAST_OR_RETURN(skPath, SkiaRenderPath*, path);
+    m_Canvas->clipPath(skPath->path(), true);
 }
 
 void SkiaRenderer::drawImage(const RenderImage* image, BlendMode blendMode, float opacity)
 {
+    LITE_RTTI_CAST_OR_RETURN(skiaImage, const SkiaRenderImage*, image);
     SkPaint paint;
     paint.setAlphaf(opacity);
     paint.setBlendMode(ToSkia::convert(blendMode));
-    auto skiaImage = static_cast<const SkiaRenderImage*>(image);
     m_Canvas->drawImage(skiaImage->skImage(), 0.0f, 0.0f, gSampling, &paint);
 }
 
@@ -163,6 +166,11 @@
                                  BlendMode blendMode,
                                  float opacity)
 {
+    LITE_RTTI_CAST_OR_RETURN(skImage, const SkiaRenderImage*, image);
+    LITE_RTTI_CAST_OR_RETURN(skVertices, DataRenderBuffer*, vertices.get());
+    LITE_RTTI_CAST_OR_RETURN(skUVCoords, DataRenderBuffer*, uvCoords.get());
+    LITE_RTTI_CAST_OR_RETURN(skIndices, DataRenderBuffer*, indices.get());
+
     // need our buffers and counts to agree
     assert(vertices->sizeInBytes() == vertexCount * sizeof(Vec2D));
     assert(uvCoords->sizeInBytes() == vertexCount * sizeof(Vec2D));
@@ -170,7 +178,7 @@
 
     SkMatrix scaleM;
 
-    auto uvs = (const SkPoint*)DataRenderBuffer::Cast(uvCoords.get())->vecs();
+    auto uvs = (const SkPoint*)skUVCoords->vecs();
 
 #ifdef SKIA_BUG_13047
     // The local matrix is ignored for drawVertices, so we have to manually scale
@@ -189,7 +197,7 @@
     scaleM = SkMatrix::Scale(2.0f / image->width(), 2.0f / image->height());
 #endif
 
-    auto skiaImage = static_cast<const SkiaRenderImage*>(image)->skImage();
+    auto skiaImage = skImage->skImage();
     auto shader = skiaImage->makeShader(SkTileMode::kClamp, SkTileMode::kClamp, gSampling, &scaleM);
 
     SkPaint paint;
@@ -201,11 +209,11 @@
     auto vertexMode = SkVertices::kTriangles_VertexMode;
     auto vt = SkVertices::MakeCopy(vertexMode,
                                    vertexCount,
-                                   (const SkPoint*)DataRenderBuffer::Cast(vertices.get())->vecs(),
+                                   (const SkPoint*)skVertices->vecs(),
                                    uvs,
                                    no_colors,
                                    indexCount,
-                                   DataRenderBuffer::Cast(indices.get())->u16s());
+                                   skIndices->u16s());
 
     // The blend mode is ignored if we don't have colors && uvs
     m_Canvas->drawVertices(vt, SkBlendMode::kModulate, paint);
diff --git a/tess/include/rive/tess/sokol/sokol_tess_renderer.hpp b/tess/include/rive/tess/sokol/sokol_tess_renderer.hpp
index 463bcc1..67454fa 100644
--- a/tess/include/rive/tess/sokol/sokol_tess_renderer.hpp
+++ b/tess/include/rive/tess/sokol/sokol_tess_renderer.hpp
@@ -27,7 +27,7 @@
 
 // The unique render image associated with a given source Rive asset. Can be stored in sub-region of
 // an actual graphics device image (SokolRenderImageResource).
-class SokolRenderImage : public RenderImage
+class SokolRenderImage : public lite_rtti_override<RenderImage, SokolRenderImage>
 {
 private:
     rcp<SokolRenderImageResource> m_gpuImage;
diff --git a/tess/include/rive/tess/tess_render_path.hpp b/tess/include/rive/tess/tess_render_path.hpp
index 3e97bad..b374c75 100644
--- a/tess/include/rive/tess/tess_render_path.hpp
+++ b/tess/include/rive/tess/tess_render_path.hpp
@@ -12,7 +12,7 @@
 {
 
 class ContourStroke;
-class TessRenderPath : public RenderPath
+class TessRenderPath : public lite_rtti_override<RenderPath, TessRenderPath>
 {
 private:
     // TessRenderPath stores a RawPath so that it can use utility classes
diff --git a/tess/src/sokol/sokol_factory.cpp b/tess/src/sokol/sokol_factory.cpp
index 51b0192..00904ab 100644
--- a/tess/src/sokol/sokol_factory.cpp
+++ b/tess/src/sokol/sokol_factory.cpp
@@ -2,7 +2,7 @@
 
 using namespace rive;
 
-class NoOpRenderPaint : public RenderPaint
+class NoOpRenderPaint : public lite_rtti_override<RenderPaint, NoOpRenderPaint>
 {
 public:
     void color(unsigned int value) override {}
@@ -15,7 +15,7 @@
     void invalidateStroke() override {}
 };
 
-class NoOpRenderPath : public RenderPath
+class NoOpRenderPath : public lite_rtti_override<RenderPath, NoOpRenderPath>
 {
 public:
     void rewind() override {}
diff --git a/tess/src/sokol/sokol_tess_renderer.cpp b/tess/src/sokol/sokol_tess_renderer.cpp
index b7fb081..6dc792e 100644
--- a/tess/src/sokol/sokol_tess_renderer.cpp
+++ b/tess/src/sokol/sokol_tess_renderer.cpp
@@ -15,11 +15,11 @@
     buffer[3] = colorOpacity(value);
 }
 
-class SokolRenderPath : public TessRenderPath
+class SokolRenderPath : public lite_rtti_override<TessRenderPath, SokolRenderPath>
 {
 public:
     SokolRenderPath() {}
-    SokolRenderPath(RawPath& rawPath, FillRule fillRule) : TessRenderPath(rawPath, fillRule) {}
+    SokolRenderPath(RawPath& rawPath, FillRule fillRule) : lite_rtti_override(rawPath, fillRule) {}
 
     ~SokolRenderPath()
     {
@@ -66,7 +66,8 @@
         {
             for (auto& subPath : m_subPaths)
             {
-                static_cast<SokolRenderPath*>(subPath.path())->drawStroke(stroke);
+                LITE_RTTI_CAST_OR_CONTINUE(sokolPath, SokolRenderPath*, subPath.path());
+                sokolPath->drawStroke(stroke);
             }
             return;
         }
@@ -149,11 +150,12 @@
     return rivestd::make_unique<SokolRenderPath>();
 }
 
-class SokolBuffer : public RenderBuffer
+class SokolBuffer : public lite_rtti_override<RenderBuffer, SokolBuffer>
 {
 public:
     SokolBuffer(RenderBufferType type, RenderBufferFlags renderBufferFlags, size_t sizeInBytes) :
-        RenderBuffer(type, renderBufferFlags, sizeInBytes), m_mappedMemory(new char[sizeInBytes])
+        lite_rtti_override(type, renderBufferFlags, sizeInBytes),
+        m_mappedMemory(new char[sizeInBytes])
     {
         // If the buffer will be immutable, defer creation until the client unmaps for the only time
         // and we have our initial data.
@@ -581,12 +583,13 @@
 
 void SokolTessRenderer::drawImage(const RenderImage* image, BlendMode, float opacity)
 {
+    LITE_RTTI_CAST_OR_RETURN(sokolImage, const SokolRenderImage*, image);
+
     vs_params_t vs_params;
 
     const Mat2D& world = transform();
     vs_params.mvp = m_Projection * world;
 
-    auto sokolImage = static_cast<const SokolRenderImage*>(image);
     setPipeline(m_meshPipeline);
     sg_bindings bind = {
         .vertex_buffers[0] = sokolImage->vertexBuffer(),
@@ -609,6 +612,11 @@
                                       BlendMode blendMode,
                                       float opacity)
 {
+    LITE_RTTI_CAST_OR_RETURN(sokolVertices, SokolBuffer*, vertices_f32.get());
+    LITE_RTTI_CAST_OR_RETURN(sokolUVCoords, SokolBuffer*, uvCoords_f32.get());
+    LITE_RTTI_CAST_OR_RETURN(sokolIndices, SokolBuffer*, indices_u16.get());
+    LITE_RTTI_CAST_OR_RETURN(sokolRenderImage, const SokolRenderImage*, renderImage);
+
     vs_params_t vs_params;
 
     const Mat2D& world = transform();
@@ -616,10 +624,10 @@
 
     setPipeline(m_meshPipeline);
     sg_bindings bind = {
-        .vertex_buffers[0] = static_cast<SokolBuffer*>(vertices_f32.get())->buffer(),
-        .vertex_buffers[1] = static_cast<SokolBuffer*>(uvCoords_f32.get())->buffer(),
-        .index_buffer = static_cast<SokolBuffer*>(indices_u16.get())->buffer(),
-        .fs_images[SLOT_tex] = static_cast<const SokolRenderImage*>(renderImage)->image(),
+        .vertex_buffers[0] = sokolVertices->buffer(),
+        .vertex_buffers[1] = sokolUVCoords->buffer(),
+        .index_buffer = sokolIndices->buffer(),
+        .fs_images[SLOT_tex] = sokolRenderImage->image(),
     };
 
     sg_apply_bindings(&bind);
@@ -627,7 +635,7 @@
     sg_draw(0, indexCount, 1);
 }
 
-class SokolGradient : public RenderShader
+class SokolGradient : public lite_rtti_override<RenderShader, SokolGradient>
 {
 private:
     Vec2D m_start;
@@ -722,11 +730,11 @@
     return rcp<RenderShader>(new SokolGradient(cx, cy, radius, colors, stops, count));
 }
 
-class SokolRenderPaint : public RenderPaint
+class SokolRenderPaint : public lite_rtti_override<RenderPaint, SokolRenderPaint>
 {
 private:
     fs_path_uniforms_t m_uniforms = {0};
-    rcp<RenderShader> m_shader;
+    rcp<SokolGradient> m_shader;
     RenderPaintStyle m_style;
     std::unique_ptr<ContourStroke> m_stroke;
     bool m_strokeDirty = false;
@@ -801,13 +809,16 @@
     void blendMode(BlendMode value) override { m_blendMode = value; }
     BlendMode blendMode() const { return m_blendMode; }
 
-    void shader(rcp<RenderShader> shader) override { m_shader = shader; }
+    void shader(rcp<RenderShader> shader) override
+    {
+        m_shader = lite_rtti_rcp_cast<SokolGradient>(std::move(shader));
+    }
 
     void draw(vs_path_params_t& vertexUniforms, SokolRenderPath* path)
     {
         if (m_shader)
         {
-            static_cast<SokolGradient*>(m_shader.get())->bind(vertexUniforms, m_uniforms);
+            m_shader->bind(vertexUniforms, m_uniforms);
         }
 
         sg_apply_uniforms(SG_SHADERSTAGE_VS, SLOT_vs_path_params, SG_RANGE_REF(vertexUniforms));
@@ -984,11 +995,11 @@
         if (decr)
         {
             // Draw appliedPath.path() with decr pipeline
+            LITE_RTTI_CAST_OR_CONTINUE(sokolPath, SokolRenderPath*, appliedPath.path());
             setPipeline(m_decClipPipeline);
             vs_params.mvp = m_Projection * appliedPath.transform();
             sg_apply_uniforms(SG_SHADERSTAGE_VS, SLOT_vs_path_params, SG_RANGE_REF(vs_params));
             sg_apply_uniforms(SG_SHADERSTAGE_FS, SLOT_fs_path_uniforms, SG_RANGE_REF(uniforms));
-            auto sokolPath = static_cast<SokolRenderPath*>(appliedPath.path());
             sokolPath->drawFill();
         }
     }
@@ -1002,11 +1013,11 @@
             continue;
         }
         // Draw nextClipPath.path() with incr pipeline
+        LITE_RTTI_CAST_OR_CONTINUE(sokolPath, SokolRenderPath*, nextClipPath.path());
         setPipeline(m_incClipPipeline);
         vs_params.mvp = m_Projection * nextClipPath.transform();
         sg_apply_uniforms(SG_SHADERSTAGE_VS, SLOT_vs_path_params, SG_RANGE_REF(vs_params));
         sg_apply_uniforms(SG_SHADERSTAGE_FS, SLOT_fs_path_uniforms, SG_RANGE_REF(uniforms));
-        auto sokolPath = static_cast<SokolRenderPath*>(nextClipPath.path());
         sokolPath->drawFill();
     }
 
@@ -1030,7 +1041,8 @@
 
 void SokolTessRenderer::drawPath(RenderPath* path, RenderPaint* paint)
 {
-    auto sokolPaint = static_cast<SokolRenderPaint*>(paint);
+    LITE_RTTI_CAST_OR_RETURN(sokolPath, SokolRenderPath*, path);
+    LITE_RTTI_CAST_OR_RETURN(sokolPaint, SokolRenderPaint*, paint);
 
     applyClipping();
     vs_path_params_t vs_params = {.fillType = 0};
@@ -1056,7 +1068,7 @@
             break;
     }
 
-    static_cast<SokolRenderPaint*>(paint)->draw(vs_params, static_cast<SokolRenderPath*>(path));
+    sokolPaint->draw(vs_params, sokolPath);
 }
 
 SokolRenderImageResource::SokolRenderImageResource(const uint8_t* bytes,
@@ -1076,7 +1088,7 @@
                                    uint32_t height,
                                    const Mat2D& uvTransform) :
 
-    RenderImage(uvTransform), m_gpuImage(image)
+    lite_rtti_override(uvTransform), m_gpuImage(image)
 
 {
     float halfWidth = width / 2.0f;
diff --git a/test/lite_rtti_test.cpp b/test/lite_rtti_test.cpp
new file mode 100644
index 0000000..d29d39f
--- /dev/null
+++ b/test/lite_rtti_test.cpp
@@ -0,0 +1,86 @@
+#include "utils/lite_rtti.hpp"
+#include <catch.hpp>
+
+using namespace rive;
+
+TEST_CASE("lite rtti behaves correctly", "[lite_rtti]")
+{
+    class A : public enable_lite_rtti<A>
+    {};
+    A a;
+    CHECK(lite_type_id<A>() == lite_type_id<A>());
+    CHECK(lite_type_id<const A>() == lite_type_id<A>());
+    CHECK(lite_type_id<const A>() == a.liteTypeID());
+
+    class B : public lite_rtti_override<A, B>
+    {};
+    B b;
+    CHECK(lite_type_id<B>() != lite_type_id<A>());
+    CHECK(b.liteTypeID() != a.liteTypeID());
+    CHECK(b.liteTypeID() == lite_type_id<const B>());
+
+    class C : public lite_rtti_override<B, C>
+    {};
+    const C c;
+    CHECK(lite_type_id<C>() != lite_type_id<A>());
+    CHECK(lite_type_id<C>() != lite_type_id<B>());
+    CHECK(c.liteTypeID() != a.liteTypeID());
+    CHECK(c.liteTypeID() != b.liteTypeID());
+    CHECK(c.liteTypeID() == lite_type_id<C>());
+
+    A* pA = &a;
+    A* pA_b = &b;
+    const A* pA_c = &c;
+
+    CHECK(lite_rtti_cast<B*>(pA) == nullptr);
+    CHECK(lite_rtti_cast<const C*>(pA) == nullptr);
+
+    CHECK(lite_rtti_cast<const B*>(pA_b) == &b);
+    CHECK(lite_rtti_cast<C*>(pA_b) == nullptr);
+
+    CHECK(lite_rtti_cast<const B*>(pA_c) == nullptr);
+    CHECK(lite_rtti_cast<C*>(const_cast<A*>(pA_c)) == &c);
+
+    const B* pB_c = &c;
+    CHECK(lite_rtti_cast<B*>(const_cast<B*>(pB_c)) == nullptr);
+    CHECK(lite_rtti_cast<const C*>(pB_c) == &c);
+
+    A* nil = nullptr;
+    CHECK(lite_rtti_cast<B*>(nil) == nullptr);
+    CHECK(lite_rtti_cast<const C*>(nil) == nullptr);
+
+    // Check constructor arguments.
+    struct D : public enable_lite_rtti<D>
+    {
+        D() = delete;
+        D(float x_, int y_) : x(x_), y(y_) {}
+        float x;
+        int y;
+    };
+
+    struct E : public lite_rtti_override<D, E>
+    {
+        E(float x_, int y_) : lite_rtti_override(x_, y_) {}
+    };
+
+    D* pD_e = new E(4.5f, 6);
+    E* pE = lite_rtti_cast<E*>(pD_e);
+    CHECK(pD_e->liteTypeID() == lite_type_id<const E>());
+    REQUIRE(pE != nullptr);
+    CHECK(pE->x == 4.5f);
+    CHECK(pE->y == 6);
+    delete pD_e;
+
+    // Check rcp
+    class F : public RefCnt<F>, public enable_lite_rtti<F>
+    {};
+    class G : public lite_rtti_override<F, G>
+    {};
+    class H : public lite_rtti_override<F, H>
+    {};
+    rcp<F> pF_g = make_rcp<G>();
+    rcp<G> pG = lite_rtti_rcp_cast<G>(pF_g);
+    rcp<H> pH = lite_rtti_rcp_cast<H>(pF_g);
+    CHECK(pG != nullptr);
+    CHECK(pH == nullptr);
+}
diff --git a/viewer/src/sample_tools/sample_atlas_packer.cpp b/viewer/src/sample_tools/sample_atlas_packer.cpp
index b250b34..8453615 100644
--- a/viewer/src/sample_tools/sample_atlas_packer.cpp
+++ b/viewer/src/sample_tools/sample_atlas_packer.cpp
@@ -9,7 +9,7 @@
 
 using namespace rive;
 
-class AtlasRenderImage : public RenderImage
+class AtlasRenderImage : public lite_rtti_override<RenderImage, AtlasRenderImage>
 {
 private:
     std::vector<uint8_t> m_Pixels;
@@ -62,7 +62,9 @@
                 Mat2D uvTransform;
 
                 auto imageAsset = asset->as<ImageAsset>();
-                auto renderImage = static_cast<AtlasRenderImage*>(imageAsset->renderImage());
+                LITE_RTTI_CAST_OR_CONTINUE(renderImage,
+                                           AtlasRenderImage*,
+                                           imageAsset->renderImage());
 
                 if (m_atlases.empty())
                 {