Merge rive-cpp into rive-runtime
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..5e7b8bd
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,74 @@
+# Prerequisites
+*.d
+
+# Compiled Object files
+*.slo
+*.lo
+*.o
+*.obj
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Compiled Dynamic libraries
+*.so
+*.dylib
+*.dll
+
+# Fortran module files
+*.mod
+*.smod
+
+# Compiled Static libraries
+*.lai
+*.la
+*.a
+*.lib
+
+# Executables
+*.exe
+*.out
+*.app
+
+# test builds
+dev/test/build/bin/*
+
+# aot snapshots
+dev/bin/*
+
+# Generated make files
+Makefile
+*.make
+
+# Dart stuff
+pubspec.lock
+.packages
+.dart_tool
+
+# OSX stuff
+*.DS_Store
+
+# Generated docs
+docs
+
+# Analysis results
+dev/analysis_report
+
+# Build directories
+build/bin
+
+# Skia dependencies
+skia/dependencies/skia_recorder
+skia/dependencies/skia
+skia/dependencies/skia_rive_optimized
+skia/dependencies/glfw_build
+skia/dependencies/FFmpeg
+skia/dependencies/x264
+skia/renderer/build/bin
+skia/**/build/bin
+/skia/dependencies/glfw
+
+
+**/build/obj
+**/build/bin
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..286469c
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,77 @@
+name: Tests
+
+on:
+  push:
+
+jobs:
+  build-linux:
+    runs-on: ubuntu-latest
+
+    strategy:
+      matrix:
+        platform: [linux]
+
+    steps:
+      - uses: actions/checkout@v2
+      - name: Install
+        run: |
+          wget -q https://github.com/premake/premake-core/releases/download/v5.0.0-alpha16/premake-5.0.0-alpha16-linux.tar.gz
+          tar -xf premake-5.0.0-alpha16-linux.tar.gz
+          sudo chmod a+x premake5
+          sudo mv premake5 /usr/local/bin
+
+      - name: Build
+        run: |
+          ./build.sh clean
+          ./build.sh
+          ./build.sh release
+
+      - name: Tests
+        run: |
+          cd dev
+          ./test.sh
+
+  build-windows:
+    runs-on: windows-2022
+    steps:
+      - uses: actions/checkout@v2
+      - name: Tests
+        run: |
+          cd dev
+          ./test.sh
+
+  build-macos:
+    runs-on: macOS-latest
+
+    strategy:
+      matrix:
+        platform: [macOS]
+
+    steps:
+      - uses: actions/checkout@v2
+
+      - name: Install
+        run: |
+          wget https://github.com/premake/premake-core/releases/download/v5.0.0-alpha16/premake-5.0.0-alpha16-macosx.tar.gz
+          tar -xvf premake-5.0.0-alpha16-macosx.tar.gz
+          sudo chmod a+x premake5
+          sudo mv premake5 /usr/local/bin
+
+      - name: Build
+        run: |
+          ./build.sh
+          ./build.sh release
+
+      - name: Tests
+        if: matrix.platform == 'macOS'
+        run: |
+          echo Testing for ${{matrix.platform}}
+          cd dev
+          ./test.sh
+
+      - name: Tess Tests
+        if: matrix.platform == 'macOS'
+        run: |
+          echo Testing for ${{matrix.platform}}
+          cd tess/build/macosx
+          ./build_tess.sh test
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..91cb7de
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,92 @@
+# Prerequisites
+*.d
+
+# Compiled Object files
+*.slo
+*.lo
+*.o
+*.obj
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Compiled Dynamic libraries
+*.so
+*.dylib
+*.dll
+
+# Fortran module files
+*.mod
+*.smod
+
+# Compiled Static libraries
+*.lai
+*.la
+*.a
+*.lib
+
+# Executables
+*.exe
+*.out
+*.app
+
+# aot snapshots
+dev/bin/*
+
+# Generated make files
+Makefile
+*.make
+
+# Dart stuff
+pubspec.lock
+.packages
+.dart_tool
+
+# OSX stuff
+*.DS_Store
+
+# Generated docs
+docs
+
+# Analysis results
+dev/analysis_report
+
+# Build directories
+build/bin
+dev/test/build/bin
+rivinfo/build/macosx
+
+# Skia dependencies
+skia/dependencies/skia_recorder
+skia/dependencies/skia
+skia/dependencies/skia_full
+skia/dependencies/glfw_build
+skia/dependencies/FFmpeg
+skia/dependencies/x264
+skia/renderer/build/bin
+skia/renderer/build/obj
+skia/renderer/build/macosx
+skia/renderer/build/android
+skia/**/build/bin
+skia/**/build/macosx
+skia/**/build/android
+/skia/dependencies/glfw
+/skia/dependencies/gl3w
+/skia/dependencies/imgui
+/skia/dependencies/libzip
+/skia/dependencies/libzip_build
+/skia/viewer/imgui.ini
+/skia/viewer/build/macosx
+/skia/thumbnail_generator/build/macosx
+/skia/thumbnail_generator/build/dependencies
+/viewer/build/macosx
+/build/macosx
+/build/android
+/skia/dependencies/skia_rive_optimized
+/skia/dependencies/skia_debug
+/skia/dependencies/skia-experimental
+dependencies/windows/cache
+
+# Local development setup
+compile_commands.json
diff --git a/.lua-format b/.lua-format
new file mode 100644
index 0000000..9a55042
--- /dev/null
+++ b/.lua-format
@@ -0,0 +1,32 @@
+column_limit: 80
+indent_width: 4
+use_tab: false
+tab_width: 4
+continuation_indent_width: 4
+spaces_before_call: 1
+keep_simple_control_block_one_line: true
+keep_simple_function_one_line: true
+align_args: true
+break_after_functioncall_lp: false
+break_before_functioncall_rp: false
+spaces_inside_functioncall_parens: false
+spaces_inside_functiondef_parens: false
+align_parameter: true
+chop_down_parameter: false
+break_after_functiondef_lp: false
+break_before_functiondef_rp: false
+align_table_field: true
+break_after_table_lb: true
+break_before_table_rb: true
+chop_down_table: true
+chop_down_kv_table: true
+table_sep: ","
+column_table_limit: column_limit
+extra_sep_at_table_end: false
+spaces_inside_table_braces: false
+break_after_operator: true
+double_quote_to_single_quote: false
+single_quote_to_double_quote: false
+spaces_around_equals_in_field: true
+line_breaks_after_function_body: 1
+line_separator: input
diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json
new file mode 100644
index 0000000..c74f34f
--- /dev/null
+++ b/.vscode/c_cpp_properties.json
@@ -0,0 +1,19 @@
+{
+    "configurations": [
+        {
+            "name": "Mac",
+            "includePath": [
+                "${workspaceFolder}/**",
+                "${workspaceFolder}/dev/test/include",
+                "${workspaceFolder}/include"
+            ],
+            "defines": [],
+            "macFrameworkPath": [],
+            "compilerPath": "/usr/local/bin/arm-none-eabi-gcc",
+            "cStandard": "gnu11",
+            "cppStandard": "gnu++14",
+            "intelliSenseMode": "clang-x64"
+        }
+    ],
+    "version": 4
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..3cd954c
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,109 @@
+{
+    "disasexpl.associations": {
+        "**/*.c": "${workspaceFolder}/build/bin/assembly/${fileDirname}/${fileBasenameNoExtension}.S",
+        "**/*.cpp": "${workspaceFolder}/build/bin/assembly/${fileDirname}/${fileBasenameNoExtension}.S"
+    },
+    "files.associations": {
+        "type_traits": "cpp",
+        "string": "cpp",
+        "bitset": "cpp",
+        "charconv": "cpp",
+        "__bit_reference": "cpp",
+        "__config": "cpp",
+        "__debug": "cpp",
+        "__errc": "cpp",
+        "__functional_base": "cpp",
+        "__hash_table": "cpp",
+        "__locale": "cpp",
+        "__mutex_base": "cpp",
+        "__node_handle": "cpp",
+        "__nullptr": "cpp",
+        "__split_buffer": "cpp",
+        "__sso_allocator": "cpp",
+        "__std_stream": "cpp",
+        "__string": "cpp",
+        "__threading_support": "cpp",
+        "__tree": "cpp",
+        "__tuple": "cpp",
+        "algorithm": "cpp",
+        "any": "cpp",
+        "array": "cpp",
+        "atomic": "cpp",
+        "bit": "cpp",
+        "cctype": "cpp",
+        "chrono": "cpp",
+        "cinttypes": "cpp",
+        "clocale": "cpp",
+        "cmath": "cpp",
+        "codecvt": "cpp",
+        "complex": "cpp",
+        "condition_variable": "cpp",
+        "csignal": "cpp",
+        "cstdarg": "cpp",
+        "cstddef": "cpp",
+        "cstdint": "cpp",
+        "cstdio": "cpp",
+        "cstdlib": "cpp",
+        "cstring": "cpp",
+        "ctime": "cpp",
+        "cwchar": "cpp",
+        "cwctype": "cpp",
+        "deque": "cpp",
+        "exception": "cpp",
+        "forward_list": "cpp",
+        "fstream": "cpp",
+        "functional": "cpp",
+        "future": "cpp",
+        "initializer_list": "cpp",
+        "iomanip": "cpp",
+        "ios": "cpp",
+        "iosfwd": "cpp",
+        "iostream": "cpp",
+        "istream": "cpp",
+        "iterator": "cpp",
+        "limits": "cpp",
+        "list": "cpp",
+        "locale": "cpp",
+        "map": "cpp",
+        "memory": "cpp",
+        "mutex": "cpp",
+        "new": "cpp",
+        "numeric": "cpp",
+        "optional": "cpp",
+        "ostream": "cpp",
+        "queue": "cpp",
+        "random": "cpp",
+        "ratio": "cpp",
+        "regex": "cpp",
+        "set": "cpp",
+        "shared_mutex": "cpp",
+        "sstream": "cpp",
+        "stack": "cpp",
+        "stdexcept": "cpp",
+        "streambuf": "cpp",
+        "string_view": "cpp",
+        "strstream": "cpp",
+        "system_error": "cpp",
+        "thread": "cpp",
+        "tuple": "cpp",
+        "typeinfo": "cpp",
+        "unordered_map": "cpp",
+        "unordered_set": "cpp",
+        "utility": "cpp",
+        "valarray": "cpp",
+        "variant": "cpp",
+        "vector": "cpp",
+        "*.ipp": "cpp",
+        "*.inc": "cpp",
+        "__bits": "cpp",
+        "cfenv": "cpp",
+        "compare": "cpp",
+        "concepts": "cpp",
+        "scoped_allocator": "cpp",
+        "span": "cpp",
+        "typeindex": "cpp",
+        "filesystem": "cpp",
+        "*.idl": "cpp"
+    },
+    "git.ignoreLimitWarning": true
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..c62cb09
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,36 @@
+{
+    // See https://go.microsoft.com/fwlink/?LinkId=733558
+    // for the documentation about the tasks.json format
+    "version": "2.0.0",
+    "tasks": [{
+            "label": "run tests",
+            "type": "shell",
+            "command": "cd dev && ./test.sh",
+            "group": "test",
+            "presentation": {
+                "reveal": "always",
+                "panel": "new"
+            }
+        },
+        {
+            "label": "gen assembly",
+            "type": "shell",
+            "command": "mkdir -p ${workspaceFolder}/build/bin/assembly/${fileDirname}/ && clang++ -O3 -g -std=c++17 -o ${workspaceFolder}/build/bin/assembly/${fileDirname}/${fileBasenameNoExtension}.S -Wall -fno-exceptions -fno-rtti -I${workspaceFolder}/include -S ${file}",
+            "group": "test",
+            "presentation": {
+                "reveal": "silent",
+                "panel": "new"
+            }
+        },
+        {
+            "label": "disassemble",
+            "command": "${command:disasexpl.show}",
+            "dependsOn": ["gen assembly"],
+            "problemMatcher": [],
+            "presentation": {
+                "reveal": "silent",
+                "panel": "new"
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..e3e6d18
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,28 @@
+FROM dart:stable
+
+RUN apt update && apt-get -y install unzip zip clang cmake ninja-build pkg-config libgtk-3-dev xvfb cargo wget g++
+
+WORKDIR /
+RUN wget -q https://github.com/premake/premake-core/releases/download/v5.0.0-beta2/premake-5.0.0-beta2-linux.tar.gz
+RUN tar -xf premake-5.0.0-beta2-linux.tar.gz
+RUN mv premake5 /usr/bin/
+
+ENV LDFLAGS="-pthreads"
+ENV CC=/usr/bin/clang
+ENV CXX=/usr/bin/clang++
+
+# ADD skia/dependencies/make_dependencies.sh /app/skia/dependencies/make_dependencies.sh
+ADD skia/dependencies/make_skia.sh /app/skia/dependencies/make_skia.sh
+ADD skia/dependencies/make_glfw.sh /app/skia/dependencies/make_glfw.sh
+WORKDIR /app/skia/dependencies
+# RUN /app/skia/dependencies/make_dependencies.sh
+RUN /app/skia/dependencies/make_skia.sh
+RUN /app/skia/dependencies/make_glfw.sh
+
+WORKDIR /app/packages/peon_process
+ADD rive /app/rive
+ADD skia /app/skia
+WORKDIR /app/skia/thumbnail_generator
+
+RUN /app/skia/thumbnail_generator/build.sh clean
+RUN /app/skia/thumbnail_generator/build.sh
diff --git a/Doxyfile b/Doxyfile
new file mode 100644
index 0000000..4912ac2
--- /dev/null
+++ b/Doxyfile
@@ -0,0 +1,2553 @@
+# Doxyfile 1.8.18
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project.
+#
+# All text after a double hash (##) is considered a comment and is placed in
+# front of the TAG it is preceding.
+#
+# All text after a single hash (#) is considered a comment and will be ignored.
+# The format is:
+# TAG = value [value, ...]
+# For lists, items can also be appended using:
+# TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (\" \").
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# This tag specifies the encoding used for all characters in the configuration
+# file that follow. The default is UTF-8 which is also the encoding used for all
+# text before the first occurrence of this tag. Doxygen uses libiconv (or the
+# iconv built into libc) for the transcoding. See
+# https://www.gnu.org/software/libiconv/ for the list of possible encodings.
+# The default value is: UTF-8.
+
+DOXYFILE_ENCODING      = UTF-8
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by
+# double-quotes, unless you are using Doxywizard) that should identify the
+# project for which the documentation is generated. This name is used in the
+# title of most generated pages and in a few other places.
+# The default value is: My Project.
+
+PROJECT_NAME           = "Rive C++ Runtime"
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
+# could be handy for archiving the generated documentation or if some version
+# control system is used.
+
+PROJECT_NUMBER         = 1
+
+# Using the PROJECT_BRIEF tag one can provide an optional one line description
+# for a project that appears at the top of each page and should give viewer a
+# quick idea about the purpose of the project. Keep the description short.
+
+PROJECT_BRIEF          = "C++ implementation of the Rive runtime"
+
+# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
+# in the documentation. The maximum height of the logo should not exceed 55
+# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
+# the logo to the output directory.
+
+PROJECT_LOGO           =
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
+# into which the generated documentation will be written. If a relative path is
+# entered, it will be relative to the location where doxygen was started. If
+# left blank the current directory will be used.
+
+OUTPUT_DIRECTORY       = "docs"
+
+# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
+# directories (in 2 levels) under the output directory of each output format and
+# will distribute the generated files over these directories. Enabling this
+# option can be useful when feeding doxygen a huge amount of source files, where
+# putting all generated files in the same directory would otherwise causes
+# performance problems for the file system.
+# The default value is: NO.
+
+CREATE_SUBDIRS         = NO
+
+# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
+# characters to appear in the names of generated files. If set to NO, non-ASCII
+# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
+# U+3044.
+# The default value is: NO.
+
+ALLOW_UNICODE_NAMES    = NO
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,
+# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),
+# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,
+# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),
+# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,
+# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,
+# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,
+# Ukrainian and Vietnamese.
+# The default value is: English.
+
+OUTPUT_LANGUAGE        = English
+
+# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all generated output in the proper direction.
+# Possible values are: None, LTR, RTL and Context.
+# The default value is: None.
+
+OUTPUT_TEXT_DIRECTION  = None
+
+# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
+# descriptions after the members that are listed in the file and class
+# documentation (similar to Javadoc). Set to NO to disable this.
+# The default value is: YES.
+
+BRIEF_MEMBER_DESC      = YES
+
+# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief
+# description of a member or function before the detailed description
+#
+# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+# The default value is: YES.
+
+REPEAT_BRIEF           = YES
+
+# This tag implements a quasi-intelligent brief description abbreviator that is
+# used to form the text in various listings. Each string in this list, if found
+# as the leading text of the brief description, will be stripped from the text
+# and the result, after processing the whole list, is used as the annotated
+# text. Otherwise, the brief description is used as-is. If left blank, the
+# following values are used ($name is automatically replaced with the name of
+# the entity):The $name class, The $name widget, The $name file, is, provides,
+# specifies, contains, represents, a, an and the.
+
+ABBREVIATE_BRIEF       = "The $name class" \
+                         "The $name widget" \
+                         "The $name file" \
+                         is \
+                         provides \
+                         specifies \
+                         contains \
+                         represents \
+                         a \
+                         an \
+                         the
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# doxygen will generate a detailed section even if there is only a brief
+# description.
+# The default value is: NO.
+
+ALWAYS_DETAILED_SEC    = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+# The default value is: NO.
+
+INLINE_INHERITED_MEMB  = NO
+
+# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path
+# before files name in the file list and in the header files. If set to NO the
+# shortest path that makes the file name unique will be used
+# The default value is: YES.
+
+FULL_PATH_NAMES        = YES
+
+# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.
+# Stripping is only done if one of the specified strings matches the left-hand
+# part of the path. The tag can be used to show relative paths in the file list.
+# If left blank the directory from which doxygen is run is used as the path to
+# strip.
+#
+# Note that you can specify absolute paths here, but also relative paths, which
+# will be relative from the directory where doxygen is started.
+# This tag requires that the tag FULL_PATH_NAMES is set to YES.
+
+STRIP_FROM_PATH        =
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the
+# path mentioned in the documentation of a class, which tells the reader which
+# header file to include in order to use a class. If left blank only the name of
+# the header file containing the class definition is used. Otherwise one should
+# specify the list of include paths that are normally passed to the compiler
+# using the -I flag.
+
+STRIP_FROM_INC_PATH    =
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but
+# less readable) file names. This can be useful is your file systems doesn't
+# support long names like on DOS, Mac, or CD-ROM.
+# The default value is: NO.
+
+SHORT_NAMES            = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the
+# first line (until the first dot) of a Javadoc-style comment as the brief
+# description. If set to NO, the Javadoc-style will behave just like regular Qt-
+# style comments (thus requiring an explicit @brief command for a brief
+# description.)
+# The default value is: NO.
+
+JAVADOC_AUTOBRIEF      = NO
+
+# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line
+# such as
+# /***************
+# as being the beginning of a Javadoc-style comment "banner". If set to NO, the
+# Javadoc-style will behave just like regular comments and it will not be
+# interpreted by doxygen.
+# The default value is: NO.
+
+JAVADOC_BANNER         = NO
+
+# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first
+# line (until the first dot) of a Qt-style comment as the brief description. If
+# set to NO, the Qt-style will behave just like regular Qt-style comments (thus
+# requiring an explicit \brief command for a brief description.)
+# The default value is: NO.
+
+QT_AUTOBRIEF           = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a
+# multi-line C++ special comment block (i.e. a block of //! or /// comments) as
+# a brief description. This used to be the default behavior. The new default is
+# to treat a multi-line C++ comment block as a detailed description. Set this
+# tag to YES if you prefer the old behavior instead.
+#
+# Note that setting this tag to YES also means that rational rose comments are
+# not recognized any more.
+# The default value is: NO.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
+# documentation from any documented member that it re-implements.
+# The default value is: YES.
+
+INHERIT_DOCS           = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new
+# page for each member. If set to NO, the documentation of a member will be part
+# of the file/class/namespace that contains it.
+# The default value is: NO.
+
+SEPARATE_MEMBER_PAGES  = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen
+# uses this value to replace tabs by spaces in code fragments.
+# Minimum value: 1, maximum value: 16, default value: 4.
+
+TAB_SIZE               = 4
+
+# This tag can be used to specify a number of aliases that act as commands in
+# the documentation. An alias has the form:
+# name=value
+# For example adding
+# "sideeffect=@par Side Effects:\n"
+# will allow you to put the command \sideeffect (or @sideeffect) in the
+# documentation, which will result in a user-defined paragraph with heading
+# "Side Effects:". You can put \n's in the value part of an alias to insert
+# newlines (in the resulting output). You can put ^^ in the value part of an
+# alias to insert a newline as if a physical newline was in the original file.
+# When you need a literal { or } or , in the value part of an alias you have to
+# escape them by means of a backslash (\), this can lead to conflicts with the
+# commands \{ and \} for these it is advised to use the version @{ and @} or use
+# a double escape (\\{ and \\})
+
+ALIASES                =
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
+# only. Doxygen will then generate output that is more tailored for C. For
+# instance, some of the names that are used will be different. The list of all
+# members will be omitted, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_FOR_C  = NO
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
+# Python sources only. Doxygen will then generate output that is more tailored
+# for that language. For instance, namespaces will be presented as packages,
+# qualified scopes will look different, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_JAVA   = NO
+
+# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
+# sources. Doxygen will then generate output that is tailored for Fortran.
+# The default value is: NO.
+
+OPTIMIZE_FOR_FORTRAN   = NO
+
+# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
+# sources. Doxygen will then generate output that is tailored for VHDL.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_VHDL   = NO
+
+# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice
+# sources only. Doxygen will then generate output that is more tailored for that
+# language. For instance, namespaces will be presented as modules, types will be
+# separated into more groups, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_SLICE  = NO
+
+# Doxygen selects the parser to use depending on the extension of the files it
+# parses. With this tag you can assign which parser to use for a given
+# extension. Doxygen has a built-in mapping, but you can override or extend it
+# using this tag. The format is ext=language, where ext is a file extension, and
+# language is one of the parsers supported by doxygen: IDL, Java, JavaScript,
+# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, VHDL,
+# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran:
+# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser
+# tries to guess whether the code is fixed or free formatted code, this is the
+# default for Fortran type files). For instance to make doxygen treat .inc files
+# as Fortran files (default is PHP), and .f files as C (default is Fortran),
+# use: inc=Fortran f=C.
+#
+# Note: For files without extension you can use no_extension as a placeholder.
+#
+# Note that for custom extensions you also need to set FILE_PATTERNS otherwise
+# the files are not read by doxygen.
+
+EXTENSION_MAPPING      =
+
+# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
+# according to the Markdown format, which allows for more readable
+# documentation. See https://daringfireball.net/projects/markdown/ for details.
+# The output of markdown processing is further processed by doxygen, so you can
+# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in
+# case of backward compatibilities issues.
+# The default value is: YES.
+
+MARKDOWN_SUPPORT       = YES
+
+# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up
+# to that level are automatically included in the table of contents, even if
+# they do not have an id attribute.
+# Note: This feature currently applies only to Markdown headings.
+# Minimum value: 0, maximum value: 99, default value: 5.
+# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
+
+TOC_INCLUDE_HEADINGS   = 5
+
+# When enabled doxygen tries to link words that correspond to documented
+# classes, or namespaces to their corresponding documentation. Such a link can
+# be prevented in individual cases by putting a % sign in front of the word or
+# globally by setting AUTOLINK_SUPPORT to NO.
+# The default value is: YES.
+
+AUTOLINK_SUPPORT       = YES
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
+# to include (a tag file for) the STL sources as input, then you should set this
+# tag to YES in order to let doxygen match functions declarations and
+# definitions whose arguments contain STL classes (e.g. func(std::string);
+# versus func(std::string) {}). This also make the inheritance and collaboration
+# diagrams that involve STL classes more complete and accurate.
+# The default value is: NO.
+
+BUILTIN_STL_SUPPORT    = NO
+
+# If you use Microsoft's C++/CLI language, you should set this option to YES to
+# enable parsing support.
+# The default value is: NO.
+
+CPP_CLI_SUPPORT        = NO
+
+# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:
+# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen
+# will parse them like normal C++ but will assume all classes use public instead
+# of private inheritance when no explicit protection keyword is present.
+# The default value is: NO.
+
+SIP_SUPPORT            = NO
+
+# For Microsoft's IDL there are propget and propput attributes to indicate
+# getter and setter methods for a property. Setting this option to YES will make
+# doxygen to replace the get and set methods by a property in the documentation.
+# This will only work if the methods are indeed getting or setting a simple
+# type. If this is not the case, or you want to show the methods anyway, you
+# should set this option to NO.
+# The default value is: YES.
+
+IDL_PROPERTY_SUPPORT   = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+# The default value is: NO.
+
+DISTRIBUTE_GROUP_DOC   = NO
+
+# If one adds a struct or class to a group and this option is enabled, then also
+# any nested class or struct is added to the same group. By default this option
+# is disabled and one has to add nested compounds explicitly via \ingroup.
+# The default value is: NO.
+
+GROUP_NESTED_COMPOUNDS = NO
+
+# Set the SUBGROUPING tag to YES to allow class member groups of the same type
+# (for instance a group of public functions) to be put as a subgroup of that
+# type (e.g. under the Public Functions section). Set it to NO to prevent
+# subgrouping. Alternatively, this can be done per class using the
+# \nosubgrouping command.
+# The default value is: YES.
+
+SUBGROUPING            = YES
+
+# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions
+# are shown inside the group in which they are included (e.g. using \ingroup)
+# instead of on a separate page (for HTML and Man pages) or section (for LaTeX
+# and RTF).
+#
+# Note that this feature does not work in combination with
+# SEPARATE_MEMBER_PAGES.
+# The default value is: NO.
+
+INLINE_GROUPED_CLASSES = NO
+
+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions
+# with only public data fields or simple typedef fields will be shown inline in
+# the documentation of the scope in which they are defined (i.e. file,
+# namespace, or group documentation), provided this scope is documented. If set
+# to NO, structs, classes, and unions are shown on a separate page (for HTML and
+# Man pages) or section (for LaTeX and RTF).
+# The default value is: NO.
+
+INLINE_SIMPLE_STRUCTS  = NO
+
+# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or
+# enum is documented as struct, union, or enum with the name of the typedef. So
+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
+# with name TypeT. When disabled the typedef will appear as a member of a file,
+# namespace, or class. And the struct will be named TypeS. This can typically be
+# useful for C code in case the coding convention dictates that all compound
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+# The default value is: NO.
+
+TYPEDEF_HIDES_STRUCT   = NO
+
+# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This
+# cache is used to resolve symbols given their name and scope. Since this can be
+# an expensive process and often the same symbol appears multiple times in the
+# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small
+# doxygen will become slower. If the cache is too large, memory is wasted. The
+# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range
+# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536
+# symbols. At the end of a run doxygen will report the cache usage and suggest
+# the optimal cache size from a speed point of view.
+# Minimum value: 0, maximum value: 9, default value: 0.
+
+LOOKUP_CACHE_SIZE      = 0
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in
+# documentation are documented, even if no documentation was available. Private
+# class members and static file members will be hidden unless the
+# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.
+# Note: This will also disable the warnings about undocumented members that are
+# normally produced when WARNINGS is set to YES.
+# The default value is: NO.
+
+EXTRACT_ALL            = YES
+
+# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
+# be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIVATE        = NO
+
+# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual
+# methods of a class will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIV_VIRTUAL   = NO
+
+# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal
+# scope will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PACKAGE        = NO
+
+# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be
+# included in the documentation.
+# The default value is: NO.
+
+EXTRACT_STATIC         = NO
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
+# locally in source files will be included in the documentation. If set to NO,
+# only classes defined in header files are included. Does not have any effect
+# for Java sources.
+# The default value is: YES.
+
+EXTRACT_LOCAL_CLASSES  = YES
+
+# This flag is only useful for Objective-C code. If set to YES, local methods,
+# which are defined in the implementation section but not in the interface are
+# included in the documentation. If set to NO, only methods in the interface are
+# included.
+# The default value is: NO.
+
+EXTRACT_LOCAL_METHODS  = NO
+
+# If this flag is set to YES, the members of anonymous namespaces will be
+# extracted and appear in the documentation as a namespace called
+# 'anonymous_namespace{file}', where file will be replaced with the base name of
+# the file that contains the anonymous namespace. By default anonymous namespace
+# are hidden.
+# The default value is: NO.
+
+EXTRACT_ANON_NSPACES   = NO
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all
+# undocumented members inside documented classes or files. If set to NO these
+# members will be included in the various overviews, but no documentation
+# section is generated. This option has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_MEMBERS     = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy. If set
+# to NO, these classes will be included in the various overviews. This option
+# has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_CLASSES     = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend
+# declarations. If set to NO, these declarations will be included in the
+# documentation.
+# The default value is: NO.
+
+HIDE_FRIEND_COMPOUNDS  = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any
+# documentation blocks found inside the body of a function. If set to NO, these
+# blocks will be appended to the function's detailed documentation block.
+# The default value is: NO.
+
+HIDE_IN_BODY_DOCS      = NO
+
+# The INTERNAL_DOCS tag determines if documentation that is typed after a
+# \internal command is included. If the tag is set to NO then the documentation
+# will be excluded. Set it to YES to include the internal documentation.
+# The default value is: NO.
+
+INTERNAL_DOCS          = NO
+
+# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file
+# names in lower-case letters. If set to YES, upper-case letters are also
+# allowed. This is useful if you have classes or files whose names only differ
+# in case and if your file system supports case sensitive file names. Windows
+# (including Cygwin) ands Mac users are advised to set this option to NO.
+# The default value is: system dependent.
+
+CASE_SENSE_NAMES       = NO
+
+# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with
+# their full class and namespace scopes in the documentation. If set to YES, the
+# scope will be hidden.
+# The default value is: NO.
+
+HIDE_SCOPE_NAMES       = NO
+
+# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will
+# append additional text to a page's title, such as Class Reference. If set to
+# YES the compound reference will be hidden.
+# The default value is: NO.
+
+HIDE_COMPOUND_REFERENCE= NO
+
+# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
+# the files that are included by a file in the documentation of that file.
+# The default value is: YES.
+
+SHOW_INCLUDE_FILES     = YES
+
+# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each
+# grouped member an include statement to the documentation, telling the reader
+# which file to include in order to use the member.
+# The default value is: NO.
+
+SHOW_GROUPED_MEMB_INC  = NO
+
+# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include
+# files with double quotes in the documentation rather than with sharp brackets.
+# The default value is: NO.
+
+FORCE_LOCAL_INCLUDES   = NO
+
+# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the
+# documentation for inline members.
+# The default value is: YES.
+
+INLINE_INFO            = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the
+# (detailed) documentation of file and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order.
+# The default value is: YES.
+
+SORT_MEMBER_DOCS       = YES
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
+# descriptions of file, namespace and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order. Note that
+# this will also influence the order of the classes in the class list.
+# The default value is: NO.
+
+SORT_BRIEF_DOCS        = NO
+
+# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the
+# (brief and detailed) documentation of class members so that constructors and
+# destructors are listed first. If set to NO the constructors will appear in the
+# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.
+# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief
+# member documentation.
+# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting
+# detailed member documentation.
+# The default value is: NO.
+
+SORT_MEMBERS_CTORS_1ST = NO
+
+# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy
+# of group names into alphabetical order. If set to NO the group names will
+# appear in their defined order.
+# The default value is: NO.
+
+SORT_GROUP_NAMES       = NO
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
+# fully-qualified names, including namespaces. If set to NO, the class list will
+# be sorted only by class name, not including the namespace part.
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the alphabetical
+# list.
+# The default value is: NO.
+
+SORT_BY_SCOPE_NAME     = NO
+
+# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper
+# type resolution of all parameters of a function it will reject a match between
+# the prototype and the implementation of a member function even if there is
+# only one candidate or it is obvious which candidate to choose by doing a
+# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still
+# accept a match between prototype and implementation in such cases.
+# The default value is: NO.
+
+STRICT_PROTO_MATCHING  = NO
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo
+# list. This list is created by putting \todo commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TODOLIST      = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test
+# list. This list is created by putting \test commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TESTLIST      = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug
+# list. This list is created by putting \bug commands in the documentation.
+# The default value is: YES.
+
+GENERATE_BUGLIST       = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)
+# the deprecated list. This list is created by putting \deprecated commands in
+# the documentation.
+# The default value is: YES.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional documentation
+# sections, marked by \if <section_label> ... \endif and \cond <section_label>
+# ... \endcond blocks.
+
+ENABLED_SECTIONS       =
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the
+# initial value of a variable or macro / define can have for it to appear in the
+# documentation. If the initializer consists of more lines than specified here
+# it will be hidden. Use a value of 0 to hide initializers completely. The
+# appearance of the value of individual variables and macros / defines can be
+# controlled using \showinitializer or \hideinitializer command in the
+# documentation regardless of this setting.
+# Minimum value: 0, maximum value: 10000, default value: 30.
+
+MAX_INITIALIZER_LINES  = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at
+# the bottom of the documentation of classes and structs. If set to YES, the
+# list will mention the files that were used to generate the documentation.
+# The default value is: YES.
+
+SHOW_USED_FILES        = YES
+
+# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This
+# will remove the Files entry from the Quick Index and from the Folder Tree View
+# (if specified).
+# The default value is: YES.
+
+SHOW_FILES             = YES
+
+# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces
+# page. This will remove the Namespaces entry from the Quick Index and from the
+# Folder Tree View (if specified).
+# The default value is: YES.
+
+SHOW_NAMESPACES        = YES
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that
+# doxygen should invoke to get the current version for each file (typically from
+# the version control system). Doxygen will invoke the program by executing (via
+# popen()) the command command input-file, where command is the value of the
+# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided
+# by doxygen. Whatever the program writes to standard output is used as the file
+# version. For an example see the documentation.
+
+FILE_VERSION_FILTER    =
+
+# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
+# by doxygen. The layout file controls the global structure of the generated
+# output files in an output format independent way. To create the layout file
+# that represents doxygen's defaults, run doxygen with the -l option. You can
+# optionally specify a file name after the option, if omitted DoxygenLayout.xml
+# will be used as the name of the layout file.
+#
+# Note that if you run doxygen from a directory containing a file called
+# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
+# tag is left empty.
+
+LAYOUT_FILE            =
+
+# The CITE_BIB_FILES tag can be used to specify one or more bib files containing
+# the reference definitions. This must be a list of .bib files. The .bib
+# extension is automatically appended if omitted. This requires the bibtex tool
+# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info.
+# For LaTeX the style of the bibliography can be controlled using
+# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the
+# search path. See also \cite for info how to create references.
+
+CITE_BIB_FILES         =
+
+#---------------------------------------------------------------------------
+# Configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated to
+# standard output by doxygen. If QUIET is set to YES this implies that the
+# messages are off.
+# The default value is: NO.
+
+QUIET                  = NO
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are
+# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES
+# this implies that the warnings are on.
+#
+# Tip: Turn warnings on while writing the documentation.
+# The default value is: YES.
+
+WARNINGS               = YES
+
+# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate
+# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag
+# will automatically be disabled.
+# The default value is: YES.
+
+WARN_IF_UNDOCUMENTED   = YES
+
+# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
+# potential errors in the documentation, such as not documenting some parameters
+# in a documented function, or documenting parameters that don't exist or using
+# markup commands wrongly.
+# The default value is: YES.
+
+WARN_IF_DOC_ERROR      = YES
+
+# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
+# are documented, but have no documentation for their parameters or return
+# value. If set to NO, doxygen will only warn about wrong or incomplete
+# parameter documentation, but not about the absence of documentation. If
+# EXTRACT_ALL is set to YES then this flag will automatically be disabled.
+# The default value is: NO.
+
+WARN_NO_PARAMDOC       = NO
+
+# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
+# a warning is encountered.
+# The default value is: NO.
+
+WARN_AS_ERROR          = NO
+
+# The WARN_FORMAT tag determines the format of the warning messages that doxygen
+# can produce. The string should contain the $file, $line, and $text tags, which
+# will be replaced by the file and line number from which the warning originated
+# and the warning text. Optionally the format may contain $version, which will
+# be replaced by the version of the file (if it could be obtained via
+# FILE_VERSION_FILTER)
+# The default value is: $file:$line: $text.
+
+WARN_FORMAT            = "$file:$line: $text"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning and error
+# messages should be written. If left blank the output is written to standard
+# error (stderr).
+
+WARN_LOGFILE           =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag is used to specify the files and/or directories that contain
+# documented source files. You may enter file names like myfile.cpp or
+# directories like /usr/src/myproject. Separate the files or directories with
+# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
+# Note: If this tag is empty the current directory is searched.
+
+INPUT                  = "include"
+
+# This tag can be used to specify the character encoding of the source files
+# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
+# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
+# documentation (see: https://www.gnu.org/software/libiconv/) for the list of
+# possible encodings.
+# The default value is: UTF-8.
+
+INPUT_ENCODING         = UTF-8
+
+# If the value of the INPUT tag contains directories, you can use the
+# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
+# *.h) to filter out the source-files in the directories.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# read by doxygen.
+#
+# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
+# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
+# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc,
+# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment),
+# *.doc (to be provided as doxygen C comment), *.txt (to be provided as doxygen
+# C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd,
+# *.vhdl, *.ucf, *.qsf and *.ice.
+
+FILE_PATTERNS          = *.c \
+                         *.cc \
+                         *.cxx \
+                         *.cpp \
+                         *.c++ \
+                         *.java \
+                         *.ii \
+                         *.ixx \
+                         *.ipp \
+                         *.i++ \
+                         *.inl \
+                         *.idl \
+                         *.ddl \
+                         *.odl \
+                         *.h \
+                         *.hh \
+                         *.hxx \
+                         *.hpp \
+                         *.h++ \
+                         *.cs \
+                         *.d \
+                         *.php \
+                         *.php4 \
+                         *.php5 \
+                         *.phtml \
+                         *.inc \
+                         *.m \
+                         *.markdown \
+                         *.md \
+                         *.mm \
+                         *.dox \
+                         *.doc \
+                         *.txt \
+                         *.py \
+                         *.pyw \
+                         *.f90 \
+                         *.f95 \
+                         *.f03 \
+                         *.f08 \
+                         *.f18 \
+                         *.f \
+                         *.for \
+                         *.vhd \
+                         *.vhdl \
+                         *.ucf \
+                         *.qsf \
+                         *.ice
+
+# The RECURSIVE tag can be used to specify whether or not subdirectories should
+# be searched for input files as well.
+# The default value is: NO.
+
+RECURSIVE              = NO
+
+# The EXCLUDE tag can be used to specify files and/or directories that should be
+# excluded from the INPUT source files. This way you can easily exclude a
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+#
+# Note that relative paths are relative to the directory from which doxygen is
+# run.
+
+EXCLUDE                =
+
+# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
+# directories that are symbolic links (a Unix file system feature) are excluded
+# from the input.
+# The default value is: NO.
+
+EXCLUDE_SYMLINKS       = NO
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories.
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories for example use the pattern */test/*
+
+EXCLUDE_PATTERNS       =
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
+# (namespaces, classes, functions, etc.) that should be excluded from the
+# output. The symbol name can be a fully qualified name, a word, or if the
+# wildcard * is used, a substring. Examples: ANamespace, AClass,
+# AClass::ANamespace, ANamespace::*Test
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories use the pattern */test/*
+
+EXCLUDE_SYMBOLS        =
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or directories
+# that contain example code fragments that are included (see the \include
+# command).
+
+EXAMPLE_PATH           =
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and
+# *.h) to filter out the source-files in the directories. If left blank all
+# files are included.
+
+EXAMPLE_PATTERNS       = *
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude commands
+# irrespective of the value of the RECURSIVE tag.
+# The default value is: NO.
+
+EXAMPLE_RECURSIVE      = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or directories
+# that contain images that are to be included in the documentation (see the
+# \image command).
+
+IMAGE_PATH             =
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should
+# invoke to filter for each input file. Doxygen will invoke the filter program
+# by executing (via popen()) the command:
+#
+# <filter> <input-file>
+#
+# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the
+# name of an input file. Doxygen will then use the output that the filter
+# program writes to standard output. If FILTER_PATTERNS is specified, this tag
+# will be ignored.
+#
+# Note that the filter must not add or remove lines; it is applied before the
+# code is scanned, but not when the output code is generated. If lines are added
+# or removed, the anchors will not be placed correctly.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+INPUT_FILTER           =
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
+# basis. Doxygen will compare the file name with each pattern and apply the
+# filter if there is a match. The filters are a list of the form: pattern=filter
+# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how
+# filters are used. If the FILTER_PATTERNS tag is empty or if none of the
+# patterns match the file name, INPUT_FILTER is applied.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+FILTER_PATTERNS        =
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
+# INPUT_FILTER) will also be used to filter the input files that are used for
+# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).
+# The default value is: NO.
+
+FILTER_SOURCE_FILES    = NO
+
+# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file
+# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and
+# it is also possible to disable source filtering for a specific pattern using
+# *.ext= (so without naming a filter).
+# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.
+
+FILTER_SOURCE_PATTERNS =
+
+# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that
+# is part of the input, its contents will be placed on the main page
+# (index.html). This can be useful if you have a project on for instance GitHub
+# and want to reuse the introduction page also for the doxygen output.
+
+USE_MDFILE_AS_MAINPAGE =
+
+#---------------------------------------------------------------------------
+# Configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will be
+# generated. Documented entities will be cross-referenced with these sources.
+#
+# Note: To get rid of all source code in the generated output, make sure that
+# also VERBATIM_HEADERS is set to NO.
+# The default value is: NO.
+
+SOURCE_BROWSER         = NO
+
+# Setting the INLINE_SOURCES tag to YES will include the body of functions,
+# classes and enums directly into the documentation.
+# The default value is: NO.
+
+INLINE_SOURCES         = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any
+# special comment blocks from generated source code fragments. Normal C, C++ and
+# Fortran comments will always remain visible.
+# The default value is: YES.
+
+STRIP_CODE_COMMENTS    = YES
+
+# If the REFERENCED_BY_RELATION tag is set to YES then for each documented
+# entity all documented functions referencing it will be listed.
+# The default value is: NO.
+
+REFERENCED_BY_RELATION = NO
+
+# If the REFERENCES_RELATION tag is set to YES then for each documented function
+# all documented entities called/used by that function will be listed.
+# The default value is: NO.
+
+REFERENCES_RELATION    = NO
+
+# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set
+# to YES then the hyperlinks from functions in REFERENCES_RELATION and
+# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will
+# link to the documentation.
+# The default value is: YES.
+
+REFERENCES_LINK_SOURCE = YES
+
+# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the
+# source code will show a tooltip with additional information such as prototype,
+# brief description and links to the definition and documentation. Since this
+# will make the HTML file larger and loading of large files a bit slower, you
+# can opt to disable this feature.
+# The default value is: YES.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+SOURCE_TOOLTIPS        = YES
+
+# If the USE_HTAGS tag is set to YES then the references to source code will
+# point to the HTML generated by the htags(1) tool instead of doxygen built-in
+# source browser. The htags tool is part of GNU's global source tagging system
+# (see https://www.gnu.org/software/global/global.html). You will need version
+# 4.8.6 or higher.
+#
+# To use it do the following:
+# - Install the latest version of global
+# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file
+# - Make sure the INPUT points to the root of the source tree
+# - Run doxygen as normal
+#
+# Doxygen will invoke htags (and that will in turn invoke gtags), so these
+# tools must be available from the command line (i.e. in the search path).
+#
+# The result: instead of the source browser generated by doxygen, the links to
+# source code will now point to the output of htags.
+# The default value is: NO.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+USE_HTAGS              = NO
+
+# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a
+# verbatim copy of the header file for each class for which an include is
+# specified. Set to NO to disable this.
+# See also: Section \class.
+# The default value is: YES.
+
+VERBATIM_HEADERS       = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all
+# compounds will be generated. Enable this if the project contains a lot of
+# classes, structs, unions or interfaces.
+# The default value is: YES.
+
+ALPHABETICAL_INDEX     = YES
+
+# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in
+# which the alphabetical index list will be split.
+# Minimum value: 1, maximum value: 20, default value: 5.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+COLS_IN_ALPHA_INDEX    = 5
+
+# In case all classes in a project start with a common prefix, all classes will
+# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag
+# can be used to specify a prefix (or a list of prefixes) that should be ignored
+# while generating the index headers.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+IGNORE_PREFIX          =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
+# The default value is: YES.
+
+GENERATE_HTML          = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_OUTPUT            = html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
+# generated HTML page (for example: .htm, .php, .asp).
+# The default value is: .html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FILE_EXTENSION    = .html
+
+# The HTML_HEADER tag can be used to specify a user-defined HTML header file for
+# each generated HTML page. If the tag is left blank doxygen will generate a
+# standard header.
+#
+# To get valid HTML the header file that includes any scripts and style sheets
+# that doxygen needs, which is dependent on the configuration options used (e.g.
+# the setting GENERATE_TREEVIEW). It is highly recommended to start with a
+# default header using
+# doxygen -w html new_header.html new_footer.html new_stylesheet.css
+# YourConfigFile
+# and then modify the file new_header.html. See also section "Doxygen usage"
+# for information on how to generate the default header that doxygen normally
+# uses.
+# Note: The header is subject to change so you typically have to regenerate the
+# default header when upgrading to a newer version of doxygen. For a description
+# of the possible markers and block names see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_HEADER            =
+
+# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
+# generated HTML page. If the tag is left blank doxygen will generate a standard
+# footer. See HTML_HEADER for more information on how to generate a default
+# footer and what special commands can be used inside the footer. See also
+# section "Doxygen usage" for information on how to generate the default footer
+# that doxygen normally uses.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FOOTER            =
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style
+# sheet that is used by each HTML page. It can be used to fine-tune the look of
+# the HTML output. If left blank doxygen will generate a default style sheet.
+# See also section "Doxygen usage" for information on how to generate the style
+# sheet that doxygen normally uses.
+# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as
+# it is more robust and this tag (HTML_STYLESHEET) will in the future become
+# obsolete.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_STYLESHEET        =
+
+# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# cascading style sheets that are included after the standard style sheets
+# created by doxygen. Using this option one can overrule certain style aspects.
+# This is preferred over using HTML_STYLESHEET since it does not replace the
+# standard style sheet and is therefore more robust against future updates.
+# Doxygen will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list). For an example see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_STYLESHEET  =
+
+# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the HTML output directory. Note
+# that these files will be copied to the base HTML output directory. Use the
+# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
+# files. In the HTML_STYLESHEET file, use the file name only. Also note that the
+# files will be copied as-is; there are no commands or markers available.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_FILES       =
+
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
+# will adjust the colors in the style sheet and background images according to
+# this color. Hue is specified as an angle on a colorwheel, see
+# https://en.wikipedia.org/wiki/Hue for more information. For instance the value
+# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
+# purple, and 360 is red again.
+# Minimum value: 0, maximum value: 359, default value: 220.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_HUE    = 220
+
+# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
+# in the HTML output. For a value of 0 the output will use grayscales only. A
+# value of 255 will produce the most vivid colors.
+# Minimum value: 0, maximum value: 255, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_SAT    = 100
+
+# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the
+# luminance component of the colors in the HTML output. Values below 100
+# gradually make the output lighter, whereas values above 100 make the output
+# darker. The value divided by 100 is the actual gamma applied, so 80 represents
+# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not
+# change the gamma.
+# Minimum value: 40, maximum value: 240, default value: 80.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_GAMMA  = 80
+
+# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
+# page will contain the date and time when the page was generated. Setting this
+# to YES can help to show when doxygen was last run and thus if the
+# documentation is up to date.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_TIMESTAMP         = NO
+
+# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
+# documentation will contain a main index with vertical navigation menus that
+# are dynamically created via JavaScript. If disabled, the navigation index will
+# consists of multiple levels of tabs that are statically embedded in every HTML
+# page. Disable this option to support browsers that do not have JavaScript,
+# like the Qt help browser.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_MENUS     = YES
+
+# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
+# documentation will contain sections that can be hidden and shown after the
+# page has loaded.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_SECTIONS  = NO
+
+# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
+# shown in the various tree structured indices initially; the user can expand
+# and collapse entries dynamically later on. Doxygen will expand the tree to
+# such a level that at most the specified number of entries are visible (unless
+# a fully collapsed tree already exceeds this amount). So setting the number of
+# entries 1 will produce a full collapsed tree by default. 0 is a special value
+# representing an infinite number of entries and will result in a full expanded
+# tree by default.
+# Minimum value: 0, maximum value: 9999, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_INDEX_NUM_ENTRIES = 100
+
+# If the GENERATE_DOCSET tag is set to YES, additional index files will be
+# generated that can be used as input for Apple's Xcode 3 integrated development
+# environment (see: https://developer.apple.com/xcode/), introduced with OSX
+# 10.5 (Leopard). To create a documentation set, doxygen will generate a
+# Makefile in the HTML output directory. Running make will produce the docset in
+# that directory and running make install will install the docset in
+# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at
+# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy
+# genXcode/_index.html for more information.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_DOCSET        = NO
+
+# This tag determines the name of the docset feed. A documentation feed provides
+# an umbrella under which multiple documentation sets from a single provider
+# (such as a company or product suite) can be grouped.
+# The default value is: Doxygen generated docs.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_FEEDNAME        = "Doxygen generated docs"
+
+# This tag specifies a string that should uniquely identify the documentation
+# set bundle. This should be a reverse domain-name style string, e.g.
+# com.mycompany.MyDocSet. Doxygen will append .docset to the name.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_BUNDLE_ID       = org.doxygen.Project
+
+# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify
+# the documentation publisher. This should be a reverse domain-name style
+# string, e.g. com.mycompany.MyDocSet.documentation.
+# The default value is: org.doxygen.Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_ID    = org.doxygen.Publisher
+
+# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.
+# The default value is: Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_NAME  = Publisher
+
+# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
+# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
+# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
+# (see: https://www.microsoft.com/en-us/download/details.aspx?id=21138) on
+# Windows.
+#
+# The HTML Help Workshop contains a compiler that can convert all HTML output
+# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
+# files are now used as the Windows 98 help format, and will replace the old
+# Windows help format (.hlp) on all Windows platforms in the future. Compressed
+# HTML files also contain an index, a table of contents, and you can search for
+# words in the documentation. The HTML workshop also contains a viewer for
+# compressed HTML files.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_HTMLHELP      = NO
+
+# The CHM_FILE tag can be used to specify the file name of the resulting .chm
+# file. You can add a path in front of the file if the result should not be
+# written to the html output directory.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_FILE               =
+
+# The HHC_LOCATION tag can be used to specify the location (absolute path
+# including file name) of the HTML help compiler (hhc.exe). If non-empty,
+# doxygen will try to run the HTML help compiler on the generated index.hhp.
+# The file has to be specified with full path.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+HHC_LOCATION           =
+
+# The GENERATE_CHI flag controls if a separate .chi index file is generated
+# (YES) or that it should be included in the master .chm file (NO).
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+GENERATE_CHI           = NO
+
+# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)
+# and project file content.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_INDEX_ENCODING     =
+
+# The BINARY_TOC flag controls whether a binary table of contents is generated
+# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it
+# enables the Previous and Next buttons.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+BINARY_TOC             = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members to
+# the table of contents of the HTML help documentation and to the tree view.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+TOC_EXPAND             = NO
+
+# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
+# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
+# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
+# (.qch) of the generated HTML documentation.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_QHP           = NO
+
+# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify
+# the file name of the resulting .qch file. The path specified is relative to
+# the HTML output folder.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QCH_FILE               =
+
+# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
+# Project output. For more information please see Qt Help Project / Namespace
+# (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace).
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_NAMESPACE          = org.doxygen.Project
+
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
+# Help Project output. For more information please see Qt Help Project / Virtual
+# Folders (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-
+# folders).
+# The default value is: doc.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_VIRTUAL_FOLDER     = doc
+
+# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
+# filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_NAME   =
+
+# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see Qt Help Project / Custom
+# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-
+# filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_ATTRS  =
+
+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
+# project's filter section matches. Qt Help Project / Filter Attributes (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_SECT_FILTER_ATTRS  =
+
+# The QHG_LOCATION tag can be used to specify the location of Qt's
+# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the
+# generated .qhp file.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHG_LOCATION           =
+
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be
+# generated, together with the HTML files, they form an Eclipse help plugin. To
+# install this plugin and make it available under the help contents menu in
+# Eclipse, the contents of the directory containing the HTML and XML files needs
+# to be copied into the plugins directory of eclipse. The name of the directory
+# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.
+# After copying Eclipse needs to be restarted before the help appears.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_ECLIPSEHELP   = NO
+
+# A unique identifier for the Eclipse help plugin. When installing the plugin
+# the directory name containing the HTML and XML files should also have this
+# name. Each documentation set should have its own identifier.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.
+
+ECLIPSE_DOC_ID         = org.doxygen.Project
+
+# If you want full control over the layout of the generated HTML pages it might
+# be necessary to disable the index and replace it with your own. The
+# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top
+# of each HTML page. A value of NO enables the index and the value YES disables
+# it. Since the tabs in the index contain the same information as the navigation
+# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+DISABLE_INDEX          = NO
+
+# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
+# structure should be generated to display hierarchical information. If the tag
+# value is set to YES, a side panel will be generated containing a tree-like
+# index structure (just like the one that is generated for HTML Help). For this
+# to work a browser that supports JavaScript, DHTML, CSS and frames is required
+# (i.e. any modern browser). Windows users are probably better off using the
+# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
+# further fine-tune the look of the index. As an example, the default style
+# sheet generated by doxygen has an example that shows how to put an image at
+# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
+# the same information as the tab index, you could consider setting
+# DISABLE_INDEX to YES when enabling this option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_TREEVIEW      = NO
+
+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
+# doxygen will group on one line in the generated HTML documentation.
+#
+# Note that a value of 0 will completely suppress the enum values from appearing
+# in the overview section.
+# Minimum value: 0, maximum value: 20, default value: 4.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+ENUM_VALUES_PER_LINE   = 4
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used
+# to set the initial width (in pixels) of the frame in which the tree is shown.
+# Minimum value: 0, maximum value: 1500, default value: 250.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+TREEVIEW_WIDTH         = 250
+
+# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to
+# external symbols imported via tag files in a separate window.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+EXT_LINKS_IN_WINDOW    = NO
+
+# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg
+# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see
+# https://inkscape.org) to generate formulas as SVG images instead of PNGs for
+# the HTML output. These images will generally look nicer at scaled resolutions.
+# Possible values are: png The default and svg Looks nicer but requires the
+# pdf2svg tool.
+# The default value is: png.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FORMULA_FORMAT    = png
+
+# Use this tag to change the font size of LaTeX formulas included as images in
+# the HTML documentation. When you change the font size after a successful
+# doxygen run you need to manually remove any form_*.png images from the HTML
+# output directory to force them to be regenerated.
+# Minimum value: 8, maximum value: 50, default value: 10.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_FONTSIZE       = 10
+
+# Use the FORMULA_TRANSPARENT tag to determine whether or not the images
+# generated for formulas are transparent PNGs. Transparent PNGs are not
+# supported properly for IE 6.0, but are supported on all modern browsers.
+#
+# Note that when changing this option you need to delete any form_*.png files in
+# the HTML output directory before the changes have effect.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_TRANSPARENT    = YES
+
+# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands
+# to create new LaTeX commands to be used in formulas as building blocks. See
+# the section "Including formulas" for details.
+
+FORMULA_MACROFILE      =
+
+# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see
+# https://www.mathjax.org) which uses client side JavaScript for the rendering
+# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX
+# installed or if you want to formulas look prettier in the HTML output. When
+# enabled you may also need to install MathJax separately and configure the path
+# to it using the MATHJAX_RELPATH option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+USE_MATHJAX            = NO
+
+# When MathJax is enabled you can set the default output format to be used for
+# the MathJax output. See the MathJax site (see:
+# http://docs.mathjax.org/en/latest/output.html) for more details.
+# Possible values are: HTML-CSS (which is slower, but has the best
+# compatibility), NativeMML (i.e. MathML) and SVG.
+# The default value is: HTML-CSS.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_FORMAT         = HTML-CSS
+
+# When MathJax is enabled you need to specify the location relative to the HTML
+# output directory using the MATHJAX_RELPATH option. The destination directory
+# should contain the MathJax.js script. For instance, if the mathjax directory
+# is located at the same level as the HTML output directory, then
+# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
+# Content Delivery Network so you can quickly see the result without installing
+# MathJax. However, it is strongly recommended to install a local copy of
+# MathJax from https://www.mathjax.org before deployment.
+# The default value is: https://cdn.jsdelivr.net/npm/mathjax@2.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_RELPATH        = https://cdn.jsdelivr.net/npm/mathjax@2
+
+# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
+# extension names that should be enabled during MathJax rendering. For example
+# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_EXTENSIONS     =
+
+# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
+# of code that will be used on startup of the MathJax code. See the MathJax site
+# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an
+# example see the documentation.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_CODEFILE       =
+
+# When the SEARCHENGINE tag is enabled doxygen will generate a search box for
+# the HTML output. The underlying search engine uses javascript and DHTML and
+# should work on any modern browser. Note that when using HTML help
+# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)
+# there is already a search function so this one should typically be disabled.
+# For large projects the javascript based search engine can be slow, then
+# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to
+# search using the keyboard; to jump to the search box use <access key> + S
+# (what the <access key> is depends on the OS and browser, but it is typically
+# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down
+# key> to jump into the search results window, the results can be navigated
+# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel
+# the search. The filter options can be selected when the cursor is inside the
+# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>
+# to select a filter and <Enter> or <escape> to activate or cancel the filter
+# option.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+SEARCHENGINE           = YES
+
+# When the SERVER_BASED_SEARCH tag is enabled the search engine will be
+# implemented using a web server instead of a web client using JavaScript. There
+# are two flavors of web server based searching depending on the EXTERNAL_SEARCH
+# setting. When disabled, doxygen will generate a PHP script for searching and
+# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing
+# and searching needs to be provided by external tools. See the section
+# "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SERVER_BASED_SEARCH    = NO
+
+# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP
+# script for searching. Instead the search results are written to an XML file
+# which needs to be processed by an external indexer. Doxygen will invoke an
+# external search engine pointed to by the SEARCHENGINE_URL option to obtain the
+# search results.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: https://xapian.org/).
+#
+# See the section "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH        = NO
+
+# The SEARCHENGINE_URL should point to a search engine hosted by a web server
+# which will return the search results when EXTERNAL_SEARCH is enabled.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see: https://xapian.org/). See the section "External Indexing and
+# Searching" for details.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHENGINE_URL       =
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed
+# search data is written to a file for indexing by an external tool. With the
+# SEARCHDATA_FILE tag the name of this file can be specified.
+# The default file is: searchdata.xml.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHDATA_FILE        = searchdata.xml
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the
+# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is
+# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple
+# projects and redirect the results back to the right project.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH_ID     =
+
+# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen
+# projects other than the one defined by this configuration file, but that are
+# all added to the same external search index. Each project needs to have a
+# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of
+# to a relative location where the documentation can be found. The format is:
+# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTRA_SEARCH_MAPPINGS  =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.
+# The default value is: YES.
+
+GENERATE_LATEX         = NO
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_OUTPUT           = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
+# invoked.
+#
+# Note that when not enabling USE_PDFLATEX the default is latex when enabling
+# USE_PDFLATEX the default is pdflatex and when in the later case latex is
+# chosen this is overwritten by pdflatex. For specific output languages the
+# default can have been set differently, this depends on the implementation of
+# the output language.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_CMD_NAME         =
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate
+# index for LaTeX.
+# Note: This tag is used in the Makefile / make.bat.
+# See also: LATEX_MAKEINDEX_CMD for the part in the generated output file
+# (.tex).
+# The default file is: makeindex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+MAKEINDEX_CMD_NAME     = makeindex
+
+# The LATEX_MAKEINDEX_CMD tag can be used to specify the command name to
+# generate index for LaTeX. In case there is no backslash (\) as first character
+# it will be automatically added in the LaTeX code.
+# Note: This tag is used in the generated output file (.tex).
+# See also: MAKEINDEX_CMD_NAME for the part in the Makefile / make.bat.
+# The default value is: makeindex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_MAKEINDEX_CMD    = makeindex
+
+# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+COMPACT_LATEX          = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used by the
+# printer.
+# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x
+# 14 inches) and executive (7.25 x 10.5 inches).
+# The default value is: a4.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PAPER_TYPE             = a4
+
+# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names
+# that should be included in the LaTeX output. The package can be specified just
+# by its name or with the correct syntax as to be used with the LaTeX
+# \usepackage command. To get the times font for instance you can specify :
+# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times}
+# To use the option intlimits with the amsmath package you can specify:
+# EXTRA_PACKAGES=[intlimits]{amsmath}
+# If left blank no extra packages will be included.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+EXTRA_PACKAGES         =
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the
+# generated LaTeX document. The header should contain everything until the first
+# chapter. If it is left blank doxygen will generate a standard header. See
+# section "Doxygen usage" for information on how to let doxygen write the
+# default header to a separate file.
+#
+# Note: Only use a user-defined header if you know what you are doing! The
+# following commands have a special meaning inside the header: $title,
+# $datetime, $date, $doxygenversion, $projectname, $projectnumber,
+# $projectbrief, $projectlogo. Doxygen will replace $title with the empty
+# string, for the replacement values of the other commands the user is referred
+# to HTML_HEADER.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HEADER           =
+
+# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the
+# generated LaTeX document. The footer should contain everything after the last
+# chapter. If it is left blank doxygen will generate a standard footer. See
+# LATEX_HEADER for more information on how to generate a default footer and what
+# special commands can be used inside the footer.
+#
+# Note: Only use a user-defined footer if you know what you are doing!
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_FOOTER           =
+
+# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# LaTeX style sheets that are included after the standard style sheets created
+# by doxygen. Using this option one can overrule certain style aspects. Doxygen
+# will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list).
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_STYLESHEET =
+
+# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the LATEX_OUTPUT output
+# directory. Note that the files will be copied as-is; there are no commands or
+# markers available.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_FILES      =
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is
+# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will
+# contain links (just like the HTML output) instead of page references. This
+# makes the output suitable for online browsing using a PDF viewer.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PDF_HYPERLINKS         = YES
+
+# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate
+# the PDF file directly from the LaTeX files. Set this option to YES, to get a
+# higher quality PDF documentation.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+USE_PDFLATEX           = YES
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
+# command to the generated LaTeX files. This will instruct LaTeX to keep running
+# if errors occur, instead of asking the user for help. This option is also used
+# when generating formulas in HTML.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BATCHMODE        = NO
+
+# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the
+# index chapters (such as File Index, Compound Index, etc.) in the output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HIDE_INDICES     = NO
+
+# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source
+# code with syntax highlighting in the LaTeX output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_SOURCE_CODE      = NO
+
+# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
+# bibliography, e.g. plainnat, or ieeetr. See
+# https://en.wikipedia.org/wiki/BibTeX and \cite for more info.
+# The default value is: plain.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BIB_STYLE        = plain
+
+# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
+# page will contain the date and time when the page was generated. Setting this
+# to NO can help when comparing the output of multiple runs.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_TIMESTAMP        = NO
+
+# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)
+# path from which the emoji images will be read. If a relative path is entered,
+# it will be relative to the LATEX_OUTPUT directory. If left blank the
+# LATEX_OUTPUT directory will be used.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EMOJI_DIRECTORY  =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The
+# RTF output is optimized for Word 97 and may not look too pretty with other RTF
+# readers/editors.
+# The default value is: NO.
+
+GENERATE_RTF           = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: rtf.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_OUTPUT             = rtf
+
+# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+COMPACT_RTF            = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will
+# contain hyperlink fields. The RTF file will contain links (just like the HTML
+# output) instead of page references. This makes the output suitable for online
+# browsing using Word or some other Word compatible readers that support those
+# fields.
+#
+# Note: WordPad (write) and others do not support links.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_HYPERLINKS         = NO
+
+# Load stylesheet definitions from file. Syntax is similar to doxygen's
+# configuration file, i.e. a series of assignments. You only have to provide
+# replacements, missing definitions are set to their default value.
+#
+# See also section "Doxygen usage" for information on how to generate the
+# default style sheet that doxygen normally uses.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_STYLESHEET_FILE    =
+
+# Set optional variables used in the generation of an RTF document. Syntax is
+# similar to doxygen's configuration file. A template extensions file can be
+# generated using doxygen -e rtf extensionFile.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_EXTENSIONS_FILE    =
+
+# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code
+# with syntax highlighting in the RTF output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_SOURCE_CODE        = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for
+# classes and files.
+# The default value is: NO.
+
+GENERATE_MAN           = NO
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it. A directory man3 will be created inside the directory specified by
+# MAN_OUTPUT.
+# The default directory is: man.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_OUTPUT             = man
+
+# The MAN_EXTENSION tag determines the extension that is added to the generated
+# man pages. In case the manual section does not start with a number, the number
+# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is
+# optional.
+# The default value is: .3.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_EXTENSION          = .3
+
+# The MAN_SUBDIR tag determines the name of the directory created within
+# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by
+# MAN_EXTENSION with the initial . removed.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_SUBDIR             =
+
+# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it
+# will generate one additional man file for each entity documented in the real
+# man page(s). These additional files only source the real man page, but without
+# them the man command would be unable to find the correct page.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_LINKS              = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that
+# captures the structure of the code including all documentation.
+# The default value is: NO.
+
+GENERATE_XML           = NO
+
+# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: xml.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_OUTPUT             = xml
+
+# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program
+# listings (including syntax highlighting and cross-referencing information) to
+# the XML output. Note that enabling this will significantly increase the size
+# of the XML output.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_PROGRAMLISTING     = YES
+
+# If the XML_NS_MEMB_FILE_SCOPE tag is set to YES, doxygen will include
+# namespace members in file scope as well, matching the HTML output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_NS_MEMB_FILE_SCOPE = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the DOCBOOK output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files
+# that can be used to generate PDF.
+# The default value is: NO.
+
+GENERATE_DOCBOOK       = NO
+
+# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in
+# front of it.
+# The default directory is: docbook.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_OUTPUT         = docbook
+
+# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the
+# program listings (including syntax highlighting and cross-referencing
+# information) to the DOCBOOK output. Note that enabling this will significantly
+# increase the size of the DOCBOOK output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_PROGRAMLISTING = NO
+
+#---------------------------------------------------------------------------
+# Configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
+# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures
+# the structure of the code including all documentation. Note that this feature
+# is still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_AUTOGEN_DEF   = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module
+# file that captures the structure of the code including all documentation.
+#
+# Note that this feature is still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_PERLMOD       = NO
+
+# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary
+# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI
+# output from the Perl module output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_LATEX          = NO
+
+# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely
+# formatted so it can be parsed by a human reader. This is useful if you want to
+# understand what is going on. On the other hand, if this tag is set to NO, the
+# size of the Perl module output will be much smaller and Perl will parse it
+# just the same.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_PRETTY         = YES
+
+# The names of the make variables in the generated doxyrules.make file are
+# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful
+# so different doxyrules.make files included by the same Makefile don't
+# overwrite each other's variables.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_MAKEVAR_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all
+# C-preprocessor directives found in the sources and include files.
+# The default value is: YES.
+
+ENABLE_PREPROCESSING   = YES
+
+# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names
+# in the source code. If set to NO, only conditional compilation will be
+# performed. Macro expansion can be done in a controlled way by setting
+# EXPAND_ONLY_PREDEF to YES.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+MACRO_EXPANSION        = NO
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then
+# the macro expansion is limited to the macros specified with the PREDEFINED and
+# EXPAND_AS_DEFINED tags.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_ONLY_PREDEF     = NO
+
+# If the SEARCH_INCLUDES tag is set to YES, the include files in the
+# INCLUDE_PATH will be searched if a #include is found.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SEARCH_INCLUDES        = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that
+# contain include files that are not input files but should be processed by the
+# preprocessor.
+# This tag requires that the tag SEARCH_INCLUDES is set to YES.
+
+INCLUDE_PATH           =
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
+# patterns (like *.h and *.hpp) to filter out the header-files in the
+# directories. If left blank, the patterns specified with FILE_PATTERNS will be
+# used.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+INCLUDE_FILE_PATTERNS  =
+
+# The PREDEFINED tag can be used to specify one or more macro names that are
+# defined before the preprocessor is started (similar to the -D option of e.g.
+# gcc). The argument of the tag is a list of macros of the form: name or
+# name=definition (no spaces). If the definition and the "=" are omitted, "=1"
+# is assumed. To prevent a macro definition from being undefined via #undef or
+# recursively expanded use the := operator instead of the = operator.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+PREDEFINED             =
+
+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
+# tag can be used to specify a list of macro names that should be expanded. The
+# macro definition that is found in the sources will be used. Use the PREDEFINED
+# tag if you want to use a different macro definition that overrules the
+# definition found in the source code.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_AS_DEFINED      =
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
+# remove all references to function-like macros that are alone on a line, have
+# an all uppercase name, and do not end with a semicolon. Such function macros
+# are typically used for boiler-plate code, and will confuse the parser if not
+# removed.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SKIP_FUNCTION_MACROS   = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to external references
+#---------------------------------------------------------------------------
+
+# The TAGFILES tag can be used to specify one or more tag files. For each tag
+# file the location of the external documentation should be added. The format of
+# a tag file without this location is as follows:
+# TAGFILES = file1 file2 ...
+# Adding location for the tag files is done as follows:
+# TAGFILES = file1=loc1 "file2 = loc2" ...
+# where loc1 and loc2 can be relative or absolute paths or URLs. See the
+# section "Linking to external documentation" for more information about the use
+# of tag files.
+# Note: Each tag file must have a unique name (where the name does NOT include
+# the path). If a tag file is not located in the directory in which doxygen is
+# run, you must also specify the path to the tagfile here.
+
+TAGFILES               =
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create a
+# tag file that is based on the input files it reads. See section "Linking to
+# external documentation" for more information about the usage of tag files.
+
+GENERATE_TAGFILE       =
+
+# If the ALLEXTERNALS tag is set to YES, all external class will be listed in
+# the class index. If set to NO, only the inherited external classes will be
+# listed.
+# The default value is: NO.
+
+ALLEXTERNALS           = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
+# in the modules index. If set to NO, only the current project's groups will be
+# listed.
+# The default value is: YES.
+
+EXTERNAL_GROUPS        = YES
+
+# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in
+# the related pages index. If set to NO, only the current project's pages will
+# be listed.
+# The default value is: YES.
+
+EXTERNAL_PAGES         = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram
+# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to
+# NO turns the diagrams off. Note that this option also works with HAVE_DOT
+# disabled, but it is recommended to install and use dot, since it yields more
+# powerful graphs.
+# The default value is: YES.
+
+CLASS_DIAGRAMS         = YES
+
+# You can include diagrams made with dia in doxygen documentation. Doxygen will
+# then run dia to produce the diagram and insert it in the documentation. The
+# DIA_PATH tag allows you to specify the directory where the dia binary resides.
+# If left empty dia is assumed to be found in the default search path.
+
+DIA_PATH               =
+
+# If set to YES the inheritance and collaboration graphs will hide inheritance
+# and usage relations if the target is undocumented or is not a class.
+# The default value is: YES.
+
+HIDE_UNDOC_RELATIONS   = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
+# available from the path. This tool is part of Graphviz (see:
+# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
+# Bell Labs. The other options in this section have no effect if this option is
+# set to NO
+# The default value is: NO.
+
+HAVE_DOT               = NO
+
+# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed
+# to run in parallel. When set to 0 doxygen will base this on the number of
+# processors available in the system. You can set it explicitly to a value
+# larger than 0 to get control over the balance between CPU load and processing
+# speed.
+# Minimum value: 0, maximum value: 32, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_NUM_THREADS        = 0
+
+# When you want a differently looking font in the dot files that doxygen
+# generates you can specify the font name using DOT_FONTNAME. You need to make
+# sure dot is able to find the font, which can be done by putting it in a
+# standard location or by setting the DOTFONTPATH environment variable or by
+# setting DOT_FONTPATH to the directory containing the font.
+# The default value is: Helvetica.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTNAME           = Helvetica
+
+# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
+# dot graphs.
+# Minimum value: 4, maximum value: 24, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTSIZE           = 10
+
+# By default doxygen will tell dot to use the default font as specified with
+# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
+# the path where dot can find it using this tag.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTPATH           =
+
+# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for
+# each documented class showing the direct and indirect inheritance relations.
+# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CLASS_GRAPH            = YES
+
+# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
+# graph for each documented class showing the direct and indirect implementation
+# dependencies (inheritance, containment, and class references variables) of the
+# class with other documented classes.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+COLLABORATION_GRAPH    = YES
+
+# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
+# groups, showing the direct groups dependencies.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GROUP_GRAPHS           = YES
+
+# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and
+# collaboration diagrams in a style similar to the OMG's Unified Modeling
+# Language.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LOOK               = NO
+
+# If the UML_LOOK tag is enabled, the fields and methods are shown inside the
+# class node. If there are many fields or methods and many nodes the graph may
+# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the
+# number of items for each type to make the size more manageable. Set this to 0
+# for no limit. Note that the threshold may be exceeded by 50% before the limit
+# is enforced. So when you set the threshold to 10, up to 15 fields may appear,
+# but if the number exceeds 15, the total amount of fields shown is limited to
+# 10.
+# Minimum value: 0, maximum value: 100, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LIMIT_NUM_FIELDS   = 10
+
+# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and
+# collaboration graphs will show the relations between templates and their
+# instances.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+TEMPLATE_RELATIONS     = NO
+
+# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
+# YES then doxygen will generate a graph for each documented file showing the
+# direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDE_GRAPH          = YES
+
+# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
+# set to YES then doxygen will generate a graph for each documented file showing
+# the direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDED_BY_GRAPH      = YES
+
+# If the CALL_GRAPH tag is set to YES then doxygen will generate a call
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable call graphs for selected
+# functions only using the \callgraph command. Disabling a call graph can be
+# accomplished by means of the command \hidecallgraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALL_GRAPH             = NO
+
+# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable caller graphs for selected
+# functions only using the \callergraph command. Disabling a caller graph can be
+# accomplished by means of the command \hidecallergraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALLER_GRAPH           = NO
+
+# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical
+# hierarchy of all classes instead of a textual one.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GRAPHICAL_HIERARCHY    = YES
+
+# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
+# dependencies a directory has on other directories in a graphical way. The
+# dependency relations are determined by the #include relations between the
+# files in the directories.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DIRECTORY_GRAPH        = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
+# generated by dot. For an explanation of the image formats see the section
+# output formats in the documentation of the dot tool (Graphviz (see:
+# http://www.graphviz.org/)).
+# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
+# to make the SVG files visible in IE 9+ (other browsers do not have this
+# requirement).
+# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo,
+# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
+# png:gdiplus:gdiplus.
+# The default value is: png.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_IMAGE_FORMAT       = png
+
+# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to
+# enable generation of interactive SVG images that allow zooming and panning.
+#
+# Note that this requires a modern browser other than Internet Explorer. Tested
+# and working are Firefox, Chrome, Safari, and Opera.
+# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make
+# the SVG files visible. Older versions of IE do not have SVG support.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INTERACTIVE_SVG        = NO
+
+# The DOT_PATH tag can be used to specify the path where the dot tool can be
+# found. If left blank, it is assumed the dot tool can be found in the path.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_PATH               =
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that
+# contain dot files that are included in the documentation (see the \dotfile
+# command).
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOTFILE_DIRS           =
+
+# The MSCFILE_DIRS tag can be used to specify one or more directories that
+# contain msc files that are included in the documentation (see the \mscfile
+# command).
+
+MSCFILE_DIRS           =
+
+# The DIAFILE_DIRS tag can be used to specify one or more directories that
+# contain dia files that are included in the documentation (see the \diafile
+# command).
+
+DIAFILE_DIRS           =
+
+# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the
+# path where java can find the plantuml.jar file. If left blank, it is assumed
+# PlantUML is not used or called during a preprocessing step. Doxygen will
+# generate a warning when it encounters a \startuml command in this case and
+# will not generate output for the diagram.
+
+PLANTUML_JAR_PATH      =
+
+# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a
+# configuration file for plantuml.
+
+PLANTUML_CFG_FILE      =
+
+# When using plantuml, the specified paths are searched for files specified by
+# the !include statement in a plantuml block.
+
+PLANTUML_INCLUDE_PATH  =
+
+# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes
+# that will be shown in the graph. If the number of nodes in a graph becomes
+# larger than this value, doxygen will truncate the graph, which is visualized
+# by representing a node as a red box. Note that doxygen if the number of direct
+# children of the root node in a graph is already larger than
+# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that
+# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.
+# Minimum value: 0, maximum value: 10000, default value: 50.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_GRAPH_MAX_NODES    = 50
+
+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs
+# generated by dot. A depth value of 3 means that only nodes reachable from the
+# root by following a path via at most 3 edges will be shown. Nodes that lay
+# further from the root node will be omitted. Note that setting this option to 1
+# or 2 may greatly reduce the computation time needed for large code bases. Also
+# note that the size of a graph can be further restricted by
+# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.
+# Minimum value: 0, maximum value: 1000, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+MAX_DOT_GRAPH_DEPTH    = 0
+
+# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
+# background. This is disabled by default, because dot on Windows does not seem
+# to support this out of the box.
+#
+# Warning: Depending on the platform used, enabling this option may lead to
+# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
+# read).
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_TRANSPARENT        = NO
+
+# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
+# files in one run (i.e. multiple -o and -T options on the command line). This
+# makes dot run faster, but since only newer versions of dot (>1.8.10) support
+# this, this feature is disabled by default.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_MULTI_TARGETS      = NO
+
+# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
+# explaining the meaning of the various boxes and arrows in the dot generated
+# graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GENERATE_LEGEND        = YES
+
+# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot
+# files that are used to generate the various graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_CLEANUP            = YES
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..b4fb898
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Rive
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2ef3d1a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,81 @@
+![Build Status](https://github.com/rive-app/rive-cpp/actions/workflows/tests.yml/badge.svg) 
+![Discord badge](https://img.shields.io/discord/532365473602600965)
+![Twitter handle](https://img.shields.io/twitter/follow/rive_app.svg?style=social&label=Follow)
+
+
+# rive-cpp
+
+![Rive hero image](https://cdn.rive.app/rive_logo_dark_bg.png)
+
+Rive C++ is a runtime library for [Rive](https://rive.app), a real-time interactive design and animation tool.
+
+The C++ runtime for Rive provides these runtime features:
+- Loading Artboards and their contents from **.riv** files.
+- Querying LinearAnimations and StateMachines from Artboards.
+- Making changes to Artboard hierarchy (fundamentally same guts used by LinearAnimations and StateMachines) and effienclty solving those changes via Artboard::advance.
+- Abstract Renderer for submitting high level vector path commands with retained path objects to optimize and minimize path re-computation (ultimately up to the concrete rendering implementation).
+- Example concrete renderer written in C++ with [Skia](https://skia.org/). Skia renderer code is in [skia/renderer/src/skia_factory.cpp](skia/renderer/src/skia_factory.cpp).
+
+## Build system
+We use [premake5](https://premake.github.io/). The Rive dev team primarily works on MacOS. There is some work done by the community to also support Windows and Linux. PRs welcomed for specific platforms you wish to support! We encourage you to use premake as it's highly extensible and configurable for a variety of platforms.
+
+## Build
+In the ```rive-cpp``` directory, run ```build.sh``` to debug build and ```build.sh release``` for a release build.
+
+If you've put the `premake5` executable in the `rive-cpp/build` folder, you can run it with `PATH=.:$PATH ./build.sh`
+
+Rive makes use of clang [vector builtins](https://reviews.llvm.org/D111529), which are, as of 2022, still a work in progress. Please use clang and ensure you have the latest version.
+
+## Building skia projects
+```
+cd skia/dependencies
+./make_skia.sh      // this will invoke get_skia.sh
+```
+To build viewer (plus you'll needed CMake installed)
+```
+./make_viewer_dependencies.sh
+```
+
+## Testing
+Uses the [Catch2](https://github.com/catchorg/Catch2) testing framework.
+
+```
+cd dev
+./test.sh
+```
+
+In the ```dev``` directory, run ```test.sh``` to compile and execute the tests.
+
+(if you've installed `premake5` in `rive-cpp/build`, you can run it with `PATH=../../build:$PATH ./test.sh`)
+
+The tests live in ```rive/test```. To add new tests, create a new ```xxx_test.cpp``` file here. The test harness will automatically pick up the new file.
+
+There's a VSCode command provided to ```run tests``` from the Tasks: Run Task command palette. 
+
+## Code formatting
+rive-cpp uses clang-format, you can install it with brew on MacOS: ```brew install clang-format```.
+
+## Memory checks
+Note that if you're on MacOS you'll want to install valgrind, which is somewhat complicated these days. This is the easiest solution (please PR a better one when it becomes available).
+
+```
+brew tap LouisBrunner/valgrind
+brew install --HEAD LouisBrunner/valgrind/valgrind
+```
+
+You can now run the all the tests through valgrind by running ```test.sh memory```.
+
+## Disassembly explorer
+If you want to examine the generated assembly code per cpp file, install [Disassembly Explorer](https://marketplace.visualstudio.com/items?itemName=dseight.disasexpl) in VSCode.
+
+A ```disassemble``` task is provided to compile and preview the generated assembly. You can reach it via the Tasks: Run Task command palette or you can bind it to a shortcut by editing your VSCode keybindings.json:
+
+```
+[
+    {
+        "key": "cmd+d",
+        "command": "workbench.action.tasks.runTask",
+        "args": "disassemble"
+    }
+]
+```
diff --git a/build.sh b/build.sh
new file mode 100755
index 0000000..e89795d
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,88 @@
+#!/bin/bash
+set -e
+
+source dependencies/config_directories.sh
+pushd build &>/dev/null
+
+while getopts p: flag; do
+    case "${flag}" in
+    p)
+        shift 2
+        platform=${OPTARG}
+        ;;
+    \?) help ;;
+    esac
+done
+
+help() {
+    echo build.sh - build debug library
+    echo build.sh clean - clean the build
+    echo build.sh release - build release library
+    echo build.sh -p ios release - build release ios library
+    echo build.sh -p ios_sim release - build release ios simulator library
+    echo build.sh -p android release - build release android library
+    exit 1
+}
+
+# make sure argument is lowercase
+OPTION="$(echo "$1" | tr '[A-Z]' '[a-z]')"
+
+if [ "$OPTION" = 'help' ]; then
+    help
+else
+    build() {
+        echo "Building Rive for platform=$platform option=$OPTION"
+        echo premake5 gmake2 --with_rive_text --with_rive_audio=system --with_rive_layout "$1"
+        PREMAKE="premake5 gmake2 --with_rive_text --with_rive_audio=system --with_rive_layout $1"
+        eval "$PREMAKE"
+        if [ "$OPTION" = "clean" ]; then
+            make clean
+            make clean config=release
+        elif [ "$OPTION" = "release" ]; then
+            make config=release -j7
+        else
+            make config=debug -j7
+        fi
+    }
+
+    case $platform in
+    ios)
+        echo "Building for iOS"
+        export IOS_SYSROOT=$(xcrun --sdk iphoneos --show-sdk-path)
+        build "--os=ios"
+        if [ "$OPTION" = "clean" ]; then
+            exit
+        fi
+        ;;
+    ios_sim)
+        export IOS_SYSROOT=$(xcrun --sdk iphonesimulator --show-sdk-path)
+        build "--os=ios --variant=emulator"
+        if [ "$OPTION" = "clean" ]; then
+            exit
+        fi
+        ;;
+    # Android supports ABIs via a custom platform format:
+    #   e.g. 'android.x86', 'android.x64', etc.
+    android*)
+        echo "Building for ${platform}"
+        # Extract ABI from this opt by splitting on '.' character
+        #   e.g. android.x86
+        IFS="." read -ra strarr <<<"$platform"
+        ARCH=${strarr[1]}
+        build "--os=android --arch=${ARCH}"
+        ;;
+    macosx)
+        echo "Building for macos"
+        export MACOS_SYSROOT=$(xcrun --sdk macosx --show-sdk-path)
+        build "--os=macosx --variant=runtime"
+        if [ "$OPTION" = "clean" ]; then
+            exit
+        fi
+        ;;
+    *)
+        build
+        ;;
+    esac
+fi
+
+popd &>/dev/null
diff --git a/build/build_rive.bat b/build/build_rive.bat
new file mode 100644
index 0000000..a0d7a19
--- /dev/null
+++ b/build/build_rive.bat
@@ -0,0 +1,10 @@
+@echo off
+REM we could check where vs is install via the registery like stated here https://superuser.com/questions/539666/how-to-get-the-visual-studio-installation-path-in-a-batch-file but i think that is overkill
+if exist "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat" CALL "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat"
+else (
+    if exist CALL "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat"
+    else echo "Visual Studio 2022 does not appear to be installed, please install visual studio to C:\Program Files\Microsoft Visual Studio"
+)
+
+cd %CD%
+sh build_rive.sh %*
diff --git a/build/build_rive.ps1 b/build/build_rive.ps1
new file mode 100644
index 0000000..b223f9c
--- /dev/null
+++ b/build/build_rive.ps1
@@ -0,0 +1,15 @@
+# requires Set-ExecutionPolicy Bypass -Scope CurrentUser
+$CWD = Get-Location
+if (Test-Path "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\Launch-VsDevShell.ps1" -PathType Leaf) {
+    & "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\Launch-VsDevShell.ps1"
+} else {
+    if ("C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\Launch-VsDevShell.ps1") {
+        & 'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\Launch-VsDevShell.ps1'
+    }
+    else {
+        Write-Error "Visual Studio 2022 does not appear to be installed, please install visual studio to C:\Program Files\Microsoft Visual Studio"
+        exit
+    }
+}
+Set-Location $CWD
+sh build_rive.sh $args
\ No newline at end of file
diff --git a/build/build_rive.sh b/build/build_rive.sh
new file mode 100755
index 0000000..e091ec0
--- /dev/null
+++ b/build/build_rive.sh
@@ -0,0 +1,293 @@
+#!/bin/bash
+
+# build_rive.sh: build any standard Rive sub-project.
+#
+# To use this script:
+#
+#   1) add packages/runtime/build to $PATH
+#   2) cd to directory with a premake5.lua
+#   3) run build_rive.sh with desired flags (in any order)
+#
+# Build for the host machine:
+#
+#   build_rive.sh                      # debug build
+#   build_rive.sh release              # release build
+#   build_rive.sh release clean        # clean, followed by a release build
+#   build_rive.sh xcode                # generate and build an xcode workspace
+#   build_rive.sh ninja                # use ninja (experimental) for a debug build
+#   build_rive.sh ninja release        # use ninja (experimental) for a release build
+#   build_rive.sh ninja --with_vulkan  # extra parameters get forwarded to premake
+#
+# Generate compile_commands.json for code completion engines:
+#
+#   build_rive.sh compdb                        # code completion for a host build
+#   build_rive.sh compdb android --with_vulkan  # code completion for android with vulkan enabled
+#
+# Specify build targets after "--":
+#
+#   build_rive.sh -- gms goldens
+#   build_rive.sh ninja release -- imagediff
+#
+# Build for android:
+#
+#   build_rive.sh android            # debug build, defaults to arm64
+#   build_rive.sh ninja android arm  # release arm32 build using ninja
+#
+# Build for ios:
+#
+#   build_rive.sh ios          # debug build, arm64 only
+#   build_rive.sh ios release  # release build, arm64 only
+#   build_rive.sh iossim       # debug build, defaults to "universal" (x64 and arm64) architecture
+#
+# Build for wasm:
+#
+#   build_rive.sh ninja release wasm                # release build
+#   build_rive.sh ninja release wasm --with-webgpu  # release build, also enable webgpu
+#
+# Incremental builds:
+#
+#   build_rive.sh                            # initial build
+#   build_rive.sh                            # any repeat command does an incremental build
+#   build_rive.sh --with_vulkan <- FAILS!!   # a repeat build with different arguments fails
+#   build_rive.sh clean --with_vulkan        # a clean build with different arguments is OK
+#   build_rive.sh rebuild out/debug  # use "rebuild" on a specific OUT directory to relaunch the
+#                                    # build with whatever args it got configured with initially
+#   build_rive.sh rebuild out/debug gms goldens  # args after OUT get forwarded to the buildsystem
+
+set -e
+set -o pipefail
+
+# Detect where build_rive.sh is located on disk.
+# https://stackoverflow.com/questions/59895/how-do-i-get-the-directory-where-a-bash-script-is-located-from-within-the-script
+SOURCE=${BASH_SOURCE[0]}
+while [ -L "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
+    DIR=$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )
+    SOURCE=$(readlink "$SOURCE")
+    # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
+    [[ $SOURCE != /* ]] && SOURCE=$DIR/$SOURCE
+done
+SCRIPT_DIR=$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )
+
+case "$(uname -s)" in
+    Darwin*)
+        if [[ $(arch) = "arm64" ]]; then
+            HOST_MACHINE="mac_arm64"
+        else
+            HOST_MACHINE="mac_x64"
+        fi
+        NUM_CORES=$(($(sysctl -n hw.physicalcpu) + 1))
+        ;;
+    MINGW*|MSYS*)
+        HOST_MACHINE="windows"
+        NUM_CORES=$NUMBER_OF_PROCESSORS
+        ;;
+    Linux*)
+        HOST_MACHINE="linux"
+        NUM_CORES=$(grep -c processor /proc/cpuinfo)
+        ;;
+esac
+
+if [[ "$1" = "rebuild" ]]; then
+    # Load args from an existing build.
+    RIVE_OUT=$2
+    shift
+    shift
+
+    if [ ! -d $RIVE_OUT ]; then
+        echo "OUT directory '$RIVE_OUT' not found."
+        exit -1
+    fi
+
+    ARGS_FILE=$RIVE_OUT/.rive_premake_args
+    if [ ! -f $ARGS_FILE ]; then
+        echo "Premake args file '$ARGS_FILE' not found."
+        exit -1
+    fi
+
+    RIVE_PREMAKE_ARGS="$(< $ARGS_FILE)"
+    RIVE_BUILD_SYSTEM="$(awk '{print $1}' $ARGS_FILE)" # RIVE_BUILD_SYSTEM is the first word in the args.
+    if grep -e "--arch=" $ARGS_FILE > /dev/null; then
+        RIVE_ARCH="$(sed -r 's/^.*--arch=([^ ]*).*$/\1/' $ARGS_FILE)"
+    fi
+else
+    # New build. Parse arguments into premake options.
+    RIVE_PREMAKE_FILE="${RIVE_PREMAKE_FILE:-./premake5.lua}"
+    if [ ! -f "$RIVE_PREMAKE_FILE" ]; then
+        echo "Premake file "$RIVE_PREMAKE_FILE" not found"
+        exit -1
+    fi
+
+    # Only use default arguments if RIVE_PREMAKE_ARGS is unset (not just empty).
+    if [ -z "${RIVE_PREMAKE_ARGS+null_detector_string}" ]; then
+        RIVE_PREMAKE_ARGS="--with_rive_text --with_rive_layout"
+    fi
+
+    while [[ $# -gt 0 ]]; do
+        case "$1" in
+            "release") RIVE_CONFIG="${RIVE_CONFIG:-release}" ;;
+            "ios") RIVE_OS="${RIVE_OS:-ios}" ;;
+            "iossim")
+                RIVE_OS=${RIVE_OS:-ios}
+                RIVE_VARIANT="${RIVE_VARIANT:-emulator}"
+                RIVE_ARCH="${RIVE_ARCH:-universal}" # The simulator requires universal builds.
+                ;;
+            "android") RIVE_OS="${RIVE_OS:-android}" ;;
+            "arm") RIVE_ARCH="${RIVE_ARCH:-arm}" ;;
+            "arm64") RIVE_ARCH="${RIVE_ARCH:-arm64}" ;;
+            "x64") RIVE_ARCH="${RIVE_ARCH:-x64}" ;;
+            "universal") RIVE_ARCH="${RIVE_ARCH:-universal}" ;;
+            "wasm") RIVE_ARCH="${RIVE_ARCH:-wasm}" ;;
+            "ninja") RIVE_BUILD_SYSTEM="${RIVE_BUILD_SYSTEM:-ninja}" ;;
+            "xcode") RIVE_BUILD_SYSTEM="${RIVE_BUILD_SYSTEM:-xcode4}" ;;
+            "clean") RIVE_CLEAN="${RIVE_CLEAN:-true}" ;;
+            "compdb")
+                RIVE_BUILD_SYSTEM="${RIVE_BUILD_SYSTEM:-export-compile-commands}"
+                RIVE_OUT="${RIVE_OUT:-out/compdb}"
+                ;;
+            "--")
+                shift
+                break
+                ;;
+            *) RIVE_PREMAKE_ARGS="$RIVE_PREMAKE_ARGS $1" ;;
+        esac
+        shift
+    done
+
+    # Android requires an architecture. Default to arm64 if not specified.
+    if [[ $RIVE_OS = "android" ]]; then
+        RIVE_ARCH="${RIVE_ARCH:-arm64}"
+    fi
+
+    RIVE_CONFIG="${RIVE_CONFIG:-debug}"
+
+    if [ -z "$RIVE_OUT" ]; then
+        RIVE_OUT="$RIVE_CONFIG"
+        if [ ! -z "$RIVE_ARCH" ]; then
+            RIVE_OUT="${RIVE_ARCH}_$RIVE_OUT"
+        fi
+        if [[ $RIVE_VARIANT = "emulator" ]]; then
+            RIVE_OUT="${RIVE_OS}sim_$RIVE_OUT"
+        elif [ ! -z "$RIVE_OS" ]; then
+            RIVE_OUT="${RIVE_OS}_$RIVE_OUT"
+        fi
+        RIVE_OUT="out/$RIVE_OUT"
+    fi
+
+    if [[ "$HOST_MACHINE" = "windows" ]]; then
+        RIVE_BUILD_SYSTEM="${RIVE_BUILD_SYSTEM:-vs2022}"
+    else
+        RIVE_BUILD_SYSTEM="${RIVE_BUILD_SYSTEM:-gmake2}"
+    fi
+
+    RIVE_PREMAKE_ARGS="$RIVE_BUILD_SYSTEM --file=$RIVE_PREMAKE_FILE --config=$RIVE_CONFIG --out=$RIVE_OUT $RIVE_PREMAKE_ARGS"
+    if [ ! -z "$RIVE_OS" ]; then RIVE_PREMAKE_ARGS="$RIVE_PREMAKE_ARGS --os=$RIVE_OS"; fi
+    if [ ! -z "$RIVE_VARIANT" ]; then RIVE_PREMAKE_ARGS="$RIVE_PREMAKE_ARGS --variant=$RIVE_VARIANT"; fi
+    if [ ! -z "$RIVE_ARCH" ]; then RIVE_PREMAKE_ARGS="$RIVE_PREMAKE_ARGS --arch=$RIVE_ARCH"; fi
+
+    if [[ "$RIVE_CLEAN" = true ]]; then
+        rm -fr "./$RIVE_OUT"
+    fi
+fi
+
+mkdir -p "$SCRIPT_DIR/dependencies"
+pushd "$SCRIPT_DIR/dependencies" > /dev/null
+
+# Setup premake5.
+if [ ! -d premake-core ]; then
+    echo Building Premake...
+    git clone https://github.com/premake/premake-core.git
+    pushd premake-core > /dev/null
+    case "$HOST_MACHINE" in
+        mac_arm64) make -f Bootstrap.mak osx PLATFORM=ARM ;;
+        mac_x64) make -f Bootstrap.mak osx ;;
+        windows) ./Bootstrap.bat ;;
+        *) make -f Bootstrap.mak linux ;;
+    esac
+    popd > /dev/null
+fi
+export PATH="$SCRIPT_DIR/dependencies/premake-core/bin/release/:$PATH"
+export PREMAKE_PATH="$SCRIPT_DIR"
+
+# Setup premake-ninja.
+if [[ $RIVE_BUILD_SYSTEM = "ninja" ]]; then
+    if [ ! -d premake-ninja ]; then
+        git clone --branch rive_modifications https://github.com/rive-app/premake-ninja.git
+    fi
+    export PREMAKE_PATH="$SCRIPT_DIR/dependencies/premake-ninja:$PREMAKE_PATH"
+fi
+
+# Setup premake-export-compile-commands.
+if [[ $RIVE_BUILD_SYSTEM = "export-compile-commands" ]]; then
+    if [ ! -d premake-export-compile-commands ]; then
+        git clone --branch more_cpp_support https://github.com/rive-app/premake-export-compile-commands.git
+    fi
+    export PREMAKE_PATH="$SCRIPT_DIR/dependencies/premake-export-compile-commands:$PREMAKE_PATH"
+fi
+
+# Setup emscripten.
+if [[ $RIVE_ARCH = "wasm" ]]; then
+    if [ ! -d emsdk ]; then
+        git clone https://github.com/emscripten-core/emsdk.git
+        emsdk/emsdk install 3.1.61
+        emsdk/emsdk activate 3.1.61
+    fi
+    source emsdk/emsdk_env.sh
+fi
+
+popd > /dev/null # leave "$SCRIPT_DIR/dependencies"
+
+if [ -d "$RIVE_OUT" ]; then
+    if [[ "$RIVE_PREMAKE_ARGS" != "$(< $RIVE_OUT/.rive_premake_args)" ]]; then
+        echo "error: premake5 arguments for current build do not match previous arguments"
+        echo "  previous command: premake5 $(< $RIVE_OUT/.rive_premake_args)"
+        echo "   current command: premake5 $RIVE_PREMAKE_ARGS"
+        echo "If you wish to overwrite the existing build, please use 'clean'"
+        exit -1
+    fi
+else
+    mkdir -p "$RIVE_OUT"
+    echo "$RIVE_PREMAKE_ARGS" > "$RIVE_OUT/.rive_premake_args"
+fi
+
+echo premake5 $RIVE_PREMAKE_ARGS
+premake5 $RIVE_PREMAKE_ARGS | grep -v '^Done ([1-9]*ms).$'
+
+case "$RIVE_BUILD_SYSTEM" in
+    export-compile-commands)
+        rm -f ./compile_commands.json
+        cp $RIVE_OUT/compile_commands/default.json compile_commands.json
+        wc ./compile_commands.json
+        ;;
+    gmake2)
+        echo make -C $RIVE_OUT -j$NUM_CORES $@
+        make -C $RIVE_OUT -j$NUM_CORES $@
+        ;;
+    ninja)
+        echo ninja -C $RIVE_OUT $@
+        ninja -C $RIVE_OUT $@
+        ;;
+    xcode4)
+        if [[ $# = 0 ]]; then
+            echo 'No targets specified for xcode: Attempting to grok them from "xcodebuild -list".'
+            XCODE_SCHEMES=$(for f in $(xcodebuild -list -workspace $RIVE_OUT/rive.xcworkspace | grep '^        '); do printf " $f"; done)
+            echo "  -> grokked:$XCODE_SCHEMES"
+        else
+            XCODE_SCHEMES="$@"
+        fi
+        for SCHEME in $XCODE_SCHEMES; do
+            echo xcodebuild -workspace $RIVE_OUT/rive.xcworkspace -scheme $SCHEME
+            xcodebuild -workspace $RIVE_OUT/rive.xcworkspace -scheme $SCHEME
+        done
+        ;;
+    vs2022)
+        for TARGET in $@; do
+            MSVC_TARGETS="$MSVC_TARGETS -t:$TARGET"
+        done
+        echo msbuild.exe "./$RIVE_OUT/rive.sln" $MSVC_TARGETS
+        msbuild.exe "./$RIVE_OUT/rive.sln" $MSVC_TARGETS
+        ;;
+    *)
+        echo "Unsupported buildsystem $RIVE_BUILD_SYSTEM"
+        exit -1
+        ;;
+esac
diff --git a/build/dependency.lua b/build/dependency.lua
new file mode 100644
index 0000000..95f6826
--- /dev/null
+++ b/build/dependency.lua
@@ -0,0 +1,55 @@
+local m = {}
+
+local last_str = ''
+
+function iop(str)
+    io.write(('\b \b'):rep(#last_str)) -- erase old line
+    io.write(str) -- write new line
+    io.flush()
+    last_str = str
+end
+
+newoption({ trigger = 'no-download-progress', description = 'Hide progress?' })
+
+function m.github(project, tag)
+    local dependencies = os.getenv('DEPENDENCIES')
+    if dependencies == nil then
+        dependencies = path.getabsolute(_WORKING_DIR) .. '/dependencies'
+        os.mkdir(dependencies)
+    end
+    local hash = string.sub(string.sha1(project .. tag), 0, 9)
+    if not os.isdir(dependencies .. '/' .. hash) then
+        function progress(total, current)
+            local ratio = current / total
+            ratio = math.min(math.max(ratio, 0), 1)
+            local percent = math.floor(ratio * 100)
+            if total == current then
+                iop('')
+            else
+                iop('Downloading ' .. project .. ' ' .. percent .. '%')
+            end
+        end
+
+        local downloadFilename = dependencies .. '/' .. hash .. '.zip'
+        http.download(
+            'https://github.com/' .. project .. '/archive/' .. tag .. '.zip',
+            downloadFilename,
+            -- Download progress explodes the github logs with megabytes of text.
+            -- github runners have a "CI" environment variable.
+            {
+                progress = not _OPTIONS['no-download-progress']
+                    and os.getenv('CI') ~= 'true'
+                    and progress,
+            }
+        )
+        print('Downloaded ' .. project .. ' to ' .. dependencies .. '/' .. hash)
+        zip.extract(downloadFilename, dependencies .. '/' .. hash)
+        os.remove(downloadFilename)
+    end
+    local dirs = os.matchdirs(dependencies .. '/' .. hash .. '/*')
+
+    local iter = pairs(dirs)
+    local currentKey, currentValue = iter(dirs)
+    return currentValue
+end
+return m
diff --git a/build/premake5.lua b/build/premake5.lua
new file mode 100644
index 0000000..66c7d20
--- /dev/null
+++ b/build/premake5.lua
@@ -0,0 +1,220 @@
+workspace('rive')
+configurations({ 'debug', 'release' })
+
+require('setup_compiler')
+
+filter({ 'options:with_rive_tools' })
+do
+    defines({ 'WITH_RIVE_TOOLS' })
+end
+filter({ 'options:with_rive_text' })
+do
+    defines({ 'WITH_RIVE_TEXT' })
+end
+filter({ 'options:with_rive_audio=system' })
+do
+    defines({ 'WITH_RIVE_AUDIO', 'MA_NO_RESOURCE_MANAGER' })
+end
+
+filter({ 'options:with_rive_audio=external' })
+do
+    defines({
+        'WITH_RIVE_AUDIO',
+        'EXTERNAL_RIVE_AUDIO_ENGINE',
+        'MA_NO_DEVICE_IO',
+        'MA_NO_RESOURCE_MANAGER',
+    })
+end
+filter({ 'options:with_rive_layout' })
+do
+    defines({ 'WITH_RIVE_LAYOUT' })
+end
+filter({})
+
+dofile(path.join(path.getabsolute('../dependencies/'), 'premake5_harfbuzz.lua'))
+dofile(path.join(path.getabsolute('../dependencies/'), 'premake5_sheenbidi.lua'))
+dofile(path.join(path.getabsolute('../dependencies/'), 'premake5_miniaudio.lua'))
+dofile(path.join(path.getabsolute('../dependencies/'), 'premake5_yoga.lua'))
+
+project('rive')
+do
+    kind('StaticLib')
+    language('C++')
+    cppdialect('C++11')
+    targetdir('%{cfg.system}/bin/%{cfg.buildcfg}')
+    objdir('%{cfg.system}/obj/%{cfg.buildcfg}')
+    includedirs({
+        '../include',
+        harfbuzz .. '/src',
+        sheenbidi .. '/Headers',
+        miniaudio,
+        yoga,
+    })
+
+    defines({ 'YOGA_EXPORT=' })
+
+    files({ '../src/**.cpp' })
+
+    flags({ 'FatalCompileWarnings' })
+
+    filter({ 'system:macosx' })
+    do
+        buildoptions({
+            -- this triggers too much on linux, so just enable here for now
+            '-Wimplicit-float-conversion',
+        })
+    end
+
+    -- filter {'toolset:not msc', 'files:../src/audio/audio_engine.cpp'}
+    filter({ 'system:not windows', 'files:../src/audio/audio_engine.cpp' })
+    do
+        buildoptions({ '-Wno-implicit-int-conversion' })
+    end
+
+    filter({ 'system:windows', 'files:../src/audio/audio_engine.cpp' })
+    do
+        -- Too many warnings from miniaudio.h
+        removeflags({ 'FatalCompileWarnings' })
+    end
+
+    -- filter 'files:../src/audio/audio_engine.cpp'
+    -- do
+    --     buildoptions {
+    --         '-Wno-implicit-int-conversion'
+    --     }
+    -- end
+
+    filter({ 'system:macosx', 'options:variant=runtime' })
+    do
+        buildoptions({
+            '-Wimplicit-float-conversion -fembed-bitcode -arch arm64 -arch x86_64 -isysroot '
+                .. (os.getenv('MACOS_SYSROOT') or ''),
+        })
+    end
+
+    filter({ 'system:macosx', 'configurations:release' })
+    do
+        buildoptions({ '-flto=full' })
+    end
+
+    filter({ 'system:ios' })
+    do
+        buildoptions({ '-flto=full', '-Wno-implicit-int-conversion' })
+        files({ '../src/audio/audio_engine.m' })
+    end
+
+    filter('system:windows')
+    do
+        architecture('x64')
+        defines({ '_USE_MATH_DEFINES' })
+    end
+
+    filter({ 'system:ios', 'options:variant=system' })
+    do
+        buildoptions({
+            '-mios-version-min=13.0 -fembed-bitcode -arch arm64 -isysroot '
+                .. (os.getenv('IOS_SYSROOT') or ''),
+        })
+    end
+
+    filter({ 'system:ios', 'options:variant=emulator' })
+    do
+        buildoptions({
+            '--target=arm64-apple-ios13.0.0-simulator',
+            '-mios-version-min=13.0 -arch arm64 -arch x86_64 -isysroot '
+                .. (os.getenv('IOS_SYSROOT') or ''),
+        })
+        targetdir('%{cfg.system}_sim/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}_sim/obj/%{cfg.buildcfg}')
+    end
+
+    filter('system:macosx or system:ios')
+    do
+        files({ '../src/text/font_hb_apple.mm' })
+    end
+
+    filter({ 'system:android', 'configurations:release' })
+    do
+        buildoptions({ '-flto=full' })
+    end
+
+    -- Is there a way to pass 'arch' as a variable here?
+    filter({ 'system:android', 'options:arch=x86' })
+    do
+        targetdir('%{cfg.system}/x86/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}/x86/obj/%{cfg.buildcfg}')
+        -- Ignore fatal warning for miniaudio on x86 devices.
+        filter({ 'files:../src/audio/audio_engine.cpp' })
+        do
+            buildoptions({ '-Wno-atomic-alignment' })
+        end
+    end
+
+    filter({ 'system:android', 'options:arch=x64' })
+    do
+        targetdir('%{cfg.system}/x64/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}/x64/obj/%{cfg.buildcfg}')
+    end
+
+    filter({ 'system:android', 'options:arch=arm' })
+    do
+        targetdir('%{cfg.system}/arm/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}/arm/obj/%{cfg.buildcfg}')
+    end
+
+    filter({ 'system:android', 'options:arch=arm64' })
+    do
+        targetdir('%{cfg.system}/arm64/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}/arm64/obj/%{cfg.buildcfg}')
+    end
+
+    filter('configurations:debug')
+    do
+        defines({ 'DEBUG' })
+        symbols('On')
+    end
+
+    filter('configurations:release')
+    do
+        defines({ 'RELEASE' })
+        defines({ 'NDEBUG' })
+        optimize('On')
+    end
+end
+
+newoption({
+    trigger = 'variant',
+    value = 'type',
+    description = 'Choose a particular variant to build',
+    allowed = {
+        { 'system', 'Builds the static library for the provided system' },
+        { 'emulator', 'Builds for an emulator/simulator for the provided system' },
+        {
+            'runtime',
+            'Build the static library specifically targeting our runtimes',
+        },
+    },
+    default = 'system',
+})
+
+newoption({
+    trigger = 'with_rive_tools',
+    description = 'Enables tools usually not necessary for runtime.',
+})
+
+newoption({
+    trigger = 'with_rive_text',
+    description = 'Compiles in text features.',
+})
+
+newoption({
+    trigger = 'with_rive_audio',
+    value = 'disabled',
+    description = 'The audio mode to use.',
+    allowed = { { 'disabled' }, { 'system' }, { 'external' } },
+})
+
+newoption({
+    trigger = 'with_rive_layout',
+    description = 'Compiles in layout features.',
+})
diff --git a/build/rive_build_config.lua b/build/rive_build_config.lua
new file mode 100644
index 0000000..9e2a7bf
--- /dev/null
+++ b/build/rive_build_config.lua
@@ -0,0 +1,583 @@
+if _ACTION == 'ninja' then
+    require('ninja')
+end
+
+if _ACTION == 'export-compile-commands' then
+    require('export-compile-commands')
+end
+
+workspace('rive')
+
+newoption({
+    trigger = 'config',
+    description = 'one-and-only config (for multiple configs, target a new --out directory)',
+    allowed = { { 'debug' }, { 'release' }, { nil } },
+    default = nil,
+})
+newoption({ trigger = 'release', description = 'shortcut for \'--config=release\'' })
+if _OPTIONS['release'] then
+    if _OPTIONS['config'] then
+        error('use either \'--release\' or \'--config=release/debug\' (not both)')
+    end
+    _OPTIONS['config'] = 'release'
+    _OPTIONS['release'] = nil
+elseif not _OPTIONS['config'] then
+    _OPTIONS['config'] = 'debug'
+end
+RIVE_BUILD_CONFIG = _OPTIONS['config']
+
+newoption({
+    trigger = 'out',
+    description = 'Directory to generate build files',
+    default = nil,
+})
+RIVE_BUILD_OUT = _WORKING_DIR .. '/' .. (_OPTIONS['out'] or ('out/' .. RIVE_BUILD_CONFIG))
+
+newoption({
+    trigger = 'toolset',
+    value = 'type',
+    description = 'Choose which toolchain to build with',
+    allowed = {
+        { 'clang', 'Build with Clang' },
+        { 'msc', 'Build with the Microsoft C/C++ compiler' },
+    },
+    default = 'clang',
+})
+
+newoption({
+    trigger = 'arch',
+    value = 'ABI',
+    description = 'The ABI with the right toolchain for this build, generally with Android',
+    allowed = {
+        { 'host' },
+        { 'x86' },
+        { 'x64' },
+        { 'arm' },
+        { 'arm64' },
+        { 'universal', '"fat" library on apple platforms' },
+        { 'wasm', 'emscripten targeting web assembly' },
+        { 'js', 'emscripten targeting javascript' },
+    },
+    default = 'host',
+})
+
+newoption({
+    trigger = 'variant',
+    value = 'type',
+    description = 'Choose a particular variant to build',
+    allowed = {
+        { 'system', 'Builds the static library for the provided system' },
+        { 'emulator', 'Builds for a simulator for the provided system' },
+    },
+    default = 'system',
+})
+
+newoption({
+    trigger = 'with-rtti',
+    description = 'don\'t disable rtti (nonstandard for Rive)',
+})
+
+newoption({
+    trigger = 'with-pic',
+    description = 'enable position independent code',
+})
+
+newoption({
+    trigger = 'with-exceptions',
+    description = 'don\'t disable exceptions (nonstandard for Rive)',
+})
+
+newoption({
+    trigger = 'no-wasm-simd',
+    description = 'disable simd for wasm builds',
+})
+
+newoption({
+    trigger = 'wasm_single',
+    description = 'Embed wasm directly into the js, instead of side-loading it.',
+})
+
+newoption({
+    trigger = 'no-lto',
+    description = 'Don\'t build with link time optimizations.',
+})
+
+location(RIVE_BUILD_OUT)
+targetdir(RIVE_BUILD_OUT)
+objdir(RIVE_BUILD_OUT .. '/obj')
+toolset(_OPTIONS['toolset'] or 'clang')
+language('C++')
+cppdialect('C++17')
+configurations({ 'default' })
+filter({ 'options:not with-rtti' })
+rtti('Off')
+filter({ 'options:with-rtti' })
+rtti('On')
+filter({ 'options:not with-exceptions' })
+exceptionhandling('Off')
+filter({ 'options:with-exceptions' })
+exceptionhandling('On')
+
+filter({ 'options:with-pic' })
+do
+    pic('on')
+    buildoptions({ '-fPIC' })
+    linkoptions({ '-fPIC' })
+end
+
+filter('options:config=debug')
+do
+    defines({ 'DEBUG' })
+    symbols('On')
+end
+
+filter('options:config=release')
+do
+    defines({ 'RELEASE' })
+    defines({ 'NDEBUG' })
+    optimize('On')
+end
+
+filter({ 'options:config=release', 'options:not no-lto', 'system:not macosx', 'system:not ios' })
+do
+    flags({ 'LinkTimeOptimization' })
+end
+
+filter({ 'options:config=release', 'options:not no-lto', 'system:macosx or ios' })
+do
+    -- The 'LinkTimeOptimization' flag attempts to use llvm-ar, which doesn't always exist on macos.
+    buildoptions({ '-flto=full' })
+    linkoptions({ '-flto=full' })
+end
+
+newoption({
+    trigger = 'windows_runtime',
+    description = 'Choose whether to use staticruntime on/off/default',
+    allowed = {
+        { 'default', 'Use default runtime' },
+        { 'static', 'Use static runtime' },
+        { 'dynamic', 'Use dynamic runtime' },
+        { 'dynamic_debug', 'Use dynamic runtime force debug' },
+        { 'dynamic_release', 'Use dynamic runtime force release' },
+    },
+    default = 'default',
+})
+
+-- This is just to match our old windows config. Rive Native specifically sets
+-- static/dynamic and maybe we should do the same elsewhere.
+filter({ 'system:windows', 'options:windows_runtime=default' })
+do
+    staticruntime('on') -- Match Skia's /MT flag for link compatibility
+    runtime('Release')
+end
+
+filter({ 'system:windows', 'options:windows_runtime=static' })
+do
+    staticruntime('on') -- Match Skia's /MT flag for link compatibility
+end
+
+filter({ 'system:windows', 'options:windows_runtime=dynamic' })
+do
+    staticruntime('off')
+end
+
+filter({ 'system:windows', 'options:not windows_runtime=default', 'options:config=debug' })
+do
+    runtime('Debug')
+end
+
+filter({ 'system:windows', 'options:not windows_runtime=default', 'options:config=release' })
+do
+    runtime('Release')
+end
+
+filter({ 'system:windows', 'options:windows_runtime=dynamic_debug' })
+do
+    staticruntime('off')
+    runtime('Debug')
+end
+
+filter({ 'system:windows', 'options:windows_runtime=dynamic_release' })
+do
+    staticruntime('off')
+    runtime('Release')
+end
+
+filter('system:windows')
+do
+    architecture('x64')
+    defines({ '_USE_MATH_DEFINES', 'NOMINMAX' })
+end
+
+filter({ 'system:windows', 'options:toolset=clang' })
+do
+    buildoptions({
+        '-Wno-c++98-compat',
+        '-Wno-c++20-compat',
+        '-Wno-c++98-compat-pedantic',
+        '-Wno-c99-extensions',
+        '-Wno-ctad-maybe-unsupported',
+        '-Wno-deprecated-copy-with-user-provided-dtor',
+        '-Wno-deprecated-declarations',
+        '-Wno-documentation',
+        '-Wno-documentation-pedantic',
+        '-Wno-documentation-unknown-command',
+        '-Wno-double-promotion',
+        '-Wno-exit-time-destructors',
+        '-Wno-float-equal',
+        '-Wno-global-constructors',
+        '-Wno-implicit-float-conversion',
+        '-Wno-newline-eof',
+        '-Wno-old-style-cast',
+        '-Wno-reserved-identifier',
+        '-Wno-shadow',
+        '-Wno-sign-compare',
+        '-Wno-sign-conversion',
+        '-Wno-unused-macros',
+        '-Wno-unused-parameter',
+        '-Wno-four-char-constants',
+        '-Wno-unreachable-code',
+        '-Wno-switch-enum',
+        '-Wno-missing-field-initializers',
+        '-Wno-unsafe-buffer-usage',
+    })
+end
+
+filter({ 'system:windows', 'options:toolset=msc' })
+do
+    defines({ '_CRT_SECURE_NO_WARNINGS' })
+
+    -- We currently suppress several warnings for the MSVC build, some serious. Once this build
+    -- is fully integrated into GitHub actions, we will definitely want to address these.
+    disablewarnings({
+        '4061', -- enumerator 'identifier' in switch of enum 'enumeration' is not explicitly
+        -- handled by a case label
+        '4100', -- 'identifier': unreferenced formal parameter
+        '4201', -- nonstandard extension used: nameless struct/union
+        '4244', -- 'conversion_type': conversion from 'type1' to 'type2', possible loss of data
+        '4245', -- 'conversion_type': conversion from 'type1' to 'type2', signed/unsigned
+        --  mismatch
+        '4267', -- 'variable': conversion from 'size_t' to 'type', possible loss of data
+        '4355', -- 'this': used in base member initializer list
+        '4365', -- 'expression': conversion from 'type1' to 'type2', signed/unsigned mismatch
+        '4388', -- 'expression': signed/unsigned mismatch
+        '4389', -- 'operator': signed/unsigned mismatch
+        '4458', -- declaration of 'identifier' hides class member
+        '4514', -- 'function': unreferenced inline function has been removed
+        '4583', -- 'Catch::clara::detail::ResultValueBase<T>::m_value': destructor is not
+        -- implicitly called
+        '4623', -- 'Catch::AssertionInfo': default constructor was implicitly defined as deleted
+        '4625', -- 'derived class': copy constructor was implicitly defined as deleted because a
+        -- base class copy constructor is inaccessible or deleted
+        '4626', -- 'derived class': assignment operator was implicitly defined as deleted
+        --  because a base class assignment operator is inaccessible or deleted
+        '4820', -- 'bytes' bytes padding added after construct 'member_name'
+        '4868', -- (catch.hpp) compiler may not enforce left-to-right evaluation order in braced
+        -- initializer list
+        '5026', -- 'type': move constructor was implicitly defined as deleted
+        '5027', -- 'type': move assignment operator was implicitly defined as deleted
+        '5039', -- (catch.hpp) 'AddVectoredExceptionHandler': pointer or reference to
+        -- potentially throwing function passed to 'extern "C"' function under -EHc.
+        -- Undefined behavior may occur if this function throws an exception.
+        '5045', -- Compiler will insert Spectre mitigation for memory load if /Qspectre switch
+        -- specified
+        '5204', -- 'Catch::Matchers::Impl::MatcherMethod<T>': class has virtual functions, but
+        -- its trivial destructor is not virtual; instances of objects derived from this
+        -- class may not be destructed correctly
+        '5219', -- implicit conversion from 'type-1' to 'type-2', possible loss of data
+        '5262', -- MSVC\14.34.31933\include\atomic(917,9): implicit fall-through occurs here;
+        -- are you missing a break statement?
+        '5264', -- 'rive::math::PI': 'const' variable is not used
+        '4647', -- behavior change: __is_pod(rive::Vec2D) has different value in previous versions
+    })
+end
+
+filter({ 'action:vs2022' })
+do
+    flags({ 'MultiProcessorCompile' })
+end
+
+filter({})
+
+-- Don't use filter() here because we don't want to generate the "android_ndk" toolset if not
+-- building for android.
+if _OPTIONS['os'] == 'android' then
+    pic('on') -- Position-independent code is required for NDK libraries.
+
+    local ndk = os.getenv('NDK_PATH') or os.getenv('ANDROID_NDK')
+    if not ndk then
+        error('export $NDK_PATH or $ANDROID_NDK')
+    end
+
+    local ndk_toolchain = ndk .. '/toolchains/llvm/prebuilt'
+    if os.host() == 'windows' then
+        ndk_toolchain = ndk_toolchain .. '/windows-x86_64'
+    elseif os.host() == 'macosx' then
+        ndk_toolchain = ndk_toolchain .. '/darwin-x86_64'
+    else
+        ndk_toolchain = ndk_toolchain .. '/linux-x86_64'
+    end
+
+    -- clone the clang toolset into a custom one called "android_ndk".
+    premake.tools.android_ndk = {}
+    for k, v in pairs(premake.tools.clang) do
+        premake.tools.android_ndk[k] = v
+    end
+
+    -- update the android_ndk toolset to use the appropriate binaries.
+    local android_ndk_tools = {
+        cc = ndk_toolchain .. '/bin/clang',
+        cxx = ndk_toolchain .. '/bin/clang++',
+        ar = ndk_toolchain .. '/bin/llvm-ar',
+    }
+    function premake.tools.android_ndk.gettoolname(cfg, tool)
+        return android_ndk_tools[tool]
+    end
+
+    valid_cc_tools = premake.action._list['gmake2'].valid_tools['cc']
+    valid_cc_tools[#valid_cc_tools + 1] = 'android_ndk'
+    toolset('android_ndk')
+
+    buildoptions({
+        '--sysroot=' .. ndk_toolchain .. '/sysroot',
+        '-fdata-sections',
+        '-ffunction-sections',
+        '-funwind-tables',
+        '-fstack-protector-strong',
+        '-no-canonical-prefixes',
+    })
+
+    linkoptions({
+        '--sysroot=' .. ndk_toolchain .. '/sysroot',
+        '-fdata-sections',
+        '-ffunction-sections',
+        '-funwind-tables',
+        '-fstack-protector-strong',
+        '-no-canonical-prefixes',
+        '-Wl,--fatal-warnings',
+        '-Wl,--gc-sections',
+        '-Wl,--no-rosegment',
+        '-Wl,--no-undefined',
+        '-static-libstdc++',
+    })
+
+    filter('options:arch=x86')
+    do
+        architecture('x86')
+        buildoptions({ '--target=i686-none-linux-android21' })
+        linkoptions({ '--target=i686-none-linux-android21' })
+    end
+    filter('options:arch=x64')
+    do
+        architecture('x64')
+        buildoptions({ '--target=x86_64-none-linux-android21' })
+        linkoptions({ '--target=x86_64-none-linux-android21' })
+    end
+    filter('options:arch=arm')
+    do
+        architecture('arm')
+        buildoptions({ '--target=armv7a-none-linux-android21' })
+        linkoptions({ '--target=armv7a-none-linux-android21' })
+    end
+    filter('options:arch=arm64')
+    do
+        architecture('arm64')
+        buildoptions({ '--target=aarch64-none-linux-android21' })
+        linkoptions({ '--target=aarch64-none-linux-android21' })
+    end
+
+    filter({})
+end
+
+filter('system:linux', 'options:arch=x64')
+do
+    architecture('x64')
+end
+
+filter('system:linux', 'options:arch=arm')
+do
+    architecture('arm')
+end
+
+filter('system:linux', 'options:arch=arm64')
+do
+    architecture('arm64')
+end
+
+filter({})
+
+if os.host() == 'macosx' then
+    iphoneos_sysroot = os.outputof('xcrun --sdk iphoneos --show-sdk-path')
+    if iphoneos_sysroot == nil then
+        error(
+            'Unable to locate iphoneos sdk. Please ensure Xcode Command Line Tools are installed.'
+        )
+    end
+
+    iphonesimulator_sysroot = os.outputof('xcrun --sdk iphonesimulator --show-sdk-path')
+    if iphonesimulator_sysroot == nil then
+        error(
+            'Unable to locate iphonesimulator sdk. Please ensure Xcode Command Line Tools are installed.'
+        )
+    end
+
+    filter('system:ios')
+    do
+        buildoptions({ '-fembed-bitcode ' })
+    end
+
+    filter({ 'system:ios', 'options:variant=system' })
+    do
+        buildoptions({
+            '--target=arm64-apple-ios13.0.0',
+            '-mios-version-min=13.0.0',
+            '-isysroot ' .. iphoneos_sysroot,
+        })
+    end
+
+    filter({ 'system:ios', 'options:variant=emulator' })
+    do
+        buildoptions({
+            '--target=arm64-apple-ios13.0.0-simulator',
+            '-mios-version-min=13.0.0',
+            '-isysroot ' .. iphonesimulator_sysroot,
+        })
+    end
+
+    filter('system:macosx or system:ios')
+    do
+        buildoptions({ '-fobjc-arc' })
+    end
+
+    filter({ 'system:macosx' })
+    do
+        buildoptions({
+            '-mmacosx-version-min=11.0',
+        })
+    end
+
+    filter({ 'system:macosx', 'options:arch=host', 'action:xcode4' })
+    do
+        -- xcode tries to specify a target...
+        buildoptions({
+            '--target=' .. os.outputof('uname -m') .. '-apple-macos11.0',
+        })
+    end
+
+    filter({ 'system:macosx', 'options:arch=arm64 or arch=universal' })
+    do
+        buildoptions({ '-arch arm64' })
+        linkoptions({ '-arch arm64' })
+    end
+
+    filter({ 'system:macosx', 'options:arch=x64 or arch=universal' })
+    do
+        buildoptions({ '-arch x86_64' })
+        linkoptions({ '-arch x86_64' })
+    end
+
+    filter({
+        'system:ios',
+        'options:variant=system',
+        'options:arch=arm64 or arch=universal',
+    })
+    do
+        buildoptions({ '-arch arm64' })
+    end
+
+    filter({
+        'system:ios',
+        'options:variant=emulator',
+        'options:arch=x64 or arch=universal',
+    })
+    do
+        buildoptions({ '-arch x86_64' })
+    end
+
+    filter({
+        'system:ios',
+        'options:variant=emulator',
+        'options:arch=arm64 or arch=universal',
+    })
+    do
+        buildoptions({ '-arch arm64' })
+    end
+
+    filter({})
+end
+
+if _OPTIONS['arch'] == 'wasm' or _OPTIONS['arch'] == 'js' then
+    -- make a new system called "emscripten" so we don't try including host-os-specific files in the
+    -- web build.
+    premake.api.addAllowed('system', 'emscripten')
+
+    -- clone the clang toolset into a custom one called "emsdk".
+    premake.tools.emsdk = {}
+    for k, v in pairs(premake.tools.clang) do
+        premake.tools.emsdk[k] = v
+    end
+
+    -- update the emsdk toolset to use the appropriate binaries.
+    local emsdk_tools = {
+        cc = 'emcc',
+        cxx = 'em++',
+        ar = 'emar',
+    }
+    function premake.tools.emsdk.gettoolname(cfg, tool)
+        return emsdk_tools[tool]
+    end
+
+    system('emscripten')
+
+    valid_cc_tools = premake.action._list['gmake2'].valid_tools['cc']
+    valid_cc_tools[#valid_cc_tools + 1] = 'emsdk'
+    toolset('emsdk')
+
+    linkoptions({ '-sALLOW_MEMORY_GROWTH=1', '-sDYNAMIC_EXECUTION=0' })
+
+    filter('options:arch=wasm')
+    do
+        linkoptions({ '-sWASM=1' })
+    end
+
+    filter({ 'options:arch=wasm', 'options:not no-wasm-simd' })
+    do
+        buildoptions({ '-msimd128' })
+    end
+
+    filter({ 'options:arch=wasm', 'options:no-wasm-simd' })
+    do
+        linkoptions({ '-s MIN_SAFARI_VERSION=120000' })
+    end
+
+    filter({ 'options:arch=wasm', 'options:config=debug' })
+    do
+        buildoptions({
+            '-fsanitize=address',
+            '-g2',
+        })
+        linkoptions({
+            '-fsanitize=address',
+            '-g2',
+        })
+    end
+
+    filter('options:arch=js')
+    do
+        linkoptions({ '-sWASM=0' })
+    end
+
+    filter('options:wasm_single')
+    do
+        linkoptions({ '-sSINGLE_FILE=1' })
+    end
+
+    filter('options:config=release')
+    do
+        buildoptions({ '-Oz' })
+    end
+
+    filter({})
+end
+
+filter({})
diff --git a/build/setup_compiler.lua b/build/setup_compiler.lua
new file mode 100644
index 0000000..88f5463
--- /dev/null
+++ b/build/setup_compiler.lua
@@ -0,0 +1,255 @@
+-- https://github.com/TurkeyMan/premake-emscripten.git adds "emscripten" as a valid system, but
+-- premake5 still doesn't accept "--os=emscripten" from the command line. To work around this we add
+-- a custom "--wasm" flag that sets system to "emscripten" for us.
+newoption({
+    trigger = 'emsdk',
+    value = 'type',
+    description = 'Build with emscripten',
+    allowed = {
+        { 'none', 'don\'t use emscripten' },
+        { 'wasm', 'build WASM with emscripten' },
+        { 'js', 'build Javascript with emscripten' },
+    },
+    default = 'none',
+})
+if _OPTIONS['emsdk'] ~= 'none' then
+    -- Target emscripten via https://github.com/TurkeyMan/premake-emscripten.git
+    -- Premake doesn't properly load the _preload.lua for this module, so we load it here manually.
+    -- BUG: https://github.com/premake/premake-core/issues/1235
+    dofile('premake-emscripten/_preload.lua')
+    dofile('premake-emscripten/emscripten.lua')
+    system('emscripten')
+    toolset('emcc')
+end
+
+filter({ 'system:emscripten' })
+do
+    linkoptions({ '-sALLOW_MEMORY_GROWTH' })
+end
+
+filter({ 'system:emscripten', 'options:emsdk=wasm' })
+do
+    buildoptions({ '-msimd128' })
+    linkoptions({ '-sWASM=1' })
+end
+
+filter({ 'system:emscripten', 'options:emsdk=js' })
+do
+    linkoptions({ '-sWASM=0' })
+end
+
+filter('system:not emscripten')
+do
+    toolset(_OPTIONS['toolset'] or 'clang')
+end
+
+filter('system:windows')
+do
+    staticruntime('on') -- Match Skia's /MT flag for link compatibility
+    runtime('Release') -- Use /MT even in debug (/MTd is incompatible with Skia)
+end
+
+filter({ 'system:windows', 'options:toolset=clang' })
+do
+    buildoptions({
+        '-Wno-c++98-compat',
+        '-Wno-c++20-compat',
+        '-Wno-c++98-compat-pedantic',
+        '-Wno-c99-extensions',
+        '-Wno-ctad-maybe-unsupported',
+        '-Wno-deprecated-copy-with-user-provided-dtor',
+        '-Wno-deprecated-declarations',
+        '-Wno-documentation',
+        '-Wno-documentation-pedantic',
+        '-Wno-documentation-unknown-command',
+        '-Wno-double-promotion',
+        '-Wno-exit-time-destructors',
+        '-Wno-float-equal',
+        '-Wno-global-constructors',
+        '-Wno-implicit-float-conversion',
+        '-Wno-newline-eof',
+        '-Wno-old-style-cast',
+        '-Wno-reserved-identifier',
+        '-Wno-shadow',
+        '-Wno-sign-compare',
+        '-Wno-sign-conversion',
+        '-Wno-unused-macros',
+        '-Wno-unused-parameter',
+        '-Wno-four-char-constants',
+        '-Wno-unreachable-code',
+        '-Wno-switch-enum',
+        '-Wno-missing-field-initializers',
+        '-Wno-unsafe-buffer-usage',
+    })
+end
+
+filter({ 'system:windows', 'options:toolset=msc' })
+do
+    -- We currently suppress several warnings for the MSVC build, some serious. Once this build
+    -- is fully integrated into GitHub actions, we will definitely want to address these.
+    disablewarnings({
+        '4061', -- enumerator 'identifier' in switch of enum 'enumeration' is not explicitly
+        -- handled by a case label
+        '4100', -- 'identifier': unreferenced formal parameter
+        '4201', -- nonstandard extension used: nameless struct/union
+        '4244', -- 'conversion_type': conversion from 'type1' to 'type2', possible loss of data
+        '4245', -- 'conversion_type': conversion from 'type1' to 'type2', signed/unsigned
+        --  mismatch
+        '4267', -- 'variable': conversion from 'size_t' to 'type', possible loss of data
+        '4355', -- 'this': used in base member initializer list
+        '4365', -- 'expression': conversion from 'type1' to 'type2', signed/unsigned mismatch
+        '4388', -- 'expression': signed/unsigned mismatch
+        '4389', -- 'operator': signed/unsigned mismatch
+        '4458', -- declaration of 'identifier' hides class member
+        '4514', -- 'function': unreferenced inline function has been removed
+        '4583', -- 'Catch::clara::detail::ResultValueBase<T>::m_value': destructor is not
+        -- implicitly called
+        '4623', -- 'Catch::AssertionInfo': default constructor was implicitly defined as deleted
+        '4625', -- 'derived class': copy constructor was implicitly defined as deleted because a
+        -- base class copy constructor is inaccessible or deleted
+        '4626', -- 'derived class': assignment operator was implicitly defined as deleted
+        --  because a base class assignment operator is inaccessible or deleted
+        '4820', -- 'bytes' bytes padding added after construct 'member_name'
+        '4868', -- (catch.hpp) compiler may not enforce left-to-right evaluation order in braced
+        -- initializer list
+        '5026', -- 'type': move constructor was implicitly defined as deleted
+        '5027', -- 'type': move assignment operator was implicitly defined as deleted
+        '5039', -- (catch.hpp) 'AddVectoredExceptionHandler': pointer or reference to
+        -- potentially throwing function passed to 'extern "C"' function under -EHc.
+        -- Undefined behavior may occur if this function throws an exception.
+        '5045', -- Compiler will insert Spectre mitigation for memory load if /Qspectre switch
+        -- specified
+        '5204', -- 'Catch::Matchers::Impl::MatcherMethod<T>': class has virtual functions, but
+        -- its trivial destructor is not virtual; instances of objects derived from this
+        -- class may not be destructed correctly
+        '5219', -- implicit conversion from 'type-1' to 'type-2', possible loss of data
+        '5262', -- MSVC\14.34.31933\include\atomic(917,9): implicit fall-through occurs here;
+        -- are you missing a break statement?
+        '5264', -- 'rive::math::PI': 'const' variable is not used
+        '4647', -- behavior change: __is_pod(rive::Vec2D) has different value in previous versions
+    })
+end
+
+filter({})
+
+newoption({
+    trigger = 'with-rtti',
+    description = 'don\'t disable rtti (nonstandard for Rive)',
+})
+newoption({
+    trigger = 'with-exceptions',
+    description = 'don\'t disable exceptions (nonstandard for Rive)',
+})
+filter({ 'options:not with-rtti' })
+rtti('Off')
+filter({ 'options:with-rtti' })
+rtti('On')
+filter({ 'options:not with-exceptions' })
+exceptionhandling('Off')
+filter({ 'options:with-exceptions' })
+exceptionhandling('On')
+filter({})
+
+-- Don't use filter() here because we don't want to generate the "android_ndk" toolset if not
+-- building for android.
+if _OPTIONS['os'] == 'android' then
+    local ndk = os.getenv('NDK_PATH')
+    if not ndk or ndk == '' then
+        error('export $NDK_PATH')
+    end
+
+    local ndk_toolchain = ndk .. '/toolchains/llvm/prebuilt'
+    if os.host() == 'windows' then
+        ndk_toolchain = ndk_toolchain .. '/windows-x86_64'
+    elseif os.host() == 'macosx' then
+        ndk_toolchain = ndk_toolchain .. '/darwin-x86_64'
+    else
+        ndk_toolchain = ndk_toolchain .. '/linux-x86_64'
+    end
+
+    -- clone the clang toolset into a custom one called "android_ndk".
+    premake.tools.android_ndk = {}
+    for k, v in pairs(premake.tools.clang) do
+        premake.tools.android_ndk[k] = v
+    end
+
+    -- update the android_ndk toolset to use the appropriate binaries.
+    local android_ndk_tools = {
+        cc = ndk_toolchain .. '/bin/clang',
+        cxx = ndk_toolchain .. '/bin/clang++',
+        ar = ndk_toolchain .. '/bin/llvm-ar',
+    }
+    function premake.tools.android_ndk.gettoolname(cfg, tool)
+        return android_ndk_tools[tool]
+    end
+
+    toolset('android_ndk')
+
+    buildoptions({
+        '--sysroot=' .. ndk_toolchain .. '/sysroot',
+        '-fdata-sections',
+        '-ffunction-sections',
+        '-funwind-tables',
+        '-fstack-protector-strong',
+        '-no-canonical-prefixes',
+    })
+
+    linkoptions({
+        '--sysroot=' .. ndk_toolchain .. '/sysroot',
+        '-fdata-sections',
+        '-ffunction-sections',
+        '-funwind-tables',
+        '-fstack-protector-strong',
+        '-no-canonical-prefixes',
+        '-Wl,--fatal-warnings',
+        '-Wl,--gc-sections',
+        '-Wl,--no-rosegment',
+        '-Wl,--no-undefined',
+        '-static-libstdc++',
+    })
+
+    pic('on') -- Position-independent code is required for NDK libraries.
+    filter('options:arch=x86')
+    do
+        architecture('x86')
+        buildoptions({ '--target=i686-none-linux-android21' })
+        linkoptions({ '--target=i686-none-linux-android21' })
+    end
+    filter('options:arch=x64')
+    do
+        architecture('x64')
+        buildoptions({ '--target=x86_64-none-linux-android21' })
+        linkoptions({ '--target=x86_64-none-linux-android21' })
+    end
+    filter('options:arch=arm')
+    do
+        architecture('arm')
+        buildoptions({ '--target=armv7a-none-linux-android21' })
+        linkoptions({ '--target=armv7a-none-linux-android21' })
+    end
+    filter('options:arch=arm64')
+    do
+        architecture('arm64')
+        buildoptions({ '--target=aarch64-none-linux-android21' })
+        linkoptions({ '--target=aarch64-none-linux-android21' })
+    end
+    filter({})
+end
+
+newoption({
+    trigger = 'toolset',
+    value = 'type',
+    description = 'Choose which toolchain to build with',
+    allowed = {
+        { 'clang', 'Build with Clang' },
+        { 'msc', 'Build with the Microsoft C/C++ compiler' },
+    },
+    default = 'clang',
+})
+
+newoption({
+    trigger = 'arch',
+    value = 'ABI',
+    description = 'The ABI with the right toolchain for this build, generally with Android',
+    allowed = { { 'x86' }, { 'x64' }, { 'arm' }, { 'arm64' } },
+})
diff --git a/cg_renderer/include/cg_factory.hpp b/cg_renderer/include/cg_factory.hpp
new file mode 100644
index 0000000..47af775
--- /dev/null
+++ b/cg_renderer/include/cg_factory.hpp
@@ -0,0 +1,44 @@
+/*
+ * 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> makeRenderBuffer(RenderBufferType, RenderBufferFlags, size_t) override;
+
+    rcp<RenderShader> makeLinearGradient(float sx,
+                                         float sy,
+                                         float ex,
+                                         float ey,
+                                         const ColorInt colors[], // [count]
+                                         const float stops[],     // [count]
+                                         size_t count) override;
+
+    rcp<RenderShader> makeRadialGradient(float cx,
+                                         float cy,
+                                         float radius,
+                                         const ColorInt colors[], // [count]
+                                         const float stops[],     // [count]
+                                         size_t count) override;
+
+    rcp<RenderPath> makeRenderPath(RawPath&, FillRule) override;
+
+    rcp<RenderPath> makeEmptyRenderPath() override;
+
+    rcp<RenderPaint> makeRenderPaint() override;
+
+    rcp<RenderImage> decodeImage(Span<const uint8_t>) override;
+};
+
+} // namespace rive
+#endif
diff --git a/cg_renderer/include/cg_renderer.hpp b/cg_renderer/include/cg_renderer.hpp
new file mode 100644
index 0000000..5b071fc
--- /dev/null
+++ b/cg_renderer/include/cg_renderer.hpp
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_CG_RENDERER_HPP_
+#define _RIVE_CG_RENDERER_HPP_
+
+#include "rive/renderer.hpp"
+#include "utils/auto_cf.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,
+                       uint32_t vertexCount,
+                       uint32_t indexCount,
+                       BlendMode,
+                       float opacity) override;
+
+    static AutoCF<CGImageRef> DecodeToCGImage(Span<const uint8_t>);
+    static AutoCF<CGImageRef> FlipCGImageInY(AutoCF<CGImageRef>);
+};
+} // namespace rive
+#endif
diff --git a/cg_renderer/premake5.lua b/cg_renderer/premake5.lua
new file mode 100644
index 0000000..a4a6062
--- /dev/null
+++ b/cg_renderer/premake5.lua
@@ -0,0 +1,46 @@
+dofile('rive_build_config.lua')
+
+dependencies = os.getenv('DEPENDENCIES')
+
+project('rive_cg_renderer')
+do
+    kind('StaticLib')
+    includedirs({ 'include', '../include' })
+
+    libdirs({ '../../build/%{cfg.system}/bin/' .. RIVE_BUILD_CONFIG })
+
+    files({ 'src/**.cpp' })
+
+    flags({ 'FatalCompileWarnings' })
+
+    filter('system:windows')
+    do
+        architecture('x64')
+        defines({ '_USE_MATH_DEFINES' })
+    end
+
+    filter({ 'system:macosx', 'options:variant=runtime' })
+    do
+        buildoptions({ '-fembed-bitcode -arch arm64 -arch x86_64' })
+    end
+
+    if os.host() == 'macosx' then
+        iphoneos_sysroot = os.outputof('xcrun --sdk iphoneos --show-sdk-path')
+        iphonesimulator_sysroot = os.outputof('xcrun --sdk iphonesimulator --show-sdk-path')
+
+        filter({ 'system:ios', 'options:variant=system' })
+        do
+            buildoptions({
+                '-mios-version-min=13.0 -fembed-bitcode -arch arm64 -isysroot ' .. iphoneos_sysroot,
+            })
+        end
+
+        filter({ 'system:ios', 'options:variant=emulator' })
+        do
+            buildoptions({
+                '--target=arm64-apple-ios13.0.0-simulator -mios-version-min=13.0 -arch x86_64 -arch arm64 -isysroot '
+                    .. iphonesimulator_sysroot,
+            })
+        end
+    end
+end
diff --git a/cg_renderer/src/cg_factory.cpp b/cg_renderer/src/cg_factory.cpp
new file mode 100644
index 0000000..8229db7
--- /dev/null
+++ b/cg_renderer/src/cg_factory.cpp
@@ -0,0 +1,563 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "rive/rive_types.hpp"
+
+#ifdef RIVE_BUILD_FOR_APPLE
+
+#include "cg_factory.hpp"
+#include "cg_renderer.hpp"
+
+#include "utils/factory_utils.hpp"
+#include "rive/math/vec2d.hpp"
+#include "rive/core/type_conversions.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 lite_rtti_override<RenderPath, CGRenderPath>
+{
+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 rewind() 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 lite_rtti_override<RenderShader, CGRenderShader>
+{
+public:
+    CGRenderShader() {}
+
+    static constexpr int clampOptions =
+        kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation;
+
+    virtual void draw(CGContextRef) {}
+};
+
+class CGRenderPaint : public lite_rtti_override<RenderPaint, CGRenderPaint>
+{
+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<CGRenderShader> m_shader;
+
+public:
+    CGRenderPaint() {}
+
+    bool isStroke() const { return m_isStroke; }
+    float opacity() const { return m_rgba[3]; }
+
+    CGRenderShader* shader() const { return 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 = lite_rtti_rcp_cast<CGRenderShader>(std::move(sh));
+    }
+    void invalidateStroke() override {}
+};
+
+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]);
+
+        // Rive wants the colors to be premultiplied *after* interpolation
+        // Unfortunately, CG doesn't know about this option, it just does
+        // a straight interpolation and uses the result (thinking it is
+        // in premul form already). This can lead to artifacts in the drawing
+        // (e.g. sparkles) so as a hack, we premul our color stops up front.
+        // Not exactly correct, but does remove the sparkles.
+        // A better fix might be to write a custom Shading proc... but that
+        // is likely to be be slower (but need to try/time it to know for sure).
+        CGFloat* p = &c[i * 4];
+        p[0] *= p[3];
+        p[1] *= p[3];
+        p[2] *= p[3];
+    }
+    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
+    {
+        CGContextDrawRadialGradient(ctx, m_grad, m_center, 0, m_center, m_radius, clampOptions);
+    }
+};
+
+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
+    {
+        CGContextDrawLinearGradient(ctx, m_grad, m_start, m_end, clampOptions);
+    }
+};
+
+class CGRenderImage : public lite_rtti_override<RenderImage, CGRenderImage>
+{
+public:
+    AutoCF<CGImageRef> m_image;
+
+    CGRenderImage(const Span<const uint8_t> span) : m_image(CGRenderer::DecodeToCGImage(span))
+    {
+        if (m_image)
+        {
+            m_Width = rive::castTo<uint32_t>(CGImageGetWidth(m_image.get()));
+            m_Height = rive::castTo<uint32_t>(CGImageGetHeight(m_image.get()));
+        }
+    }
+
+    Mat2D localM2D() const { return Mat2D(1, 0, 0, -1, 0, (float)m_Height); }
+
+    void applyLocalMatrix(CGContextRef ctx) const
+    {
+        CGContextConcatCTM(ctx, CGAffineTransformMake(1, 0, 0, -1, 0, (float)m_Height));
+    }
+};
+
+//////////////////////////////////////////////////////////////////////////
+
+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));
+
+    CGContextSetInterpolationQuality(ctx, kCGInterpolationMedium);
+}
+
+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)
+{
+    LITE_RTTI_CAST_OR_RETURN(cgpaint, CGRenderPaint*, paint);
+    LITE_RTTI_CAST_OR_RETURN(cgpath, CGRenderPath*, path);
+
+    cgpaint->apply(m_ctx);
+
+    CGContextBeginPath(m_ctx);
+    CGContextAddPath(m_ctx, cgpath->path());
+    if (auto sh = cgpaint->shader())
+    {
+        if (cgpaint->isStroke())
+        {
+            // so we can clip against the "stroke" of the path
+            CGContextReplacePathWithStrokedPath(m_ctx);
+        }
+        CGContextSaveGState(m_ctx);
+        CGContextClip(m_ctx);
+
+        // so the gradient modulates with the color's alpha
+        CGContextSetAlpha(m_ctx, cgpaint->opacity());
+
+        sh->draw(m_ctx);
+        CGContextRestoreGState(m_ctx);
+    }
+    else
+    {
+        CGContextDrawPath(m_ctx, cgpath->drawingMode(cgpaint->isStroke()));
+    }
+
+    assert(CGContextIsPathEmpty(m_ctx));
+}
+
+void CGRenderer::clipPath(RenderPath* path)
+{
+    LITE_RTTI_CAST_OR_RETURN(cgpath, CGRenderPath*, path);
+
+    CGContextBeginPath(m_ctx);
+    CGContextAddPath(m_ctx, cgpath->path());
+    CGContextClip(m_ctx);
+}
+
+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));
+    cgimg->applyLocalMatrix(m_ctx);
+    CGContextDrawImage(m_ctx, bounds, cgimg->m_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,
+                               uint32_t vertexCount,
+                               uint32_t indexCount,
+                               BlendMode blendMode,
+                               float opacity)
+{
+    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();
+    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 = indexCount / 3;
+    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
+    // 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() * localMatrix;
+        CGContextConcatCTM(m_ctx, convert(mx));
+        CGContextDrawImage(m_ctx, bounds, cgimage->m_image);
+
+        CGContextRestoreGState(m_ctx);
+    }
+
+    CGContextRestoreGState(m_ctx); // restore opacity, antialias, etc.
+}
+
+// Factory
+
+rcp<RenderBuffer> CGFactory::makeRenderBuffer(RenderBufferType type,
+                                              RenderBufferFlags flags,
+                                              size_t sizeInBytes)
+{
+    return make_rcp<DataRenderBuffer>(type, flags, sizeInBytes);
+}
+
+rcp<RenderShader> CGFactory::makeLinearGradient(float sx,
+                                                float sy,
+                                                float ex,
+                                                float ey,
+                                                const ColorInt colors[], // [count]
+                                                const float stops[],     // [count]
+                                                size_t count)
+{
+    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)
+{
+    return rcp<RenderShader>(
+        new CGRadialGradientRenderShader(cx, cy, radius, colors, stops, count));
+}
+
+rcp<RenderPath> CGFactory::makeRenderPath(RawPath& rawPath, FillRule fillRule)
+{
+    return make_rcp<CGRenderPath>(rawPath.points(), rawPath.verbs(), fillRule);
+}
+
+rcp<RenderPath> CGFactory::makeEmptyRenderPath() { return make_rcp<CGRenderPath>(); }
+
+rcp<RenderPaint> CGFactory::makeRenderPaint() { return make_rcp<CGRenderPaint>(); }
+
+rcp<RenderImage> CGFactory::decodeImage(Span<const uint8_t> encoded)
+{
+    return make_rcp<CGRenderImage>(encoded);
+}
+
+AutoCF<CGImageRef> CGRenderer::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> CGRenderer::DecodeToCGImage(rive::Span<const uint8_t> span)
+{
+    AutoCF data = CFDataCreate(nullptr, span.data(), span.size());
+    if (!data)
+    {
+        printf("CFDataCreate failed\n");
+        return nullptr;
+    }
+
+    AutoCF source = CGImageSourceCreateWithData(data, nullptr);
+    if (!source)
+    {
+        printf("CGImageSourceCreateWithData failed\n");
+        return nullptr;
+    }
+
+    AutoCF image = CGImageSourceCreateImageAtIndex(source, 0, nullptr);
+    if (!image)
+    {
+        printf("CGImageSourceCreateImageAtIndex failed\n");
+    }
+    return image;
+}
+#endif // APPLE
diff --git a/decoders/build/premake5.lua b/decoders/build/premake5.lua
new file mode 100644
index 0000000..ee183fa
--- /dev/null
+++ b/decoders/build/premake5.lua
@@ -0,0 +1,39 @@
+workspace('rive')
+configurations({ 'debug', 'release' })
+
+require('setup_compiler')
+
+rive = path.getabsolute('../../')
+
+dofile(rive .. '/dependencies/premake5_libpng.lua')
+
+project('rive_decoders')
+dependson('libpng')
+kind('StaticLib')
+language('C++')
+cppdialect('C++17')
+targetdir('%{cfg.buildcfg}')
+objdir('obj/%{cfg.buildcfg}')
+flags({ 'FatalWarnings' })
+
+includedirs({ '../include', '../../include', libpng })
+
+files({ '../src/**.cpp' })
+
+filter({ 'system:windows' })
+do
+    architecture('x64')
+end
+
+filter('configurations:debug')
+do
+    defines({ 'DEBUG' })
+    symbols('On')
+end
+
+filter('configurations:release')
+do
+    defines({ 'RELEASE' })
+    defines({ 'NDEBUG' })
+    optimize('On')
+end
diff --git a/decoders/include/rive/decoders/bitmap_decoder.hpp b/decoders/include/rive/decoders/bitmap_decoder.hpp
new file mode 100644
index 0000000..881028c
--- /dev/null
+++ b/decoders/include/rive/decoders/bitmap_decoder.hpp
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2023 Rive
+ */
+
+#ifndef _RIVE_BITMAP_DECODER_HPP_
+#define _RIVE_BITMAP_DECODER_HPP_
+
+#include <memory>
+
+/// Bitmap will always take ownership of the bytes it is constructed with.
+class Bitmap
+{
+public:
+    enum class PixelFormat : uint8_t
+    {
+        RGB,
+        RGBA
+    };
+
+    Bitmap(uint32_t width,
+           uint32_t height,
+           PixelFormat pixelFormat,
+           std::unique_ptr<const uint8_t[]> bytes);
+
+    Bitmap(uint32_t width, uint32_t height, PixelFormat pixelFormat, const uint8_t* bytes);
+
+private:
+    uint32_t m_Width;
+    uint32_t m_Height;
+    PixelFormat m_PixelFormat;
+    std::unique_ptr<const uint8_t[]> m_Bytes;
+
+public:
+    uint32_t width() const { return m_Width; }
+    uint32_t height() const { return m_Height; }
+    PixelFormat pixelFormat() const { return m_PixelFormat; }
+    const uint8_t* bytes() const { return m_Bytes.get(); }
+    std::unique_ptr<const uint8_t[]> detachBytes() { return std::move(m_Bytes); }
+    size_t byteSize() const;
+    size_t byteSize(PixelFormat format) const;
+    size_t bytesPerPixel(PixelFormat format) const;
+
+    static std::unique_ptr<Bitmap> decode(const uint8_t bytes[], size_t byteCount);
+
+    // Change the pixel format (note this will resize bytes).
+    void pixelFormat(PixelFormat format);
+};
+
+#endif
diff --git a/decoders/premake5_v2.lua b/decoders/premake5_v2.lua
new file mode 100644
index 0000000..6c90d95
--- /dev/null
+++ b/decoders/premake5_v2.lua
@@ -0,0 +1,45 @@
+dofile('rive_build_config.lua')
+
+if _OPTIONS['no-rive-decoders'] then
+    return
+end
+
+rive = path.getabsolute('../')
+
+dofile(rive .. '/dependencies/premake5_libpng_v2.lua')
+dofile(rive .. '/dependencies/premake5_libjpeg_v2.lua')
+dofile(rive .. '/dependencies/premake5_libwebp_v2.lua')
+
+project('rive_decoders')
+do
+    dependson('libpng', 'zlib', 'libjpeg', 'libwebp')
+    kind('StaticLib')
+    flags({ 'FatalWarnings' })
+
+    includedirs({ 'include', '../include', libpng, libjpeg, libwebp .. '/src' })
+
+    files({ 'src/bitmap_decoder.cpp' })
+
+    filter({ 'options:not no-libjpeg-renames' })
+    do
+        includedirs({
+            rive .. '/dependencies',
+        })
+        forceincludes({ 'rive_libjpeg_renames.h' })
+    end
+
+    filter({ 'system:macosx or system:ios' })
+    do
+        files({ 'src/**.mm' })
+    end
+
+    filter({ 'system:not macosx', 'system:not ios' })
+    do
+        files({
+            'src/bitmap_decoder_thirdparty.cpp',
+            'src/decode_webp.cpp',
+            'src/decode_jpeg.cpp',
+            'src/decode_png.cpp',
+        })
+    end
+end
diff --git a/decoders/src/bitmap_decoder.cpp b/decoders/src/bitmap_decoder.cpp
new file mode 100644
index 0000000..5bb4ee0
--- /dev/null
+++ b/decoders/src/bitmap_decoder.cpp
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023 Rive
+ */
+
+#include "rive/decoders/bitmap_decoder.hpp"
+#include "rive/rive_types.hpp"
+#include <stdio.h>
+#include <string.h>
+#include <vector>
+
+Bitmap::Bitmap(uint32_t width,
+               uint32_t height,
+               PixelFormat pixelFormat,
+               std::unique_ptr<const uint8_t[]> bytes) :
+    m_Width(width), m_Height(height), m_PixelFormat(pixelFormat), m_Bytes(std::move(bytes))
+{}
+
+Bitmap::Bitmap(uint32_t width, uint32_t height, PixelFormat pixelFormat, const uint8_t* bytes) :
+    Bitmap(width, height, pixelFormat, std::unique_ptr<const uint8_t[]>(bytes))
+{}
+
+size_t Bitmap::bytesPerPixel(PixelFormat format) const
+{
+    switch (format)
+    {
+        case PixelFormat::RGB:
+            return 3;
+        case PixelFormat::RGBA:
+            return 4;
+    }
+    RIVE_UNREACHABLE();
+}
+
+size_t Bitmap::byteSize(PixelFormat format) const
+{
+    return m_Width * m_Height * bytesPerPixel(format);
+}
+
+size_t Bitmap::byteSize() const { return byteSize(m_PixelFormat); }
+
+void Bitmap::pixelFormat(PixelFormat format)
+{
+    if (format == m_PixelFormat)
+    {
+        return;
+    }
+    auto nextByteSize = byteSize(format);
+    auto nextBytes = std::unique_ptr<uint8_t[]>(new uint8_t[nextByteSize]);
+
+    size_t fromBytesPerPixel = bytesPerPixel(m_PixelFormat);
+    size_t toBytesPerPixel = bytesPerPixel(format);
+    int writeIndex = 0;
+    int readIndex = 0;
+    for (uint32_t i = 0; i < m_Width * m_Height; i++)
+    {
+        for (size_t j = 0; j < toBytesPerPixel; j++)
+        {
+            nextBytes[writeIndex++] = j < fromBytesPerPixel ? m_Bytes[readIndex++] : 255;
+        }
+    }
+
+    m_Bytes = std::move(nextBytes);
+    m_PixelFormat = format;
+}
diff --git a/decoders/src/bitmap_decoder_cg.mm b/decoders/src/bitmap_decoder_cg.mm
new file mode 100644
index 0000000..82b15d6
--- /dev/null
+++ b/decoders/src/bitmap_decoder_cg.mm
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2023 Rive
+ */
+
+#include "rive/decoders/bitmap_decoder.hpp"
+#include "rive/rive_types.hpp"
+#include "rive/math/simd.hpp"
+#include "rive/math/math_types.hpp"
+#include "rive/core/type_conversions.hpp"
+#include "utils/auto_cf.hpp"
+
+#include <TargetConditionals.h>
+
+#if TARGET_OS_IPHONE
+#include <CoreGraphics/CoreGraphics.h>
+#include <ImageIO/ImageIO.h>
+#elif TARGET_OS_MAC
+#include <ApplicationServices/ApplicationServices.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <vector>
+
+// Represents raw, premultiplied, RGBA image data with tightly packed rows (width * 4 bytes).
+struct PlatformCGImage
+{
+    uint32_t width = 0;
+    uint32_t height = 0;
+    bool opaque = false;
+    std::unique_ptr<uint8_t[]> pixels;
+};
+
+bool cg_image_decode(const uint8_t* encodedBytes,
+                     size_t encodedSizeInBytes,
+                     PlatformCGImage* platformImage)
+{
+    AutoCF data = CFDataCreate(kCFAllocatorDefault, encodedBytes, encodedSizeInBytes);
+    if (!data)
+    {
+        return false;
+    }
+
+    AutoCF source = CGImageSourceCreateWithData(data, nullptr);
+    if (!source)
+    {
+        return false;
+    }
+
+    AutoCF image = CGImageSourceCreateImageAtIndex(source, 0, nullptr);
+    if (!image)
+    {
+        return false;
+    }
+
+    bool isOpaque = false;
+    switch (CGImageGetAlphaInfo(image.get()))
+    {
+        case kCGImageAlphaNone:
+        case kCGImageAlphaNoneSkipFirst:
+        case kCGImageAlphaNoneSkipLast:
+            isOpaque = true;
+            break;
+        default:
+            break;
+    }
+
+    const size_t width = CGImageGetWidth(image);
+    const size_t height = CGImageGetHeight(image);
+    const size_t rowBytes = width * 4; // 4 bytes per pixel
+    const size_t size = rowBytes * height;
+
+    const size_t bitsPerComponent = 8;
+    CGBitmapInfo cgInfo = kCGBitmapByteOrder32Big; // rgba
+    if (isOpaque)
+    {
+        cgInfo |= kCGImageAlphaNoneSkipLast;
+    }
+    else
+    {
+        cgInfo |= kCGImageAlphaPremultipliedLast;
+    }
+
+    std::unique_ptr<uint8_t[]> pixels(new uint8_t[size]);
+
+    AutoCF cs = CGColorSpaceCreateDeviceRGB();
+    AutoCF cg =
+        CGBitmapContextCreate(pixels.get(), width, height, bitsPerComponent, rowBytes, cs, cgInfo);
+    if (!cg)
+    {
+        return false;
+    }
+
+    CGContextSetBlendMode(cg, kCGBlendModeCopy);
+    CGContextDrawImage(cg, CGRectMake(0, 0, width, height), image);
+
+    platformImage->width = rive::castTo<uint32_t>(width);
+    platformImage->height = rive::castTo<uint32_t>(height);
+    platformImage->opaque = isOpaque;
+    platformImage->pixels = std::move(pixels);
+
+    return true;
+}
+
+std::unique_ptr<Bitmap> Bitmap::decode(const uint8_t bytes[], size_t byteCount)
+{
+    PlatformCGImage image;
+    if (!cg_image_decode(bytes, byteCount, &image))
+    {
+        return nullptr;
+    }
+
+    // CG only supports premultiplied alpha. Unmultiply now.
+    size_t imageNumPixels = image.height * image.width;
+    size_t imageSizeInBytes = imageNumPixels * 4;
+    // Process 2 pixels at once, deal with odd number of pixels
+    if (imageNumPixels & 1)
+    {
+        imageSizeInBytes -= 4;
+    }
+    size_t i;
+    for (i = 0; i < imageSizeInBytes; i += 8)
+    {
+        // Load 2 pixels into 64 bits
+        auto twoPixels = rive::simd::load<uint8_t, 8>(&image.pixels[i]);
+        auto a0 = twoPixels[3];
+        auto a1 = twoPixels[7];
+        // Avoid computation if both pixels are either fully transparent or opaque pixels
+        if ((a0 > 0 && a0 < 255) || (a1 > 0 && a1 < 255))
+        {
+            // Avoid potential division by zero
+            a0 = std::max<uint8_t>(a0, 1);
+            a1 = std::max<uint8_t>(a1, 1);
+            // Cast to 16 bits to avoid overflow
+            rive::uint16x8 rgbaWidex2 = rive::simd::cast<uint16_t>(twoPixels);
+            // Unpremult: multiply by RGB by "255.0 / alpha"
+            rgbaWidex2 *= rive::uint16x8{255, 255, 255, 1, 255, 255, 255, 1};
+            rgbaWidex2 /= rive::uint16x8{a0, a0, a0, 1, a1, a1, a1, 1};
+            // Cast back to 8 bits and store
+            twoPixels = rive::simd::cast<uint8_t>(rgbaWidex2);
+            rive::simd::store(&image.pixels[i], twoPixels);
+        }
+    }
+    // Process last odd pixel if needed
+    if (imageNumPixels & 1)
+    {
+        // Load 1 pixel into 32 bits
+        auto rgba = rive::simd::load<uint8_t, 4>(&image.pixels[i]);
+        // Avoid computation for fully transparent or opaque pixels
+        if (rgba.a > 0 && rgba.a < 255)
+        {
+            // Cast to 16 bits to avoid overflow
+            rive::uint16x4 rgbaWide = rive::simd::cast<uint16_t>(rgba);
+            // Unpremult: multiply by RGB by "255.0 / alpha"
+            rgbaWide *= rive::uint16x4{255, 255, 255, 1};
+            rgbaWide /= rive::uint16x4{rgba.a, rgba.a, rgba.a, 1};
+            // Cast back to 8 bits and store
+            rgba = rive::simd::cast<uint8_t>(rgbaWide);
+            rive::simd::store(&image.pixels[i], rgba);
+        }
+    }
+
+    return std::make_unique<Bitmap>(
+        image.width, image.height, PixelFormat::RGBA, std::move(image.pixels));
+}
diff --git a/decoders/src/bitmap_decoder_thirdparty.cpp b/decoders/src/bitmap_decoder_thirdparty.cpp
new file mode 100644
index 0000000..37c5a66
--- /dev/null
+++ b/decoders/src/bitmap_decoder_thirdparty.cpp
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2023 Rive
+ */
+
+#include "rive/decoders/bitmap_decoder.hpp"
+#include "rive/rive_types.hpp"
+#include <stdio.h>
+#include <string.h>
+#include <vector>
+
+std::unique_ptr<Bitmap> DecodePng(const uint8_t bytes[], size_t byteCount);
+std::unique_ptr<Bitmap> DecodeJpeg(const uint8_t bytes[], size_t byteCount);
+std::unique_ptr<Bitmap> DecodeWebP(const uint8_t bytes[], size_t byteCount);
+
+using BitmapDecoder = std::unique_ptr<Bitmap> (*)(const uint8_t bytes[], size_t byteCount);
+struct ImageFormat
+{
+    const char* name;
+    std::vector<uint8_t> fingerprint;
+    BitmapDecoder decodeImage;
+};
+
+std::unique_ptr<Bitmap> Bitmap::decode(const uint8_t bytes[], size_t byteCount)
+{
+    static ImageFormat decoders[] = {
+        {
+            "png",
+            {0x89, 0x50, 0x4E, 0x47},
+            DecodePng,
+        },
+        {
+            "jpeg",
+            {0xFF, 0xD8, 0xFF},
+            DecodeJpeg,
+        },
+        {
+            "webp",
+            {0x52, 0x49, 0x46},
+            DecodeWebP,
+        },
+    };
+
+    for (auto recognizer : decoders)
+    {
+        auto& fingerprint = recognizer.fingerprint;
+
+        // Immediately discard decoders with fingerprints that are longer than
+        // the file buffer.
+        if (recognizer.fingerprint.size() > byteCount)
+        {
+            continue;
+        }
+
+        // If the fingerprint doesn't match, discrd this decoder. These are all bytes so .size() is
+        // fine here.
+        if (memcmp(fingerprint.data(), bytes, fingerprint.size()) != 0)
+        {
+            continue;
+        }
+
+        auto bitmap = recognizer.decodeImage(bytes, byteCount);
+        if (!bitmap)
+        {
+            fprintf(stderr, "Bitmap::decode - failed to decode a %s.\n", recognizer.name);
+        }
+        return bitmap;
+    }
+    return nullptr;
+}
diff --git a/decoders/src/decode_jpeg.cpp b/decoders/src/decode_jpeg.cpp
new file mode 100644
index 0000000..2ed8c9a
--- /dev/null
+++ b/decoders/src/decode_jpeg.cpp
@@ -0,0 +1,127 @@
+// Adapted from libjpeg-turbo's example:
+// https://github.com/libjpeg-turbo/libjpeg-turbo/blob/main/example.c
+#include "rive/decoders/bitmap_decoder.hpp"
+
+#include "jpeglib.h"
+#include "jerror.h"
+
+#include <setjmp.h>
+#include <algorithm>
+#include <cassert>
+#include <string.h>
+
+struct my_error_mgr
+{
+    struct jpeg_error_mgr pub;
+    jmp_buf setjmp_buffer;
+};
+
+typedef struct my_error_mgr* my_error_ptr;
+
+void my_error_exit(j_common_ptr cinfo)
+{
+    // cinfo.err really points to a my_error_mgr struct, so coerce pointer
+    my_error_ptr myerr = (my_error_ptr)cinfo->err;
+
+    // Always display the message.
+    // We could postpone this until after returning, if we chose.
+    (*cinfo->err->output_message)(cinfo);
+
+    // Return control to the setjmp point
+    longjmp(myerr->setjmp_buffer, 1);
+}
+
+std::unique_ptr<Bitmap> DecodeJpeg(const uint8_t bytes[], size_t byteCount)
+{
+    struct jpeg_decompress_struct cinfo;
+    struct my_error_mgr jerr;
+
+    JSAMPARRAY buffer = nullptr;
+    std::unique_ptr<const uint8_t[]> pixelBuffer;
+    int row_stride;
+
+    // Step 1: allocate and initialize JPEG decompression object.
+
+    // We set up the normal JPEG error routines, then override error_exit.
+    cinfo.err = jpeg_std_error(&jerr.pub);
+    jerr.pub.error_exit = my_error_exit;
+    // Establish the setjmp return context for my_error_exit to use.
+    if (setjmp(jerr.setjmp_buffer))
+    {
+        // If we get here, the JPEG code has signaled an error.
+        // We need to clean up the JPEG object, close the input file, and return.
+        jpeg_destroy_decompress(&cinfo);
+        return nullptr;
+    }
+
+    // Now we can initialize the JPEG decompression object.
+    jpeg_create_decompress(&cinfo);
+
+    // Step 2: specify data source
+    jpeg_mem_src(&cinfo, bytes, byteCount);
+
+    // Step 3: read file parameters with jpeg_read_header()
+
+    jpeg_read_header(&cinfo, TRUE);
+
+    // Step 4: set parameters for decompression
+
+    // always want 8 bit RGB
+    cinfo.data_precision = 8;
+    cinfo.out_color_space = JCS_RGB;
+
+    // Step 5: Start decompressor
+    jpeg_start_decompress(&cinfo);
+
+    /// Api worked as expected and gave us correct format even for jpeg 12 or 16
+    assert(cinfo.data_precision == 8);
+    assert(cinfo.output_components == 3);
+
+    size_t pixelBufferSize = static_cast<size_t>(cinfo.output_width) *
+                             static_cast<size_t>(cinfo.output_height) *
+                             static_cast<size_t>(cinfo.output_components);
+    pixelBuffer = std::make_unique<uint8_t[]>(pixelBufferSize);
+
+    uint8_t* pixelWriteBuffer = (uint8_t*)pixelBuffer.get();
+    const uint8_t* pixelWriteBufferEnd = pixelWriteBuffer + pixelBufferSize;
+
+    // Samples per row in output buffer
+    row_stride = cinfo.output_width * cinfo.output_components;
+    // Make a one-row-high sample array that will go away when done with image
+    buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, row_stride, 1);
+
+    // Step 6: while (scan lines remain to be read)
+    //           jpeg_read_scanlines(...);
+
+    // Here we use the library's state variable cinfo->output_scanline as the
+    // loop counter, so that we don't have to keep track ourselves.
+    //
+    while (cinfo.output_scanline < cinfo.output_height)
+    {
+        // jpeg_read_scanlines expects an array of pointers to scanlines.
+        // Here the array is only one element long, but you could ask for
+        // more than one scanline at a time if that's more convenient.
+        jpeg_read_scanlines(&cinfo, buffer, 1);
+
+        if (pixelWriteBuffer + row_stride > pixelWriteBufferEnd)
+        {
+            // memcpy would cause an overflow.
+            jpeg_finish_decompress(&cinfo);
+            jpeg_destroy_decompress(&cinfo);
+            return nullptr;
+        }
+        memcpy(pixelWriteBuffer, buffer[0], row_stride);
+        pixelWriteBuffer += row_stride;
+    }
+
+    // Step 7: Finish decompression
+    jpeg_finish_decompress(&cinfo);
+
+    // Step 8: Release JPEG decompression object
+    jpeg_destroy_decompress(&cinfo);
+
+    return std::make_unique<Bitmap>(cinfo.output_width,
+                                    cinfo.output_height,
+                                    Bitmap::PixelFormat::RGB,
+                                    std::move(pixelBuffer));
+}
\ No newline at end of file
diff --git a/decoders/src/decode_png.cpp b/decoders/src/decode_png.cpp
new file mode 100644
index 0000000..fcf441c
--- /dev/null
+++ b/decoders/src/decode_png.cpp
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2023 Rive
+ */
+
+#include "rive/decoders/bitmap_decoder.hpp"
+#include "png.h"
+#include <algorithm>
+#include <cassert>
+#include <string.h>
+#include <setjmp.h>
+
+struct EncodedImageBuffer
+{
+    const uint8_t* bytes;
+    size_t position;
+    size_t size;
+};
+
+static void ReadDataFromMemory(png_structp png_ptr, png_bytep outBytes, png_size_t byteCountToRead)
+{
+    png_voidp a = png_get_io_ptr(png_ptr);
+    if (a == nullptr)
+    {
+        return;
+    }
+    EncodedImageBuffer& stream = *(EncodedImageBuffer*)a;
+
+    size_t bytesRead = std::min(byteCountToRead, (stream.size - stream.position));
+    memcpy(outBytes, stream.bytes + stream.position, bytesRead);
+    stream.position += bytesRead;
+
+    if ((png_size_t)bytesRead != byteCountToRead)
+    {
+        // Report image error?
+    }
+}
+
+std::unique_ptr<Bitmap> DecodePng(const uint8_t bytes[], size_t byteCount)
+{
+    png_structp png_ptr;
+    png_infop info_ptr;
+    png_uint_32 width, height;
+    int bit_depth, color_type, interlace_type;
+    std::unique_ptr<uint8_t[]> pixelBuffer;
+    std::unique_ptr<png_bytep[]> rowsPointer;
+
+    png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
+
+    if (png_ptr == nullptr)
+    {
+        printf("DecodePng - libpng failed (png_create_read_struct).");
+        return nullptr;
+    }
+
+    info_ptr = png_create_info_struct(png_ptr);
+    if (info_ptr == NULL)
+    {
+        png_destroy_read_struct(&png_ptr, (png_infopp)NULL, (png_infopp)NULL);
+        printf("DecodePng - libpng failed (png_create_info_struct).");
+        return nullptr;
+    }
+
+    if (setjmp(png_jmpbuf(png_ptr)))
+    {
+        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
+        return nullptr;
+    }
+
+    EncodedImageBuffer stream = {bytes, 0, byteCount};
+
+    png_set_read_fn(png_ptr, &stream, ReadDataFromMemory);
+
+    png_read_info(png_ptr, info_ptr);
+
+    png_get_IHDR(png_ptr,
+                 info_ptr,
+                 &width,
+                 &height,
+                 &bit_depth,
+                 &color_type,
+                 &interlace_type,
+                 NULL,
+                 NULL);
+
+    if (color_type == PNG_COLOR_TYPE_PALETTE ||
+        (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8))
+    {
+        // expand to 3 or 4 channels
+        png_set_expand(png_ptr);
+    }
+
+    png_bytep trns = 0;
+    int trnsCount = 0;
+    if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
+    {
+        png_get_tRNS(png_ptr, info_ptr, &trns, &trnsCount, 0);
+        png_set_expand(png_ptr);
+    }
+
+    if (bit_depth == 16)
+    {
+        png_set_strip_16(png_ptr);
+    }
+
+    if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
+    {
+        png_set_gray_to_rgb(png_ptr);
+    }
+
+    png_read_update_info(png_ptr, info_ptr);
+    uint8_t channels = png_get_channels(png_ptr, info_ptr);
+
+    size_t pixelBufferSize =
+        static_cast<size_t>(width) * static_cast<size_t>(height) * static_cast<size_t>(channels);
+    pixelBuffer = std::make_unique<uint8_t[]>(pixelBufferSize);
+    const uint8_t* pixelBufferEnd = pixelBuffer.get() + pixelBufferSize;
+
+    rowsPointer = std::make_unique<png_bytep[]>(height);
+    png_bytep* rows = rowsPointer.get();
+    size_t rowStride = (size_t)width * (size_t)channels;
+    uint8_t* rowWrite = pixelBuffer.get();
+    for (png_uint_32 row = 0; row < height; row++)
+    {
+        rows[row] = rowWrite;
+        rowWrite += rowStride;
+        if (rowWrite > pixelBufferEnd)
+        {
+            // Read would overflow.
+            return nullptr;
+        }
+    }
+    png_read_image(png_ptr, rows);
+    png_read_end(png_ptr, info_ptr);
+
+    png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp) nullptr);
+
+    Bitmap::PixelFormat pixelFormat;
+    assert(channels == 3 || channels == 4);
+    switch (channels)
+    {
+        case 4:
+            pixelFormat = Bitmap::PixelFormat::RGBA;
+            break;
+        case 3:
+            pixelFormat = Bitmap::PixelFormat::RGB;
+            break;
+    }
+    return std::make_unique<Bitmap>(width, height, pixelFormat, std::move(pixelBuffer));
+}
diff --git a/decoders/src/decode_webp.cpp b/decoders/src/decode_webp.cpp
new file mode 100644
index 0000000..f261798
--- /dev/null
+++ b/decoders/src/decode_webp.cpp
@@ -0,0 +1,68 @@
+#include "rive/decoders/bitmap_decoder.hpp"
+#include "webp/decode.h"
+#include "webp/demux.h"
+#include <stdio.h>
+#include <vector>
+#include <memory>
+
+std::unique_ptr<Bitmap> DecodeWebP(const uint8_t bytes[], size_t byteCount)
+{
+    WebPDecoderConfig config;
+    if (!WebPInitDecoderConfig(&config))
+    {
+        fprintf(stderr, "DecodeWebP - Library version mismatch!\n");
+        return nullptr;
+    }
+    config.options.dithering_strength = 50;
+    config.options.alpha_dithering_strength = 100;
+
+    if (!WebPGetInfo(bytes, byteCount, nullptr, nullptr))
+    {
+        fprintf(stderr, "DecodeWebP - Input file doesn't appear to be WebP format.\n");
+    }
+
+    WebPData data = {bytes, byteCount};
+    WebPDemuxer* demuxer = WebPDemux(&data);
+    if (demuxer == nullptr)
+    {
+        fprintf(stderr, "DecodeWebP - Could not create demuxer.\n");
+    }
+
+    WebPIterator currentFrame;
+    if (!WebPDemuxGetFrame(demuxer, 1, &currentFrame))
+    {
+        fprintf(stderr, "DecodeWebP - WebPDemuxGetFrame couldn't get frame.\n");
+        WebPDemuxDelete(demuxer);
+        return nullptr;
+    }
+    config.output.colorspace = MODE_RGBA;
+
+    uint32_t width = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH);
+    uint32_t height = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT);
+
+    size_t pixelBufferSize =
+        static_cast<size_t>(width) * static_cast<size_t>(height) * static_cast<size_t>(4);
+    std::unique_ptr<uint8_t[]> pixelBuffer = std::make_unique<uint8_t[]>(pixelBufferSize);
+
+    config.output.u.RGBA.rgba = (uint8_t*)pixelBuffer.get();
+    config.output.u.RGBA.stride = (int)(width * 4);
+    config.output.u.RGBA.size = pixelBufferSize;
+    config.output.is_external_memory = 1;
+
+    if (WebPDecode(currentFrame.fragment.bytes, currentFrame.fragment.size, &config) !=
+        VP8_STATUS_OK)
+    {
+        fprintf(stderr, "DecodeWebP - WebPDemuxGetFrame couldn't decode.\n");
+        WebPDemuxReleaseIterator(&currentFrame);
+        WebPDemuxDelete(demuxer);
+        return nullptr;
+    }
+
+    WebPDemuxReleaseIterator(&currentFrame);
+    WebPDemuxDelete(demuxer);
+
+    return std::make_unique<Bitmap>(width,
+                                    height,
+                                    Bitmap::PixelFormat::RGBA,
+                                    std::move(pixelBuffer));
+}
\ No newline at end of file
diff --git a/dependencies/config_directories.sh b/dependencies/config_directories.sh
new file mode 100755
index 0000000..5b1b8de
--- /dev/null
+++ b/dependencies/config_directories.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+# Platform specific build scripts should source directly the platform specific
+# version of this scripts. If you're using an older script that tries to handle
+# all platforms then call this one. We've moved to using platform specific build
+# scripts where possible since most of our Windows builds diverge so far from
+# the mac/linux ones. Most Mac/Linux ones can still be shared.
+set -e
+
+unameOut="$(uname -s)"
+case "${unameOut}" in
+Linux*) machine=linux ;;
+Darwin*) machine=macosx ;;
+*) machine="unhandled:${unameOut}" ;;
+esac
+
+source $(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)/${machine}/config_directories.sh
diff --git a/dependencies/gen_harfbuzz_renames/gen_header.dart b/dependencies/gen_harfbuzz_renames/gen_header.dart
new file mode 100644
index 0000000..1acb016
--- /dev/null
+++ b/dependencies/gen_harfbuzz_renames/gen_header.dart
@@ -0,0 +1,80 @@
+import 'dart:collection';
+import 'dart:io';
+
+final skip = HashSet.from(
+  [
+    'hb_color_get_alpha',
+    'hb_color_get_green',
+    'hb_color_get_blue',
+    'hb_color_get_red',
+    'hb_glyph_info_get_glyph_flags',
+    'hb_declval',
+  ],
+);
+
+final extras = HashSet.from(
+  [
+    'lookup_standard_encoding_for_code',
+    'lookup_expert_encoding_for_code',
+    'lookup_expert_charset_for_sid',
+    'lookup_expert_subset_charset_for_sid',
+    'lookup_standard_encoding_for_sid',
+    'accelerator_t',
+    'get_seac_components',
+    'data_destroy_arabic',
+  ],
+);
+
+void main() {
+  final uniqueNames = HashSet<String>();
+  var header = StringBuffer();
+  header.writeln('// clang-format off');
+  header.writeln('// hb_*');
+  var contents = File('harfbuzz_names.txt').readAsStringSync();
+  RegExp exp = RegExp(r'\s(hb_([a-zA-Z0-9_]*))', multiLine: true);
+  Iterable<RegExpMatch> matches = exp.allMatches(contents);
+  for (final m in matches) {
+    var symbolName = m[1];
+    if (symbolName == null ||
+        skip.contains(symbolName) ||
+        uniqueNames.contains(symbolName)) {
+      continue;
+    }
+    uniqueNames.add(symbolName);
+    header.writeln('#define $symbolName rive_$symbolName');
+  }
+  header.writeln('// _hb_*');
+  {
+    RegExp exp = RegExp(r'\s_(hb_([a-zA-Z0-9_]*))', multiLine: true);
+    Iterable<RegExpMatch> matches = exp.allMatches(contents);
+    for (final m in matches) {
+      var symbolName = m[1];
+      if (symbolName == null ||
+          skip.contains(symbolName) ||
+          uniqueNames.contains(symbolName)) {
+        continue;
+      }
+      uniqueNames.add(symbolName);
+      header.writeln('#define $symbolName rive_$symbolName');
+    }
+  }
+  header.writeln('// __hb_*');
+  {
+    RegExp exp = RegExp(r'\s_(_hb_([a-zA-Z0-9_]*))', multiLine: true);
+    Iterable<RegExpMatch> matches = exp.allMatches(contents);
+    for (final m in matches) {
+      var symbolName = m[1];
+      if (symbolName == null ||
+          skip.contains(symbolName) ||
+          uniqueNames.contains(symbolName)) {
+        continue;
+      }
+      uniqueNames.add(symbolName);
+      header.writeln('#define $symbolName rive_$symbolName');
+    }
+  }
+  for (final symbolName in extras) {
+    header.writeln('#define $symbolName rive_$symbolName');
+  }
+  File('../rive_harfbuzz_renames.h').writeAsStringSync(header.toString());
+}
diff --git a/dependencies/gen_harfbuzz_renames/gen_renames.sh b/dependencies/gen_harfbuzz_renames/gen_renames.sh
new file mode 100755
index 0000000..1900497
--- /dev/null
+++ b/dependencies/gen_harfbuzz_renames/gen_renames.sh
@@ -0,0 +1,56 @@
+#!/bin/bash
+set -e
+
+# This script should be called on a Mac!
+
+if [[ ! -f "dependencies/bin/premake5" ]]; then
+    mkdir -p dependencies/bin
+    pushd dependencies
+    # v5.0.0-beta2 doesn't support apple silicon properly, update the branch
+    # once a stable one is avaialble that supports it
+    git clone --depth 1 --branch master https://github.com/premake/premake-core.git
+    pushd premake-core
+    if [[ $LOCAL_ARCH == "arm64" ]]; then
+        PREMAKE_MAKE_ARCH=ARM
+    else
+        PREMAKE_MAKE_ARCH=x86
+    fi
+    make -f Bootstrap.mak osx PLATFORM=$PREMAKE_MAKE_ARCH
+    cp bin/release/* ../bin
+    popd
+    popd
+fi
+
+export PREMAKE=$PWD/dependencies/bin/premake5
+
+for var in "$@"; do
+    if [[ $var = "clean" ]]; then
+        echo 'Cleaning...'
+        rm -fR out
+    fi
+done
+
+mkdir -p out
+mkdir -p out_with_renames
+
+pushd ../../../
+PACKAGES=$PWD
+popd
+export PREMAKE_PATH="dependencies/export-compile-commands":"$PACKAGES/runtime_unity/native_plugin/platform":"$PACKAGES/runtime/build":"$PREMAKE_PATH"
+
+$PREMAKE gmake2 --file=../premake5_harfbuzz_v2.lua --out=out --no-harfbuzz-renames
+pushd out
+make -j$(($(sysctl -n hw.physicalcpu) + 1))
+popd
+
+nm out/librive_harfbuzz.a --demangle &>harfbuzz_names.txt
+
+dart gen_header.dart
+
+# make with renames just to examine the exported symbols...
+$PREMAKE gmake2 --file=../premake5_harfbuzz_v2.lua --out=out_with_renames
+pushd out_with_renames
+make -j$(($(sysctl -n hw.physicalcpu) + 1))
+popd
+
+nm out_with_renames/librive_harfbuzz.a --demangle &>harfbuzz_renames.txt
diff --git a/dependencies/gen_libjpeg_renames/gen_header.dart b/dependencies/gen_libjpeg_renames/gen_header.dart
new file mode 100644
index 0000000..59cc213
--- /dev/null
+++ b/dependencies/gen_libjpeg_renames/gen_header.dart
@@ -0,0 +1,74 @@
+import 'dart:collection';
+import 'dart:io';
+
+final skip = HashSet.from(
+  [],
+);
+
+final extras = HashSet.from(
+  [
+    'read_quant_tables',
+    'read_scan_script',
+    'set_quality_ratings',
+    'set_quant_slots',
+    'set_sample_factors',
+    'read_color_map',
+    'enable_signal_catcher',
+    'start_progress_monitor',
+    'end_progress_monitor',
+    'read_stdin',
+    'write_stdout',
+    'jdiv_round_up',
+    'jround_up',
+    'jzero_far',
+    'jcopy_sample_rows',
+    'jcopy_block_row',
+    'jtransform_parse_crop_spec',
+    'jtransform_request_workspace',
+    'jtransform_adjust_parameters',
+    'jtransform_execute_transform',
+    'jtransform_perfect_transform',
+    'jcopy_markers_setup',
+    'jcopy_markers_execute',
+  ],
+);
+
+void main() {
+  final uniqueNames = HashSet<String>();
+  var header = StringBuffer();
+  header.writeln('// clang-format off');
+  header.writeln('// jpeg_*');
+  var contents = File('libjpeg_names.txt').readAsStringSync();
+  RegExp exp = RegExp(r'\s_(jpeg_([a-zA-Z0-9_]*))', multiLine: true);
+  Iterable<RegExpMatch> matches = exp.allMatches(contents);
+  for (final m in matches) {
+    var symbolName = m[1];
+    if (symbolName == null ||
+        skip.contains(symbolName) ||
+        uniqueNames.contains(symbolName)) {
+      continue;
+    }
+    uniqueNames.add(symbolName);
+    header.writeln('#define $symbolName rive_$symbolName');
+  }
+  header.writeln('// jinit_*');
+  {
+    RegExp exp = RegExp(r'\s_(jinit_([a-zA-Z0-9_]*))', multiLine: true);
+    Iterable<RegExpMatch> matches = exp.allMatches(contents);
+    for (final m in matches) {
+      var symbolName = m[1];
+      if (symbolName == null ||
+          skip.contains(symbolName) ||
+          uniqueNames.contains(symbolName)) {
+        continue;
+      }
+      uniqueNames.add(symbolName);
+      header.writeln('#define $symbolName rive_$symbolName');
+    }
+  }
+  header.writeln('// _j extras');
+  for (final symbolName in extras) {
+    header.writeln('#define $symbolName rive_$symbolName');
+  }
+  File('../rive_libjpeg_renames.h').writeAsStringSync(header.toString());
+}
diff --git a/dependencies/gen_libjpeg_renames/gen_renames.sh b/dependencies/gen_libjpeg_renames/gen_renames.sh
new file mode 100755
index 0000000..6f45c0b
--- /dev/null
+++ b/dependencies/gen_libjpeg_renames/gen_renames.sh
@@ -0,0 +1,58 @@
+#!/bin/bash
+set -e
+
+# This script should be called on a Mac!
+
+# NOTE: Before building, jconfig.txt needs to be renamed to jconfig.h
+
+if [[ ! -f "dependencies/bin/premake5" ]]; then
+    mkdir -p dependencies/bin
+    pushd dependencies
+    # v5.0.0-beta2 doesn't support apple silicon properly, update the branch
+    # once a stable one is avaialble that supports it
+    git clone --depth 1 --branch master https://github.com/premake/premake-core.git
+    pushd premake-core
+    if [[ $LOCAL_ARCH == "arm64" ]]; then
+        PREMAKE_MAKE_ARCH=ARM
+    else
+        PREMAKE_MAKE_ARCH=x86
+    fi
+    make -f Bootstrap.mak osx PLATFORM=$PREMAKE_MAKE_ARCH
+    cp bin/release/* ../bin
+    popd
+    popd
+fi
+
+export PREMAKE=$PWD/dependencies/bin/premake5
+
+for var in "$@"; do
+    if [[ $var = "clean" ]]; then
+        echo 'Cleaning...'
+        rm -fR out
+    fi
+done
+
+mkdir -p out
+mkdir -p out_with_renames
+
+pushd ../../../
+PACKAGES=$PWD
+popd
+export PREMAKE_PATH="dependencies/export-compile-commands":"$PACKAGES/runtime_unity/native_plugin/platform":"$PACKAGES/runtime/build":"$PREMAKE_PATH"
+
+$PREMAKE gmake2 --file=../premake5_libjpeg_v2.lua --out=out --no-libjpeg-renames
+pushd out
+make -j$(($(sysctl -n hw.physicalcpu) + 1))
+popd
+
+nm out/liblibjpeg.a --demangle &>libjpeg_names.txt
+
+dart gen_header.dart
+
+# make with renames just to examine the exported symbols...
+$PREMAKE gmake2 --file=../premake5_libjpeg_v2.lua --out=out_with_renames
+pushd out_with_renames
+make -j$(($(sysctl -n hw.physicalcpu) + 1))
+popd
+
+nm out_with_renames/liblibjpeg.a --demangle &>libjpeg_renames.txt
diff --git a/dependencies/gen_yoga_renames/gen_header.dart b/dependencies/gen_yoga_renames/gen_header.dart
new file mode 100644
index 0000000..d5f724d
--- /dev/null
+++ b/dependencies/gen_yoga_renames/gen_header.dart
@@ -0,0 +1,46 @@
+import 'dart:collection';
+import 'dart:io';
+
+final skip = HashSet.from(
+  [],
+);
+
+final extras = HashSet.from(
+  [],
+);
+
+void main() {
+  final uniqueNames = HashSet<String>();
+  var header = StringBuffer();
+  header.writeln('// clang-format off');
+  header.writeln('// YG*');
+  var contents = File('yoga_names.txt').readAsStringSync();
+  RegExp exp = RegExp(r'\s(YG([a-zA-Z0-9_]*))', multiLine: true);
+  Iterable<RegExpMatch> matches = exp.allMatches(contents);
+  for (final m in matches) {
+    var symbolName = m[1];
+    if (symbolName == null ||
+        skip.contains(symbolName) ||
+        uniqueNames.contains(symbolName)) {
+      continue;
+    }
+    uniqueNames.add(symbolName);
+    header.writeln('#define $symbolName rive_$symbolName');
+  }
+  header.writeln('// _YG*');
+  {
+    RegExp exp = RegExp(r'\s_(YG([a-zA-Z0-9_]*))', multiLine: true);
+    Iterable<RegExpMatch> matches = exp.allMatches(contents);
+    for (final m in matches) {
+      var symbolName = m[1];
+      if (symbolName == null ||
+          skip.contains(symbolName) ||
+          uniqueNames.contains(symbolName)) {
+        continue;
+      }
+      uniqueNames.add(symbolName);
+      header.writeln('#define $symbolName rive_$symbolName');
+    }
+  }
+  File('../rive_yoga_renames.h').writeAsStringSync(header.toString());
+}
diff --git a/dependencies/gen_yoga_renames/gen_renames.sh b/dependencies/gen_yoga_renames/gen_renames.sh
new file mode 100755
index 0000000..eadf7d7
--- /dev/null
+++ b/dependencies/gen_yoga_renames/gen_renames.sh
@@ -0,0 +1,56 @@
+#!/bin/bash
+set -e
+
+# This script should be called on a Mac!
+
+if [[ ! -f "dependencies/bin/premake5" ]]; then
+    mkdir -p dependencies/bin
+    pushd dependencies
+    # v5.0.0-beta2 doesn't support apple silicon properly, update the branch
+    # once a stable one is avaialble that supports it
+    git clone --depth 1 --branch master https://github.com/premake/premake-core.git
+    pushd premake-core
+    if [[ $LOCAL_ARCH == "arm64" ]]; then
+        PREMAKE_MAKE_ARCH=ARM
+    else
+        PREMAKE_MAKE_ARCH=x86
+    fi
+    make -f Bootstrap.mak osx PLATFORM=$PREMAKE_MAKE_ARCH
+    cp bin/release/* ../bin
+    popd
+    popd
+fi
+
+export PREMAKE=$PWD/dependencies/bin/premake5
+
+for var in "$@"; do
+    if [[ $var = "clean" ]]; then
+        echo 'Cleaning...'
+        rm -fR out
+    fi
+done
+
+mkdir -p out
+mkdir -p out_with_renames
+
+pushd ../../../
+PACKAGES=$PWD
+popd
+export PREMAKE_PATH="dependencies/export-compile-commands":"$PACKAGES/runtime_unity/native_plugin/platform":"$PACKAGES/runtime/build":"$PREMAKE_PATH"
+
+$PREMAKE gmake2 --file=../premake5_yoga_v2.lua --out=out --no-yoga-renames
+pushd out
+make -j$(($(sysctl -n hw.physicalcpu) + 1))
+popd
+
+nm out/librive_yoga.a --demangle &>yoga_names.txt
+
+dart gen_header.dart
+
+# make with renames just to examine the exported symbols...
+$PREMAKE gmake2 --file=../premake5_yoga_v2.lua --out=out_with_renames
+pushd out_with_renames
+make -j$(($(sysctl -n hw.physicalcpu) + 1))
+popd
+
+nm out_with_renames/librive_yoga.a --demangle &>yoga_renames.txt
diff --git a/dependencies/jconfig.h b/dependencies/jconfig.h
new file mode 100644
index 0000000..84c05fc
--- /dev/null
+++ b/dependencies/jconfig.h
@@ -0,0 +1,13 @@
+#include <stdio.h> // Required on Mac -- libjpg expects FILE to be already defined.
+
+#define HAVE_PROTOTYPES
+#define HAVE_UNSIGNED_CHAR
+#define HAVE_UNSIGNED_SHORT
+#undef CHAR_IS_UNSIGNED
+#define HAVE_STDDEF_H
+#define HAVE_STDLIB_H
+#undef NEED_BSD_STRINGS
+#undef NEED_SYS_TYPES_H
+#undef NEED_FAR_POINTERS
+#undef NEED_SHORT_EXTERNAL_NAMES
+#undef INCOMPLETE_TYPES_BROKEN
diff --git a/dependencies/linux/config_directories.sh b/dependencies/linux/config_directories.sh
new file mode 120000
index 0000000..f47195b
--- /dev/null
+++ b/dependencies/linux/config_directories.sh
@@ -0,0 +1 @@
+../macosx/config_directories.sh
\ No newline at end of file
diff --git a/dependencies/macosx/.gitignore b/dependencies/macosx/.gitignore
new file mode 100644
index 0000000..06cf653
--- /dev/null
+++ b/dependencies/macosx/.gitignore
@@ -0,0 +1 @@
+cache
diff --git a/dependencies/macosx/config_directories.sh b/dependencies/macosx/config_directories.sh
new file mode 100755
index 0000000..7bcf2cb
--- /dev/null
+++ b/dependencies/macosx/config_directories.sh
@@ -0,0 +1,4 @@
+SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
+export DEPENDENCIES_SCRIPTS=$SCRIPT_DIR
+export DEPENDENCIES=$SCRIPT_DIR/cache
+mkdir -p $DEPENDENCIES
diff --git a/dependencies/macosx/get_earcut.sh b/dependencies/macosx/get_earcut.sh
new file mode 100755
index 0000000..9fca44e
--- /dev/null
+++ b/dependencies/macosx/get_earcut.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+set -e
+
+if [[ -z "${DEPENDENCIES}" ]]; then
+    echo "DEPENDENCIES env variable must be set. This script is usually called by other scripts."
+    exit 1
+fi
+
+pushd $DEPENDENCIES
+EARCUT_REPO=https://github.com/mapbox/earcut.hpp
+EARCUT_STABLE_BRANCH=master
+
+if [ ! -d earcut.hpp ]; then
+    echo "Cloning earcut."
+    git clone $EARCUT_REPO
+fi
+
+pushd earcut.hpp
+git checkout $EARCUT_STABLE_BRANCH && git fetch && git pull
+popd
+
+popd
diff --git a/dependencies/macosx/get_imgui.sh b/dependencies/macosx/get_imgui.sh
new file mode 100755
index 0000000..97aa788
--- /dev/null
+++ b/dependencies/macosx/get_imgui.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+set -e
+
+if [[ -z "${DEPENDENCIES}" ]]; then
+    echo "DEPENDENCIES env variable must be set. This script is usually called by other scripts."
+    exit 1
+fi
+pushd $DEPENDENCIES
+IMGUI_REPO=https://github.com/ocornut/imgui
+IMGUI_STABLE_BRANCH=master
+
+if [ ! -d imgui ]; then
+    echo "Cloning ImGui."
+    git clone $IMGUI_REPO
+fi
+
+cd imgui && git checkout $IMGUI_STABLE_BRANCH && git fetch && git pull
diff --git a/dependencies/macosx/get_libtess2.sh b/dependencies/macosx/get_libtess2.sh
new file mode 100755
index 0000000..522ce85
--- /dev/null
+++ b/dependencies/macosx/get_libtess2.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+set -e
+
+if [[ -z "${DEPENDENCIES}" ]]; then
+    echo "DEPENDENCIES env variable must be set. This script is usually called by other scripts."
+    exit 1
+fi
+
+pushd $DEPENDENCIES
+LIBTESS2_REPO=https://github.com/memononen/libtess2
+LIBTESS2_STABLE_BRANCH=master
+
+if [ ! -d libtess2 ]; then
+    echo "Cloning libtess2."
+    git clone $LIBTESS2_REPO
+fi
+
+pushd libtess2
+git checkout $LIBTESS2_STABLE_BRANCH && git fetch && git pull
+popd
+
+popd
diff --git a/dependencies/macosx/get_premake5.sh b/dependencies/macosx/get_premake5.sh
new file mode 100755
index 0000000..bb9e65e
--- /dev/null
+++ b/dependencies/macosx/get_premake5.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+set -e
+if [[ -z "${DEPENDENCIES}" ]]; then
+    echo "DEPENDENCIES env variable must be set. This script is usually called by other scripts."
+    exit 1
+fi
+
+mkdir -p $DEPENDENCIES/bin
+echo Downloading Premake5
+curl https://github.com/premake/premake-core/releases/download/v5.0.0-beta1/premake-5.0.0-beta1-macosx.tar.gz -L -o $DEPENDENCIES//bin/premake_macosx.tar.gz
+cd $DEPENDENCIES/bin
+# Export premake5 into bin
+tar -xvf premake_macosx.tar.gz 2>/dev/null
+# Delete downloaded archive
+rm premake_macosx.tar.gz
diff --git a/dependencies/macosx/get_sokol.sh b/dependencies/macosx/get_sokol.sh
new file mode 100755
index 0000000..ff2ac98
--- /dev/null
+++ b/dependencies/macosx/get_sokol.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+set -e
+
+if [[ -z "${DEPENDENCIES}" ]]; then
+    echo "DEPENDENCIES env variable must be set. This script is usually called by other scripts."
+    exit 1
+fi
+
+pushd $DEPENDENCIES
+SOKOL_REPO=https://github.com/luigi-rosso/sokol
+SOKOL_STABLE_BRANCH=support_transparent_framebuffer
+
+if [ ! -d sokol ]; then
+    echo "Cloning sokol."
+    git clone $SOKOL_REPO
+
+    if [ $(arch) == arm64 ]; then
+        SOKOL_SHDC=https://github.com/floooh/sokol-tools-bin/raw/6c8fc754ad73bf0db759ae44b224093290e5b26e/bin/osx_arm64/sokol-shdc
+    else
+        SOKOL_SHDC=https://github.com/floooh/sokol-tools-bin/raw/6c8fc754ad73bf0db759ae44b224093290e5b26e/bin/osx/sokol-shdc
+    fi
+    curl $SOKOL_SHDC -L -o ./bin/sokol-shdc
+    chmod +x ./bin/sokol-shdc
+fi
+
+cd sokol && git checkout $SOKOL_STABLE_BRANCH && git fetch && git pull
+popd
diff --git a/dependencies/macosx/make_viewer_skia.sh b/dependencies/macosx/make_viewer_skia.sh
new file mode 100755
index 0000000..507781e
--- /dev/null
+++ b/dependencies/macosx/make_viewer_skia.sh
@@ -0,0 +1,123 @@
+#!/bin/sh
+
+set -e
+
+if [[ -z "${DEPENDENCIES}" ]]; then
+    echo "DEPENDENCIES env variable must be set. This script is usually called by other scripts."
+    exit 1
+fi
+
+pushd $DEPENDENCIES
+
+if [ ! -d skia ]; then
+    git clone https://github.com/google/skia skia
+    cd skia
+    git checkout chrome/m99
+else
+    cd skia
+fi
+
+python3 tools/git-sync-deps
+
+CONFIG=debug
+RENDERER=
+SKIA_USE_GL=false
+SKIA_USE_METAL=false
+
+for var in "$@"; do
+    if [[ $var = "release" ]]; then
+        CONFIG=release
+    fi
+    if [[ $var = "gl" ]]; then
+        SKIA_USE_GL=true
+        RENDERER=gl
+    fi
+    if [[ $var = "metal" ]]; then
+        SKIA_USE_METAL=true
+        RENDERER=metal
+    fi
+done
+
+if [[ $CONFIG = "debug" ]]; then
+    bin/gn gen out/$RENDERER/debug --type=static_library --args=" \
+    extra_cflags=[\"-fno-rtti\", \"-DSK_DISABLE_SKPICTURE\", \"-DSK_DISABLE_TEXT\", \"-DRIVE_OPTIMIZED\", \"-DSK_DISABLE_LEGACY_SHADERCONTEXT\", \"-DSK_DISABLE_LOWP_RASTER_PIPELINE\", \"-DSK_FORCE_RASTER_PIPELINE_BLITTER\", \"-DSK_DISABLE_AAA\", \"-DSK_DISABLE_EFFECT_DESERIALIZATION\"] \
+
+    is_official_build=false \
+    skia_use_gl=$SKIA_USE_GL \
+    skia_use_zlib=true \
+    skia_enable_gpu=true \
+    skia_enable_fontmgr_empty=false \
+    skia_use_libpng_encode=true \
+    skia_use_libpng_decode=true \
+    skia_enable_skgpu_v1=true \
+
+    skia_use_dng_sdk=false \
+    skia_use_egl=false \
+    skia_use_expat=false \
+    skia_use_fontconfig=false \
+    skia_use_freetype=false \
+    skia_use_icu=false \
+    skia_use_libheif=false \
+    skia_use_system_libpng=false \
+    skia_use_system_libjpeg_turbo=false \
+    skia_use_libjpeg_turbo_encode=false \
+    skia_use_libjpeg_turbo_decode=true \
+    skia_use_libwebp_encode=false \
+    skia_use_libwebp_decode=true \
+    skia_use_system_libwebp=false \
+    skia_use_lua=false \
+    skia_use_piex=false \
+    skia_use_vulkan=false \
+    skia_use_metal=$SKIA_USE_METAL \
+    skia_use_angle=false \
+    skia_use_system_zlib=false \
+    skia_enable_spirv_validation=false \
+    skia_enable_pdf=false \
+    skia_enable_skottie=false \
+    skia_enable_tools=false \
+    "
+    ninja -C out/$RENDERER/debug
+    du -hs out/$RENDERER/debug/libskia.a
+fi
+
+if [[ $CONFIG = "release" ]]; then
+    bin/gn gen out/$RENDERER/release --type=static_library --args=" \
+    extra_cflags=[\"-fno-rtti\", \"-flto=full\", \"-DSK_DISABLE_SKPICTURE\", \"-DSK_DISABLE_TEXT\", \"-DRIVE_OPTIMIZED\", \"-DSK_DISABLE_LEGACY_SHADERCONTEXT\", \"-DSK_DISABLE_LOWP_RASTER_PIPELINE\", \"-DSK_FORCE_RASTER_PIPELINE_BLITTER\", \"-DSK_DISABLE_AAA\", \"-DSK_DISABLE_EFFECT_DESERIALIZATION\"] \
+
+    is_official_build=true \
+    skia_use_gl=true \
+    skia_use_zlib=true \
+    skia_enable_gpu=true \
+    skia_enable_fontmgr_empty=false \
+    skia_use_libpng_encode=true \
+    skia_use_libpng_decode=true \
+    skia_enable_skgpu_v1=true \
+
+    skia_use_dng_sdk=false \
+    skia_use_egl=false \
+    skia_use_expat=false \
+    skia_use_fontconfig=false \
+    skia_use_freetype=false \
+    skia_use_icu=false \
+    skia_use_libheif=false \
+    skia_use_system_libpng=false \
+    skia_use_system_libjpeg_turbo=false \
+    skia_use_libjpeg_turbo_encode=false \
+    skia_use_libjpeg_turbo_decode=true \
+    skia_use_libwebp_encode=false \
+    skia_use_libwebp_decode=true \
+    skia_use_system_libwebp=false \
+    skia_use_lua=false \
+    skia_use_piex=false \
+    skia_use_vulkan=false \
+    skia_use_metal=true \
+    skia_use_angle=false \
+    skia_use_system_zlib=false \
+    skia_enable_spirv_validation=false \
+    skia_enable_pdf=false \
+    skia_enable_skottie=false \
+    skia_enable_tools=false \
+    "
+    ninja -C out/$RENDERER/release
+    du -hs out/$RENDERER/release/libskia.a
+fi
diff --git a/dependencies/premake5_harfbuzz.lua b/dependencies/premake5_harfbuzz.lua
new file mode 100644
index 0000000..768cb13
--- /dev/null
+++ b/dependencies/premake5_harfbuzz.lua
@@ -0,0 +1,358 @@
+require('setup_compiler')
+local dependency = require('dependency')
+harfbuzz = dependency.github('rive-app/harfbuzz', 'rive_8.3.0')
+
+workspace('rive')
+configurations({ 'debug', 'release' })
+
+project('rive_harfbuzz')
+do
+    kind('StaticLib')
+    language('C++')
+    cppdialect('C++17')
+    targetdir('%{cfg.system}/cache/bin/%{cfg.buildcfg}/')
+    objdir('%{cfg.system}/cache/obj/%{cfg.buildcfg}/')
+
+    includedirs({ '../', harfbuzz .. '/src' })
+
+    files({
+        harfbuzz .. '/src/hb-aat-layout-ankr-table.hh',
+        harfbuzz .. '/src/hb-aat-layout-bsln-table.hh',
+        harfbuzz .. '/src/hb-aat-layout-common.hh',
+        harfbuzz .. '/src/hb-aat-layout-feat-table.hh',
+        harfbuzz .. '/src/hb-aat-layout-just-table.hh',
+        harfbuzz .. '/src/hb-aat-layout-kerx-table.hh',
+        harfbuzz .. '/src/hb-aat-layout-morx-table.hh',
+        harfbuzz .. '/src/hb-aat-layout-opbd-table.hh',
+        harfbuzz .. '/src/hb-aat-layout-trak-table.hh',
+        harfbuzz .. '/src/hb-aat-layout.cc',
+        harfbuzz .. '/src/hb-aat-layout.hh',
+        harfbuzz .. '/src/hb-aat-ltag-table.hh',
+        harfbuzz .. '/src/hb-aat-map.cc',
+        harfbuzz .. '/src/hb-aat-map.hh',
+        harfbuzz .. '/src/hb-aat.h',
+        harfbuzz .. '/src/hb-algs.hh',
+        harfbuzz .. '/src/hb-array.hh',
+        harfbuzz .. '/src/hb-atomic.hh',
+        harfbuzz .. '/src/hb-bimap.hh',
+        harfbuzz .. '/src/hb-bit-page.hh',
+        harfbuzz .. '/src/hb-bit-set-invertible.hh',
+        harfbuzz .. '/src/hb-bit-set.hh',
+        harfbuzz .. '/src/hb-blob.cc',
+        harfbuzz .. '/src/hb-blob.hh',
+        harfbuzz .. '/src/hb-buffer-deserialize-json.hh',
+        harfbuzz .. '/src/hb-buffer-deserialize-text.hh',
+        harfbuzz .. '/src/hb-buffer-serialize.cc',
+        harfbuzz .. '/src/hb-buffer-verify.cc',
+        harfbuzz .. '/src/hb-buffer.cc',
+        harfbuzz .. '/src/hb-buffer.hh',
+        harfbuzz .. '/src/hb-cache.hh',
+        harfbuzz .. '/src/hb-cff-interp-common.hh',
+        harfbuzz .. '/src/hb-cff-interp-cs-common.hh',
+        harfbuzz .. '/src/hb-cff-interp-dict-common.hh',
+        harfbuzz .. '/src/hb-cff1-interp-cs.hh',
+        harfbuzz .. '/src/hb-cff2-interp-cs.hh',
+        harfbuzz .. '/src/hb-common.cc',
+        harfbuzz .. '/src/hb-config.hh',
+        harfbuzz .. '/src/hb-debug.hh',
+        harfbuzz .. '/src/hb-dispatch.hh',
+        harfbuzz .. '/src/hb-draw.cc',
+        harfbuzz .. '/src/hb-draw.h',
+        harfbuzz .. '/src/hb-draw.hh',
+        harfbuzz .. '/src/hb-face.cc',
+        harfbuzz .. '/src/hb-face.hh',
+        harfbuzz .. '/src/hb-font.cc',
+        harfbuzz .. '/src/hb-font.hh',
+        harfbuzz .. '/src/hb-iter.hh',
+        harfbuzz .. '/src/hb-kern.hh',
+        harfbuzz .. '/src/hb-machinery.hh',
+        harfbuzz .. '/src/hb-map.cc',
+        harfbuzz .. '/src/hb-map.hh',
+        harfbuzz .. '/src/hb-meta.hh',
+        harfbuzz .. '/src/hb-ms-feature-ranges.hh',
+        harfbuzz .. '/src/hb-mutex.hh',
+        harfbuzz .. '/src/hb-null.hh',
+        harfbuzz .. '/src/hb-number-parser.hh',
+        harfbuzz .. '/src/hb-number.cc',
+        harfbuzz .. '/src/hb-number.hh',
+        harfbuzz .. '/src/hb-object.hh',
+        harfbuzz .. '/src/hb-open-file.hh',
+        harfbuzz .. '/src/hb-open-type.hh',
+        harfbuzz .. '/src/hb-ot-cff-common.hh',
+        harfbuzz .. '/src/hb-ot-cff1-std-str.hh',
+        harfbuzz .. '/src/hb-ot-cff1-table.cc',
+        harfbuzz .. '/src/hb-ot-cff1-table.hh',
+        harfbuzz .. '/src/hb-ot-cff2-table.cc',
+        harfbuzz .. '/src/hb-ot-cff2-table.hh',
+        harfbuzz .. '/src/hb-ot-cmap-table.hh',
+        harfbuzz .. '/src/hb-ot-color-cbdt-table.hh',
+        harfbuzz .. '/src/hb-ot-color-colr-table.hh',
+        harfbuzz .. '/src/hb-ot-color-colrv1-closure.hh',
+        harfbuzz .. '/src/hb-ot-color-cpal-table.hh',
+        harfbuzz .. '/src/hb-ot-color-sbix-table.hh',
+        harfbuzz .. '/src/hb-ot-color-svg-table.hh',
+        harfbuzz .. '/src/hb-ot-color.cc',
+        harfbuzz .. '/src/hb-ot-color.h',
+        harfbuzz .. '/src/hb-ot-deprecated.h',
+        harfbuzz .. '/src/hb-ot-face-table-list.hh',
+        harfbuzz .. '/src/hb-ot-face.cc',
+        harfbuzz .. '/src/hb-ot-face.hh',
+        harfbuzz .. '/src/hb-ot-font.cc',
+        harfbuzz .. '/src/hb-ot-gasp-table.hh',
+        harfbuzz .. '/src/hb-ot-glyf-table.hh',
+        harfbuzz .. '/src/hb-ot-hdmx-table.hh',
+        harfbuzz .. '/src/hb-ot-head-table.hh',
+        harfbuzz .. '/src/hb-ot-hhea-table.hh',
+        harfbuzz .. '/src/hb-ot-hmtx-table.hh',
+        harfbuzz .. '/src/hb-ot-kern-table.hh',
+        harfbuzz .. '/src/hb-ot-layout-base-table.hh',
+        harfbuzz .. '/src/hb-ot-layout-common.hh',
+        harfbuzz .. '/src/hb-ot-layout-gdef-table.hh',
+        harfbuzz .. '/src/hb-ot-layout-gpos-table.hh',
+        harfbuzz .. '/src/hb-ot-layout-gsub-table.hh',
+        harfbuzz .. '/src/hb-ot-layout-gsubgpos.hh',
+        harfbuzz .. '/src/hb-ot-layout-jstf-table.hh',
+        harfbuzz .. '/src/hb-ot-layout.cc',
+        harfbuzz .. '/src/hb-ot-layout.hh',
+        harfbuzz .. '/src/hb-ot-map.cc',
+        harfbuzz .. '/src/hb-ot-map.hh',
+        harfbuzz .. '/src/hb-ot-math-table.hh',
+        harfbuzz .. '/src/hb-ot-math.cc',
+        harfbuzz .. '/src/hb-ot-maxp-table.hh',
+        harfbuzz .. '/src/hb-ot-meta-table.hh',
+        harfbuzz .. '/src/hb-ot-meta.cc',
+        harfbuzz .. '/src/hb-ot-meta.h',
+        harfbuzz .. '/src/hb-ot-metrics.cc',
+        harfbuzz .. '/src/hb-ot-metrics.hh',
+        harfbuzz .. '/src/hb-ot-name-language-static.hh',
+        harfbuzz .. '/src/hb-ot-name-language.hh',
+        harfbuzz .. '/src/hb-ot-name-table.hh',
+        harfbuzz .. '/src/hb-ot-name.cc',
+        harfbuzz .. '/src/hb-ot-name.h',
+        harfbuzz .. '/src/hb-ot-os2-table.hh',
+        harfbuzz .. '/src/hb-ot-os2-unicode-ranges.hh',
+        harfbuzz .. '/src/hb-ot-post-macroman.hh',
+        harfbuzz .. '/src/hb-ot-post-table-v2subset.hh',
+        harfbuzz .. '/src/hb-ot-post-table.hh',
+        harfbuzz .. '/src/hb-ot-shaper-arabic-fallback.hh',
+        harfbuzz .. '/src/hb-ot-shaper-arabic-joining-list.hh',
+        harfbuzz .. '/src/hb-ot-shaper-arabic-pua.hh',
+        harfbuzz .. '/src/hb-ot-shaper-arabic-table.hh',
+        harfbuzz .. '/src/hb-ot-shaper-arabic-win1256.hh',
+        harfbuzz .. '/src/hb-ot-shaper-arabic.cc',
+        harfbuzz .. '/src/hb-ot-shaper-arabic.hh',
+        harfbuzz .. '/src/hb-ot-shaper-default.cc',
+        harfbuzz .. '/src/hb-ot-shaper-hangul.cc',
+        harfbuzz .. '/src/hb-ot-shaper-hebrew.cc',
+        harfbuzz .. '/src/hb-ot-shaper-indic-table.cc',
+        harfbuzz .. '/src/hb-ot-shaper-indic.cc',
+        harfbuzz .. '/src/hb-ot-shaper-indic.hh',
+        harfbuzz .. '/src/hb-ot-shaper-khmer.cc',
+        harfbuzz .. '/src/hb-ot-shaper-myanmar.cc',
+        harfbuzz .. '/src/hb-ot-shaper-syllabic.cc',
+        harfbuzz .. '/src/hb-ot-shaper-syllabic.hh',
+        harfbuzz .. '/src/hb-ot-shaper-thai.cc',
+        harfbuzz .. '/src/hb-ot-shaper-use-table.hh',
+        harfbuzz .. '/src/hb-ot-shaper-use.cc',
+        harfbuzz .. '/src/hb-ot-shaper-vowel-constraints.cc',
+        harfbuzz .. '/src/hb-ot-shaper-vowel-constraints.hh',
+        harfbuzz .. '/src/hb-ot-shaper.hh',
+        harfbuzz .. '/src/hb-ot-shaper-indic-machine.hh',
+        harfbuzz .. '/src/hb-ot-shaper-khmer-machine.hh',
+        harfbuzz .. '/src/hb-ot-shaper-myanmar-machine.hh',
+        harfbuzz .. '/src/hb-ot-shaper-use-machine.hh',
+        harfbuzz .. '/src/hb-ot-shape-fallback.cc',
+        harfbuzz .. '/src/hb-ot-shape-fallback.hh',
+        harfbuzz .. '/src/hb-ot-shape-normalize.cc',
+        harfbuzz .. '/src/hb-ot-shape-normalize.hh',
+        harfbuzz .. '/src/hb-ot-shape.cc',
+        harfbuzz .. '/src/hb-ot-shape.hh',
+        harfbuzz .. '/src/hb-ot-stat-table.hh',
+        harfbuzz .. '/src/hb-ot-tag-table.hh',
+        harfbuzz .. '/src/hb-ot-tag.cc',
+        harfbuzz .. '/src/hb-ot-var-avar-table.hh',
+        harfbuzz .. '/src/hb-ot-var-common.hh',
+        harfbuzz .. '/src/hb-ot-var-fvar-table.hh',
+        harfbuzz .. '/src/hb-ot-var-gvar-table.hh',
+        harfbuzz .. '/src/hb-ot-var-hvar-table.hh',
+        harfbuzz .. '/src/hb-ot-var-mvar-table.hh',
+        harfbuzz .. '/src/hb-ot-var.cc',
+        harfbuzz .. '/src/hb-ot-vorg-table.hh',
+        harfbuzz .. '/src/hb-pool.hh',
+        harfbuzz .. '/src/hb-priority-queue.hh',
+        harfbuzz .. '/src/hb-repacker.hh',
+        harfbuzz .. '/src/hb-sanitize.hh',
+        harfbuzz .. '/src/hb-serialize.hh',
+        harfbuzz .. '/src/hb-set-digest.hh',
+        harfbuzz .. '/src/hb-set.cc',
+        harfbuzz .. '/src/hb-set.hh',
+        harfbuzz .. '/src/hb-shape-plan.cc',
+        harfbuzz .. '/src/hb-shape-plan.hh',
+        harfbuzz .. '/src/hb-shape.cc',
+        harfbuzz .. '/src/hb-shaper-impl.hh',
+        harfbuzz .. '/src/hb-shaper-list.hh',
+        harfbuzz .. '/src/hb-shaper.cc',
+        harfbuzz .. '/src/hb-shaper.hh',
+        harfbuzz .. '/src/hb-static.cc',
+        harfbuzz .. '/src/hb-string-array.hh',
+        harfbuzz .. '/src/hb-subset-cff-common.cc',
+        harfbuzz .. '/src/hb-subset-cff-common.hh',
+        harfbuzz .. '/src/hb-subset-cff1.cc',
+        harfbuzz .. '/src/hb-subset-cff1.hh',
+        harfbuzz .. '/src/hb-subset-cff2.cc',
+        harfbuzz .. '/src/hb-subset-cff2.hh',
+        harfbuzz .. '/src/hb-subset-input.cc',
+        harfbuzz .. '/src/hb-subset-input.hh',
+        harfbuzz .. '/src/hb-subset-plan.cc',
+        harfbuzz .. '/src/hb-subset-plan.hh',
+        harfbuzz .. '/src/hb-subset-repacker.cc',
+        harfbuzz .. '/src/hb-subset-repacker.h',
+        harfbuzz .. '/src/hb-subset.cc',
+        harfbuzz .. '/src/hb-subset.hh',
+        harfbuzz .. '/src/hb-ucd-table.hh',
+        harfbuzz .. '/src/hb-ucd.cc',
+        harfbuzz .. '/src/hb-unicode-emoji-table.hh',
+        harfbuzz .. '/src/hb-unicode.cc',
+        harfbuzz .. '/src/hb-unicode.hh',
+        harfbuzz .. '/src/hb-utf.hh',
+        harfbuzz .. '/src/hb-vector.hh',
+        harfbuzz .. '/src/hb.hh',
+        harfbuzz .. '/src/graph/gsubgpos-context.cc',
+        harfbuzz .. '/src/hb-paint.cc',
+        harfbuzz .. '/src/hb-paint-extents.cc',
+        harfbuzz .. '/src/hb-outline.cc',
+    })
+
+    warnings('Off')
+
+    defines({
+        'HAVE_OT',
+        'HB_NO_FALLBACK_SHAPE',
+        'HB_NO_WIN1256',
+        'HB_NO_EXTERN_HELPERS',
+        'HB_DISABLE_DEPRECATED',
+        'HB_NO_COLOR',
+        'HB_NO_BITMAP',
+        'HB_NO_BUFFER_SERIALIZE',
+        'HB_NO_SETLOCALE',
+        'HB_NO_STYLE',
+        'HB_NO_VERTICAL',
+        'HB_NO_LAYOUT_COLLECT_GLYPHS',
+        'HB_NO_LAYOUT_RARELY_USED',
+        'HB_NO_LAYOUT_UNUSED',
+        'HB_NO_OT_FONT_GLYPH_NAMES',
+    })
+
+    filter('system:emscripten')
+    do
+        buildoptions({ '-pthread' })
+    end
+
+    filter('toolset:clang')
+    do
+        flags({ 'FatalWarnings' })
+        buildoptions({
+            '-Werror=format',
+            '-Wimplicit-int-conversion',
+            '-Werror=vla',
+        })
+    end
+    filter('toolset:msc')
+    do
+        buildoptions({
+            '/bigobj',
+        })
+    end
+
+    filter('configurations:debug')
+    do
+        defines({ 'DEBUG' })
+        symbols('On')
+    end
+
+    filter('configurations:release')
+    do
+        defines({ 'RELEASE' })
+        defines({ 'NDEBUG' })
+        optimize('On')
+    end
+
+    filter({ 'system:macosx', 'options:variant=runtime' })
+    do
+        buildoptions({
+            '-Wimplicit-float-conversion -fembed-bitcode -arch arm64 -arch x86_64 -isysroot '
+                .. (os.getenv('MACOS_SYSROOT') or ''),
+        })
+    end
+
+    filter({ 'system:macosx', 'configurations:release' })
+    do
+        buildoptions({ '-flto=full' })
+    end
+
+    filter({ 'system:ios' })
+    do
+        buildoptions({ '-flto=full' })
+    end
+
+    filter('system:windows')
+    do
+        architecture('x64')
+        defines({ '_USE_MATH_DEFINES' })
+    end
+
+    filter({ 'system:ios', 'options:variant=system' })
+    do
+        buildoptions({
+            '-mios-version-min=13.0 -fembed-bitcode -arch arm64 -isysroot '
+                .. (os.getenv('IOS_SYSROOT') or ''),
+        })
+    end
+
+    filter({ 'system:ios', 'options:variant=emulator' })
+    do
+        buildoptions({
+            '--target=arm64-apple-ios13.0.0-simulator',
+            '-mios-version-min=13.0 -arch arm64 -arch x86_64 -isysroot '
+                .. (os.getenv('IOS_SYSROOT') or ''),
+        })
+        targetdir('%{cfg.system}_sim/cache/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}_sim/cache/obj/%{cfg.buildcfg}')
+    end
+
+    filter({ 'system:android', 'configurations:release' })
+    do
+        buildoptions({ '-flto=full' })
+    end
+
+    -- Is there a way to pass 'arch' as a variable here?
+    filter({ 'system:android', 'options:arch=x86' })
+    do
+        targetdir('%{cfg.system}/cache/x86/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}/cache/x86/obj/%{cfg.buildcfg}')
+    end
+
+    filter({ 'system:android', 'options:arch=x64' })
+    do
+        targetdir('%{cfg.system}/cache/x64/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}/cache/x64/obj/%{cfg.buildcfg}')
+    end
+
+    filter({ 'system:android', 'options:arch=arm' })
+    do
+        targetdir('%{cfg.system}/cache/arm/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}/cache/arm/obj/%{cfg.buildcfg}')
+    end
+
+    filter({ 'system:android', 'options:arch=arm64' })
+    do
+        targetdir('%{cfg.system}/cache/arm64/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}/cache/arm64/obj/%{cfg.buildcfg}')
+    end
+
+    filter('system:macosx or system:ios')
+    do
+        defines({ 'HAVE_CORETEXT' })
+        files({ harfbuzz .. '/src/hb-coretext.cc' })
+    end
+end
diff --git a/dependencies/premake5_harfbuzz_v2.lua b/dependencies/premake5_harfbuzz_v2.lua
new file mode 100644
index 0000000..c26c1d2
--- /dev/null
+++ b/dependencies/premake5_harfbuzz_v2.lua
@@ -0,0 +1,283 @@
+dofile('rive_build_config.lua')
+
+local dependency = require('dependency')
+harfbuzz = dependency.github('rive-app/harfbuzz', 'rive_8.4.0')
+
+newoption({
+    trigger = 'no-harfbuzz-renames',
+    description = 'don\'t rename harfbuzz symbols',
+})
+
+project('rive_harfbuzz')
+do
+    kind('StaticLib')
+
+    includedirs({ '../', harfbuzz .. '/src' })
+
+    files({
+        harfbuzz .. '/src/hb-aat-layout-ankr-table.hh',
+        harfbuzz .. '/src/hb-aat-layout-bsln-table.hh',
+        harfbuzz .. '/src/hb-aat-layout-common.hh',
+        harfbuzz .. '/src/hb-aat-layout-feat-table.hh',
+        harfbuzz .. '/src/hb-aat-layout-just-table.hh',
+        harfbuzz .. '/src/hb-aat-layout-kerx-table.hh',
+        harfbuzz .. '/src/hb-aat-layout-morx-table.hh',
+        harfbuzz .. '/src/hb-aat-layout-opbd-table.hh',
+        harfbuzz .. '/src/hb-aat-layout-trak-table.hh',
+        harfbuzz .. '/src/hb-aat-layout.cc',
+        harfbuzz .. '/src/hb-aat-layout.hh',
+        harfbuzz .. '/src/hb-aat-ltag-table.hh',
+        harfbuzz .. '/src/hb-aat-map.cc',
+        harfbuzz .. '/src/hb-aat-map.hh',
+        harfbuzz .. '/src/hb-aat.h',
+        harfbuzz .. '/src/hb-algs.hh',
+        harfbuzz .. '/src/hb-array.hh',
+        harfbuzz .. '/src/hb-atomic.hh',
+        harfbuzz .. '/src/hb-bimap.hh',
+        harfbuzz .. '/src/hb-bit-page.hh',
+        harfbuzz .. '/src/hb-bit-set-invertible.hh',
+        harfbuzz .. '/src/hb-bit-set.hh',
+        harfbuzz .. '/src/hb-blob.cc',
+        harfbuzz .. '/src/hb-blob.hh',
+        harfbuzz .. '/src/hb-buffer-deserialize-json.hh',
+        harfbuzz .. '/src/hb-buffer-deserialize-text.hh',
+        harfbuzz .. '/src/hb-buffer-serialize.cc',
+        harfbuzz .. '/src/hb-buffer-verify.cc',
+        harfbuzz .. '/src/hb-buffer.cc',
+        harfbuzz .. '/src/hb-buffer.hh',
+        harfbuzz .. '/src/hb-cache.hh',
+        harfbuzz .. '/src/hb-cff-interp-common.hh',
+        harfbuzz .. '/src/hb-cff-interp-cs-common.hh',
+        harfbuzz .. '/src/hb-cff-interp-dict-common.hh',
+        harfbuzz .. '/src/hb-cff1-interp-cs.hh',
+        harfbuzz .. '/src/hb-cff2-interp-cs.hh',
+        harfbuzz .. '/src/hb-common.cc',
+        harfbuzz .. '/src/hb-config.hh',
+        harfbuzz .. '/src/hb-debug.hh',
+        harfbuzz .. '/src/hb-dispatch.hh',
+        harfbuzz .. '/src/hb-draw.cc',
+        harfbuzz .. '/src/hb-draw.h',
+        harfbuzz .. '/src/hb-draw.hh',
+        harfbuzz .. '/src/hb-face.cc',
+        harfbuzz .. '/src/hb-face.hh',
+        harfbuzz .. '/src/hb-font.cc',
+        harfbuzz .. '/src/hb-font.hh',
+        harfbuzz .. '/src/hb-iter.hh',
+        harfbuzz .. '/src/hb-kern.hh',
+        harfbuzz .. '/src/hb-machinery.hh',
+        harfbuzz .. '/src/hb-map.cc',
+        harfbuzz .. '/src/hb-map.hh',
+        harfbuzz .. '/src/hb-meta.hh',
+        harfbuzz .. '/src/hb-ms-feature-ranges.hh',
+        harfbuzz .. '/src/hb-mutex.hh',
+        harfbuzz .. '/src/hb-null.hh',
+        harfbuzz .. '/src/hb-number-parser.hh',
+        harfbuzz .. '/src/hb-number.cc',
+        harfbuzz .. '/src/hb-number.hh',
+        harfbuzz .. '/src/hb-object.hh',
+        harfbuzz .. '/src/hb-open-file.hh',
+        harfbuzz .. '/src/hb-open-type.hh',
+        harfbuzz .. '/src/hb-ot-cff-common.hh',
+        harfbuzz .. '/src/hb-ot-cff1-std-str.hh',
+        harfbuzz .. '/src/hb-ot-cff1-table.cc',
+        harfbuzz .. '/src/hb-ot-cff1-table.hh',
+        harfbuzz .. '/src/hb-ot-cff2-table.cc',
+        harfbuzz .. '/src/hb-ot-cff2-table.hh',
+        harfbuzz .. '/src/hb-ot-cmap-table.hh',
+        harfbuzz .. '/src/hb-ot-color-cbdt-table.hh',
+        harfbuzz .. '/src/hb-ot-color-colr-table.hh',
+        harfbuzz .. '/src/hb-ot-color-colrv1-closure.hh',
+        harfbuzz .. '/src/hb-ot-color-cpal-table.hh',
+        harfbuzz .. '/src/hb-ot-color-sbix-table.hh',
+        harfbuzz .. '/src/hb-ot-color-svg-table.hh',
+        harfbuzz .. '/src/hb-ot-color.cc',
+        harfbuzz .. '/src/hb-ot-color.h',
+        harfbuzz .. '/src/hb-ot-deprecated.h',
+        harfbuzz .. '/src/hb-ot-face-table-list.hh',
+        harfbuzz .. '/src/hb-ot-face.cc',
+        harfbuzz .. '/src/hb-ot-face.hh',
+        harfbuzz .. '/src/hb-ot-font.cc',
+        harfbuzz .. '/src/hb-ot-gasp-table.hh',
+        harfbuzz .. '/src/hb-ot-glyf-table.hh',
+        harfbuzz .. '/src/hb-ot-hdmx-table.hh',
+        harfbuzz .. '/src/hb-ot-head-table.hh',
+        harfbuzz .. '/src/hb-ot-hhea-table.hh',
+        harfbuzz .. '/src/hb-ot-hmtx-table.hh',
+        harfbuzz .. '/src/hb-ot-kern-table.hh',
+        harfbuzz .. '/src/hb-ot-layout-base-table.hh',
+        harfbuzz .. '/src/hb-ot-layout-common.hh',
+        harfbuzz .. '/src/hb-ot-layout-gdef-table.hh',
+        harfbuzz .. '/src/hb-ot-layout-gpos-table.hh',
+        harfbuzz .. '/src/hb-ot-layout-gsub-table.hh',
+        harfbuzz .. '/src/hb-ot-layout-gsubgpos.hh',
+        harfbuzz .. '/src/hb-ot-layout-jstf-table.hh',
+        harfbuzz .. '/src/hb-ot-layout.cc',
+        harfbuzz .. '/src/hb-ot-layout.hh',
+        harfbuzz .. '/src/hb-ot-map.cc',
+        harfbuzz .. '/src/hb-ot-map.hh',
+        harfbuzz .. '/src/hb-ot-math-table.hh',
+        harfbuzz .. '/src/hb-ot-math.cc',
+        harfbuzz .. '/src/hb-ot-maxp-table.hh',
+        harfbuzz .. '/src/hb-ot-meta-table.hh',
+        harfbuzz .. '/src/hb-ot-meta.cc',
+        harfbuzz .. '/src/hb-ot-meta.h',
+        harfbuzz .. '/src/hb-ot-metrics.cc',
+        harfbuzz .. '/src/hb-ot-metrics.hh',
+        harfbuzz .. '/src/hb-ot-name-language-static.hh',
+        harfbuzz .. '/src/hb-ot-name-language.hh',
+        harfbuzz .. '/src/hb-ot-name-table.hh',
+        harfbuzz .. '/src/hb-ot-name.cc',
+        harfbuzz .. '/src/hb-ot-name.h',
+        harfbuzz .. '/src/hb-ot-os2-table.hh',
+        harfbuzz .. '/src/hb-ot-os2-unicode-ranges.hh',
+        harfbuzz .. '/src/hb-ot-post-macroman.hh',
+        harfbuzz .. '/src/hb-ot-post-table-v2subset.hh',
+        harfbuzz .. '/src/hb-ot-post-table.hh',
+        harfbuzz .. '/src/hb-ot-shaper-arabic-fallback.hh',
+        harfbuzz .. '/src/hb-ot-shaper-arabic-joining-list.hh',
+        harfbuzz .. '/src/hb-ot-shaper-arabic-pua.hh',
+        harfbuzz .. '/src/hb-ot-shaper-arabic-table.hh',
+        harfbuzz .. '/src/hb-ot-shaper-arabic-win1256.hh',
+        harfbuzz .. '/src/hb-ot-shaper-arabic.cc',
+        harfbuzz .. '/src/hb-ot-shaper-arabic.hh',
+        harfbuzz .. '/src/hb-ot-shaper-default.cc',
+        harfbuzz .. '/src/hb-ot-shaper-hangul.cc',
+        harfbuzz .. '/src/hb-ot-shaper-hebrew.cc',
+        harfbuzz .. '/src/hb-ot-shaper-indic-table.cc',
+        harfbuzz .. '/src/hb-ot-shaper-indic.cc',
+        harfbuzz .. '/src/hb-ot-shaper-indic.hh',
+        harfbuzz .. '/src/hb-ot-shaper-khmer.cc',
+        harfbuzz .. '/src/hb-ot-shaper-myanmar.cc',
+        harfbuzz .. '/src/hb-ot-shaper-syllabic.cc',
+        harfbuzz .. '/src/hb-ot-shaper-syllabic.hh',
+        harfbuzz .. '/src/hb-ot-shaper-thai.cc',
+        harfbuzz .. '/src/hb-ot-shaper-use-table.hh',
+        harfbuzz .. '/src/hb-ot-shaper-use.cc',
+        harfbuzz .. '/src/hb-ot-shaper-vowel-constraints.cc',
+        harfbuzz .. '/src/hb-ot-shaper-vowel-constraints.hh',
+        harfbuzz .. '/src/hb-ot-shaper.hh',
+        harfbuzz .. '/src/hb-ot-shaper-indic-machine.hh',
+        harfbuzz .. '/src/hb-ot-shaper-khmer-machine.hh',
+        harfbuzz .. '/src/hb-ot-shaper-myanmar-machine.hh',
+        harfbuzz .. '/src/hb-ot-shaper-use-machine.hh',
+        harfbuzz .. '/src/hb-ot-shape-fallback.cc',
+        harfbuzz .. '/src/hb-ot-shape-fallback.hh',
+        harfbuzz .. '/src/hb-ot-shape-normalize.cc',
+        harfbuzz .. '/src/hb-ot-shape-normalize.hh',
+        harfbuzz .. '/src/hb-ot-shape.cc',
+        harfbuzz .. '/src/hb-ot-shape.hh',
+        harfbuzz .. '/src/hb-ot-stat-table.hh',
+        harfbuzz .. '/src/hb-ot-tag-table.hh',
+        harfbuzz .. '/src/hb-ot-tag.cc',
+        harfbuzz .. '/src/hb-ot-var-avar-table.hh',
+        harfbuzz .. '/src/hb-ot-var-common.hh',
+        harfbuzz .. '/src/hb-ot-var-fvar-table.hh',
+        harfbuzz .. '/src/hb-ot-var-gvar-table.hh',
+        harfbuzz .. '/src/hb-ot-var-hvar-table.hh',
+        harfbuzz .. '/src/hb-ot-var-mvar-table.hh',
+        harfbuzz .. '/src/hb-ot-var.cc',
+        harfbuzz .. '/src/hb-ot-vorg-table.hh',
+        harfbuzz .. '/src/hb-pool.hh',
+        harfbuzz .. '/src/hb-priority-queue.hh',
+        harfbuzz .. '/src/hb-repacker.hh',
+        harfbuzz .. '/src/hb-sanitize.hh',
+        harfbuzz .. '/src/hb-serialize.hh',
+        harfbuzz .. '/src/hb-set-digest.hh',
+        harfbuzz .. '/src/hb-set.cc',
+        harfbuzz .. '/src/hb-set.hh',
+        harfbuzz .. '/src/hb-shape-plan.cc',
+        harfbuzz .. '/src/hb-shape-plan.hh',
+        harfbuzz .. '/src/hb-shape.cc',
+        harfbuzz .. '/src/hb-shaper-impl.hh',
+        harfbuzz .. '/src/hb-shaper-list.hh',
+        harfbuzz .. '/src/hb-shaper.cc',
+        harfbuzz .. '/src/hb-shaper.hh',
+        harfbuzz .. '/src/hb-static.cc',
+        harfbuzz .. '/src/hb-string-array.hh',
+        harfbuzz .. '/src/hb-subset-cff-common.cc',
+        harfbuzz .. '/src/hb-subset-cff-common.hh',
+        harfbuzz .. '/src/hb-subset-cff1.cc',
+        harfbuzz .. '/src/hb-subset-cff1.hh',
+        harfbuzz .. '/src/hb-subset-cff2.cc',
+        harfbuzz .. '/src/hb-subset-cff2.hh',
+        harfbuzz .. '/src/hb-subset-input.cc',
+        harfbuzz .. '/src/hb-subset-input.hh',
+        harfbuzz .. '/src/hb-subset-plan.cc',
+        harfbuzz .. '/src/hb-subset-plan.hh',
+        harfbuzz .. '/src/hb-subset-repacker.cc',
+        harfbuzz .. '/src/hb-subset-repacker.h',
+        harfbuzz .. '/src/hb-subset.cc',
+        harfbuzz .. '/src/hb-subset.hh',
+        harfbuzz .. '/src/hb-ucd-table.hh',
+        harfbuzz .. '/src/hb-ucd.cc',
+        harfbuzz .. '/src/hb-unicode-emoji-table.hh',
+        harfbuzz .. '/src/hb-unicode.cc',
+        harfbuzz .. '/src/hb-unicode.hh',
+        harfbuzz .. '/src/hb-utf.hh',
+        harfbuzz .. '/src/hb-vector.hh',
+        harfbuzz .. '/src/hb.hh',
+        harfbuzz .. '/src/graph/gsubgpos-context.cc',
+        harfbuzz .. '/src/hb-paint.cc',
+        harfbuzz .. '/src/hb-paint-extents.cc',
+        harfbuzz .. '/src/hb-outline.cc',
+    })
+
+    warnings('Off')
+
+    defines({
+        'HB_ONLY_ONE_SHAPER', -- added this for Geotech Mac multi-module issue: https://github.com/rive-app/rive-cpp/issues/369
+        'HAVE_OT',
+        'HB_NO_FALLBACK_SHAPE',
+        'HB_NO_WIN1256',
+        'HB_NO_EXTERN_HELPERS',
+        'HB_DISABLE_DEPRECATED',
+        'HB_NO_COLOR',
+        'HB_NO_BITMAP',
+        'HB_NO_BUFFER_SERIALIZE',
+        'HB_NO_BUFFER_VERIFY',
+        'HB_NO_BUFFER_MESSAGE',
+        'HB_NO_SETLOCALE',
+        'HB_NO_STYLE',
+        'HB_NO_VERTICAL',
+        'HB_NO_LAYOUT_COLLECT_GLYPHS',
+        'HB_NO_LAYOUT_RARELY_USED',
+        'HB_NO_LAYOUT_UNUSED',
+        'HB_NO_OT_FONT_GLYPH_NAMES',
+        'HB_NO_PAINT',
+        'HB_NO_MMAP',
+        'HB_NO_META',
+    })
+
+    filter('toolset:not msc')
+    do
+        flags({ 'FatalWarnings' })
+        buildoptions({
+            '-Werror=format',
+            '-Wimplicit-int-conversion',
+            '-Werror=vla',
+        })
+    end
+    filter('toolset:msc')
+    do
+        buildoptions({
+            '/bigobj',
+        })
+    end
+
+    filter('options:config=release')
+    do
+        optimize('Size')
+    end
+
+    filter({ 'options:not no-harfbuzz-renames' })
+    do
+        includedirs({ './' })
+        forceincludes({ 'rive_harfbuzz_renames.h' })
+    end
+
+    filter('system:macosx or system:ios')
+    do
+        defines({ 'HAVE_CORETEXT' })
+        files({ harfbuzz .. '/src/hb-coretext.cc' })
+    end
+end
diff --git a/dependencies/premake5_libjpeg_v2.lua b/dependencies/premake5_libjpeg_v2.lua
new file mode 100644
index 0000000..3ed2ecc
--- /dev/null
+++ b/dependencies/premake5_libjpeg_v2.lua
@@ -0,0 +1,72 @@
+dofile('rive_build_config.lua')
+
+local dependency = require('dependency')
+libjpeg = dependency.github('rive-app/libjpeg', 'v9f')
+
+newoption({
+    trigger = 'no-libjpeg-renames',
+    description = 'don\'t rename libjpeg symbols',
+})
+
+project('libjpeg')
+do
+    kind('StaticLib')
+    optimize('Speed') -- Always optimize image encoding/decoding, even in debug builds.
+
+    includedirs({ libjpeg })
+
+    files({
+        libjpeg .. '/jaricom.c',
+        libjpeg .. '/jcapimin.c',
+        libjpeg .. '/jcapistd.c',
+        libjpeg .. '/jcarith.c',
+        libjpeg .. '/jccoefct.c',
+        libjpeg .. '/jccolor.c',
+        libjpeg .. '/jcdctmgr.c',
+        libjpeg .. '/jchuff.c',
+        libjpeg .. '/jcinit.c',
+        libjpeg .. '/jcmainct.c',
+        libjpeg .. '/jcmarker.c',
+        libjpeg .. '/jcmaster.c',
+        libjpeg .. '/jcomapi.c',
+        libjpeg .. '/jcparam.c',
+        libjpeg .. '/jcprepct.c',
+        libjpeg .. '/jcsample.c',
+        libjpeg .. '/jctrans.c',
+        libjpeg .. '/jdapimin.c',
+        libjpeg .. '/jdapistd.c',
+        libjpeg .. '/jdarith.c',
+        libjpeg .. '/jdatadst.c',
+        libjpeg .. '/jdatasrc.c',
+        libjpeg .. '/jdcoefct.c',
+        libjpeg .. '/jdcolor.c',
+        libjpeg .. '/jddctmgr.c',
+        libjpeg .. '/jdhuff.c',
+        libjpeg .. '/jdinput.c',
+        libjpeg .. '/jdmainct.c',
+        libjpeg .. '/jdmarker.c',
+        libjpeg .. '/jdmaster.c',
+        libjpeg .. '/jdmerge.c',
+        libjpeg .. '/jdpostct.c',
+        libjpeg .. '/jdsample.c',
+        libjpeg .. '/jdtrans.c',
+        libjpeg .. '/jerror.c',
+        libjpeg .. '/jfdctflt.c',
+        libjpeg .. '/jfdctfst.c',
+        libjpeg .. '/jfdctint.c',
+        libjpeg .. '/jidctflt.c',
+        libjpeg .. '/jidctfst.c',
+        libjpeg .. '/jidctint.c',
+        libjpeg .. '/jquant1.c',
+        libjpeg .. '/jquant2.c',
+        libjpeg .. '/jutils.c',
+        libjpeg .. '/jmemmgr.c',
+        libjpeg .. '/jmemansi.c',
+    })
+
+    filter({ 'options:not no-libjpeg-renames' })
+    do
+        includedirs({ './' })
+        forceincludes({ 'rive_libjpeg_renames.h' })
+    end
+end
diff --git a/dependencies/premake5_libpng.lua b/dependencies/premake5_libpng.lua
new file mode 100644
index 0000000..fdf1c4e
--- /dev/null
+++ b/dependencies/premake5_libpng.lua
@@ -0,0 +1,86 @@
+require('setup_compiler')
+local dependency = require('dependency')
+libpng = dependency.github('glennrp/libpng', 'libpng16')
+zlib = dependency.github('madler/zlib', '04f42ceca40f73e2978b50e93806c2a18c1281fc')
+
+includedirs({ './' })
+forceincludes({ 'rive_png_renames.h' })
+
+project('libpng')
+do
+    kind('StaticLib')
+    language('C++')
+    cppdialect('C++17')
+    targetdir('%{cfg.system}/cache/bin/%{cfg.buildcfg}/')
+    objdir('%{cfg.system}/cache/obj/%{cfg.buildcfg}/')
+    os.copyfile(libpng .. '/scripts/pnglibconf.h.prebuilt', libpng .. '/pnglibconf.h')
+    includedirs({ libpng, zlib })
+    files({
+        libpng .. '/png.c',
+        libpng .. '/pngerror.c',
+        libpng .. '/pngget.c',
+        libpng .. '/pngmem.c',
+        libpng .. '/pngpread.c',
+        libpng .. '/pngread.c',
+        libpng .. '/pngrio.c',
+        libpng .. '/pngrtran.c',
+        libpng .. '/pngrutil.c',
+        libpng .. '/pngset.c',
+        libpng .. '/pngtrans.c',
+        libpng .. '/pngwio.c',
+        libpng .. '/pngwrite.c',
+        libpng .. '/pngwtran.c',
+        libpng .. '/pngwutil.c',
+    })
+
+    do
+        files({
+            libpng .. '/arm/arm_init.c',
+            libpng .. '/arm/filter_neon_intrinsics.c',
+            libpng .. '/arm/palette_neon_intrinsics.c',
+        })
+    end
+
+    filter('system:windows')
+    do
+        architecture('x64')
+    end
+end
+
+project('zlib')
+do
+    kind('StaticLib')
+    language('C++')
+    cppdialect('C++17')
+    targetdir('%{cfg.system}/cache/bin/%{cfg.buildcfg}/')
+    objdir('%{cfg.system}/cache/obj/%{cfg.buildcfg}/')
+    defines({ 'ZLIB_IMPLEMENTATION' })
+    includedirs({ zlib })
+    files({
+        zlib .. '/adler32.c',
+        zlib .. '/compress.c',
+        zlib .. '/crc32.c',
+        zlib .. '/deflate.c',
+        zlib .. '/gzclose.c',
+        zlib .. '/gzlib.c',
+        zlib .. '/gzread.c',
+        zlib .. '/gzwrite.c',
+        zlib .. '/infback.c',
+        zlib .. '/inffast.c',
+        zlib .. '/inftrees.c',
+        zlib .. '/trees.c',
+        zlib .. '/uncompr.c',
+        zlib .. '/zutil.c',
+        zlib .. '/inflate.c',
+    })
+
+    filter('system:windows')
+    do
+        architecture('x64')
+    end
+
+    filter('system:not windows')
+    do
+        defines({ 'HAVE_UNISTD_H' })
+    end
+end
diff --git a/dependencies/premake5_libpng_v2.lua b/dependencies/premake5_libpng_v2.lua
new file mode 100644
index 0000000..75e0c8c
--- /dev/null
+++ b/dependencies/premake5_libpng_v2.lua
@@ -0,0 +1,81 @@
+dofile('rive_build_config.lua')
+
+local dependency = require('dependency')
+libpng = dependency.github('glennrp/libpng', 'libpng16')
+zlib = dependency.github('madler/zlib', '04f42ceca40f73e2978b50e93806c2a18c1281fc')
+
+includedirs({ './' })
+forceincludes({ 'rive_png_renames.h' })
+
+project('libpng')
+do
+    kind('StaticLib')
+    os.copyfile(libpng .. '/scripts/pnglibconf.h.prebuilt', libpng .. '/pnglibconf.h')
+    includedirs({ libpng, zlib })
+    optimize('Speed') -- Always optimize image encoding/decoding, even in debug builds.
+    files({
+        libpng .. '/png.c',
+        libpng .. '/pngerror.c',
+        libpng .. '/pngget.c',
+        libpng .. '/pngmem.c',
+        libpng .. '/pngpread.c',
+        libpng .. '/pngread.c',
+        libpng .. '/pngrio.c',
+        libpng .. '/pngrtran.c',
+        libpng .. '/pngrutil.c',
+        libpng .. '/pngset.c',
+        libpng .. '/pngtrans.c',
+        libpng .. '/pngwio.c',
+        libpng .. '/pngwrite.c',
+        libpng .. '/pngwtran.c',
+        libpng .. '/pngwutil.c',
+    })
+
+    do
+        files({
+            libpng .. '/arm/arm_init.c',
+            libpng .. '/arm/filter_neon_intrinsics.c',
+            libpng .. '/arm/palette_neon_intrinsics.c',
+        })
+    end
+end
+
+project('zlib')
+do
+    kind('StaticLib')
+    defines({ 'ZLIB_IMPLEMENTATION' })
+    includedirs({ zlib })
+    optimize('Speed') -- Always optimize image encoding/decoding, even in debug builds.
+    files({
+        zlib .. '/adler32.c',
+        zlib .. '/compress.c',
+        zlib .. '/crc32.c',
+        zlib .. '/deflate.c',
+        zlib .. '/gzclose.c',
+        zlib .. '/gzlib.c',
+        zlib .. '/gzread.c',
+        zlib .. '/gzwrite.c',
+        zlib .. '/infback.c',
+        zlib .. '/inffast.c',
+        zlib .. '/inftrees.c',
+        zlib .. '/trees.c',
+        zlib .. '/uncompr.c',
+        zlib .. '/zutil.c',
+        zlib .. '/inflate.c',
+    })
+
+    filter('toolset:not msc')
+    do
+        flags({ 'FatalWarnings' })
+        buildoptions({
+            '-Wno-unknown-warning-option',
+            '-Wno-deprecated-non-prototype',
+            '-Wno-shorten-64-to-32',
+        })
+    end
+
+    filter('system:not windows')
+    do
+        defines({ 'HAVE_UNISTD_H' })
+    end
+end
diff --git a/dependencies/premake5_libwebp_v2.lua b/dependencies/premake5_libwebp_v2.lua
new file mode 100644
index 0000000..00eb5f1
--- /dev/null
+++ b/dependencies/premake5_libwebp_v2.lua
@@ -0,0 +1,166 @@
+dofile('rive_build_config.lua')
+
+local dependency = require('dependency')
+libwebp = dependency.github('webmproject/libwebp', 'v1.4.0')
+
+project('libwebp')
+do
+    kind('StaticLib')
+    optimize('Speed') -- Always optimize image encoding/decoding, even in debug builds.
+
+    includedirs({ libwebp })
+
+    -- Leaving some notes here for future perf improvements. Define these when
+    -- we can determine we're on a compatible platform/perf gain is worth it.
+    --
+    -- Some extra details about each of these:
+    -- https://github.com/webmproject/libwebp/blob/main/cmake/config.h.in
+    defines({
+        -- 'WEBP_USE_NEON=1',
+        -- 'WEBP_HAVE_NEON_RTCD=1', -- runtime detection of NEON extensions
+        -- 'WEBP_HAVE_SSE41=1',
+        -- 'WEBP_USE_THREAD=1'
+    })
+
+    files({
+        -- common dsp
+        libwebp .. '/src/dsp/alpha_processing.c',
+        libwebp .. '/src/dsp/cpu.c',
+        libwebp .. '/src/dsp/dec.c',
+        libwebp .. '/src/dsp/dec_clip_tables.c',
+        libwebp .. '/src/dsp/filters.c',
+        libwebp .. '/src/dsp/lossless.c',
+        libwebp .. '/src/dsp/rescaler.c',
+        libwebp .. '/src/dsp/upsampling.c',
+        libwebp .. '/src/dsp/yuv.c',
+
+        -- encoder dsp
+        libwebp .. '/src/dsp/cost.c',
+        libwebp .. '/src/dsp/enc.c',
+        libwebp .. '/src/dsp/lossless_enc.c',
+        libwebp .. '/src/dsp/ssim.c',
+
+        -- decoder
+        libwebp .. '/src/dec/alpha_dec.c',
+        libwebp .. '/src/dec/buffer_dec.c',
+        libwebp .. '/src/dec/frame_dec.c',
+        libwebp .. '/src/dec/idec_dec.c',
+        libwebp .. '/src/dec/io_dec.c',
+        libwebp .. '/src/dec/quant_dec.c',
+        libwebp .. '/src/dec/tree_dec.c',
+        libwebp .. '/src/dec/vp8_dec.c',
+        libwebp .. '/src/dec/vp8l_dec.c',
+        libwebp .. '/src/dec/webp_dec.c',
+
+        -- libwebpdspdecode_sse41_la_SOURCES =
+        libwebp .. '/src/dsp/alpha_processing_sse41.c',
+        libwebp .. '/src/dsp/dec_sse41.c',
+        libwebp .. '/src/dsp/lossless_sse41.c',
+        libwebp .. '/src/dsp/upsampling_sse41.c',
+        libwebp .. '/src/dsp/yuv_sse41.c',
+
+        -- libwebpdspdecode_sse2_la_SOURCES =
+        libwebp .. '/src/dsp/alpha_processing_sse2.c',
+        libwebp .. '/src/dsp/common_sse2.h',
+        libwebp .. '/src/dsp/dec_sse2.c',
+        libwebp .. '/src/dsp/filters_sse2.c',
+        libwebp .. '/src/dsp/lossless_sse2.c',
+        libwebp .. '/src/dsp/rescaler_sse2.c',
+        libwebp .. '/src/dsp/upsampling_sse2.c',
+        libwebp .. '/src/dsp/yuv_sse2.c',
+
+        -- neon sources
+        -- TODO: define WEBP_HAVE_NEON when we're on a platform that supports it.
+        libwebp .. '/src/dsp/alpha_processing_neon.c',
+        libwebp .. '/src/dsp/dec_neon.c',
+        libwebp .. '/src/dsp/filters_neon.c',
+        libwebp .. '/src/dsp/lossless_neon.c',
+        libwebp .. '/src/dsp/neon.h',
+        libwebp .. '/src/dsp/rescaler_neon.c',
+        libwebp .. '/src/dsp/upsampling_neon.c',
+        libwebp .. '/src/dsp/yuv_neon.c',
+
+        -- libwebpdspdecode_msa_la_SOURCES =
+        libwebp .. '/src/dsp/dec_msa.c',
+        libwebp .. '/src/dsp/filters_msa.c',
+        libwebp .. '/src/dsp/lossless_msa.c',
+        libwebp .. '/src/dsp/msa_macro.h',
+        libwebp .. '/src/dsp/rescaler_msa.c',
+        libwebp .. '/src/dsp/upsampling_msa.c',
+
+        -- libwebpdspdecode_mips32_la_SOURCES =
+        libwebp .. '/src/dsp/dec_mips32.c',
+        libwebp .. '/src/dsp/mips_macro.h',
+        libwebp .. '/src/dsp/rescaler_mips32.c',
+        libwebp .. '/src/dsp/yuv_mips32.c',
+
+        -- libwebpdspdecode_mips_dsp_r2_la_SOURCES =
+        libwebp .. '/src/dsp/alpha_processing_mips_dsp_r2.c',
+        libwebp .. '/src/dsp/dec_mips_dsp_r2.c',
+        libwebp .. '/src/dsp/filters_mips_dsp_r2.c',
+        libwebp .. '/src/dsp/lossless_mips_dsp_r2.c',
+        libwebp .. '/src/dsp/mips_macro.h',
+        libwebp .. '/src/dsp/rescaler_mips_dsp_r2.c',
+        libwebp .. '/src/dsp/upsampling_mips_dsp_r2.c',
+        libwebp .. '/src/dsp/yuv_mips_dsp_r2.c',
+
+        -- libwebpdsp_sse2_la_SOURCES =
+        libwebp .. '/src/dsp/cost_sse2.c',
+        libwebp .. '/src/dsp/enc_sse2.c',
+        libwebp .. '/src/dsp/lossless_enc_sse2.c',
+        libwebp .. '/src/dsp/ssim_sse2.c',
+
+        -- libwebpdsp_sse41_la_SOURCES =
+        libwebp .. '/src/dsp/enc_sse41.c',
+        libwebp .. '/src/dsp/lossless_enc_sse41.c',
+
+        -- libwebpdsp_neon_la_SOURCES =
+        libwebp .. '/src/dsp/cost_neon.c',
+        libwebp .. '/src/dsp/enc_neon.c',
+        libwebp .. '/src/dsp/lossless_enc_neon.c',
+
+        -- libwebpdsp_msa_la_SOURCES =
+        libwebp .. '/src/dsp/enc_msa.c',
+        libwebp .. '/src/dsp/lossless_enc_msa.c',
+
+        -- libwebpdsp_mips32_la_SOURCES =
+        libwebp .. '/src/dsp/cost_mips32.c',
+        libwebp .. '/src/dsp/enc_mips32.c',
+        libwebp .. '/src/dsp/lossless_enc_mips32.c',
+
+        -- libwebpdsp_mips_dsp_r2_la_SOURCES =
+        libwebp .. '/src/dsp/cost_mips_dsp_r2.c',
+        libwebp .. '/src/dsp/enc_mips_dsp_r2.c',
+        libwebp .. '/src/dsp/lossless_enc_mips_dsp_r2.c',
+
+        -- COMMON_SOURCES =
+        libwebp .. '/src/utils/bit_reader_utils.c',
+        libwebp .. '/src/utils/bit_reader_utils.h',
+        libwebp .. '/src/utils/color_cache_utils.c',
+        libwebp .. '/src/utils/filters_utils.c',
+        libwebp .. '/src/utils/huffman_utils.c',
+        libwebp .. '/src/utils/palette.c',
+        libwebp .. '/src/utils/quant_levels_dec_utils.c',
+        libwebp .. '/src/utils/rescaler_utils.c',
+        libwebp .. '/src/utils/random_utils.c',
+        libwebp .. '/src/utils/thread_utils.c',
+        libwebp .. '/src/utils/utils.c',
+
+        -- ENC_SOURCES =
+        libwebp .. '/src/utils/bit_writer_utils.c',
+        libwebp .. '/src/utils/huffman_encode_utils.c',
+        libwebp .. '/src/utils/quant_levels_utils.c',
+
+        -- libwebpdemux_la_SOURCES =
+        libwebp .. '/src/demux/anim_decode.c',
+        libwebp .. '/src/demux/demux.c',
+    })
+
+    filter({ 'system:windows', 'toolset:clang' })
+    do
+        -- https://github.com/webmproject/libwebp/blob/233e86b91f4e0af7833d50013e3b978f825f73f5/src/dsp/cpu.h#L57
+        -- webp automaticall enables these for windows so we need to compile
+        -- with the correct settings or we get an error.
+        buildoptions({ '-mssse3', '-msse4.1' })
+    end
+end
diff --git a/dependencies/premake5_miniaudio.lua b/dependencies/premake5_miniaudio.lua
new file mode 100644
index 0000000..6779bba
--- /dev/null
+++ b/dependencies/premake5_miniaudio.lua
@@ -0,0 +1,2 @@
+local dependency = require('dependency')
+miniaudio = dependency.github('rive-app/miniaudio', 'rive_changes_4')
diff --git a/dependencies/premake5_miniaudio_v2.lua b/dependencies/premake5_miniaudio_v2.lua
new file mode 100644
index 0000000..c2098b4
--- /dev/null
+++ b/dependencies/premake5_miniaudio_v2.lua
@@ -0,0 +1,3 @@
+dofile('rive_build_config.lua')
+local dependency = require('dependency')
+miniaudio = dependency.github('rive-app/miniaudio', 'rive_changes_4')
diff --git a/dependencies/premake5_sheenbidi.lua b/dependencies/premake5_sheenbidi.lua
new file mode 100644
index 0000000..61ccb57
--- /dev/null
+++ b/dependencies/premake5_sheenbidi.lua
@@ -0,0 +1,140 @@
+local dependency = require('dependency')
+sheenbidi = dependency.github('Tehreer/SheenBidi', 'v2.6')
+
+workspace('rive')
+configurations({ 'debug', 'release' })
+
+project('rive_sheenbidi')
+do
+    kind('StaticLib')
+    language('C')
+    targetdir('%{cfg.system}/cache/bin/%{cfg.buildcfg}/')
+    objdir('%{cfg.system}/cache/obj/%{cfg.buildcfg}/')
+    warnings('Off')
+
+    includedirs({ sheenbidi .. '/Headers' })
+
+    buildoptions({ '-Wall', '-ansi', '-pedantic' })
+
+    linkoptions({ '-r' })
+
+    filter('system:emscripten')
+    do
+        buildoptions({ '-pthread' })
+    end
+
+    filter('configurations:debug')
+    do
+        files({
+            sheenbidi .. '/Source/BidiChain.c',
+            sheenbidi .. '/Source/BidiTypeLookup.c',
+            sheenbidi .. '/Source/BracketQueue.c',
+            sheenbidi .. '/Source/GeneralCategoryLookup.c',
+            sheenbidi .. '/Source/IsolatingRun.c',
+            sheenbidi .. '/Source/LevelRun.c',
+            sheenbidi .. '/Source/PairingLookup.c',
+            sheenbidi .. '/Source/RunQueue.c',
+            sheenbidi .. '/Source/SBAlgorithm.c',
+            sheenbidi .. '/Source/SBBase.c',
+            sheenbidi .. '/Source/SBCodepointSequence.c',
+            sheenbidi .. '/Source/SBLine.c',
+            sheenbidi .. '/Source/SBLog.c',
+            sheenbidi .. '/Source/SBMirrorLocator.c',
+            sheenbidi .. '/Source/SBParagraph.c',
+            sheenbidi .. '/Source/SBScriptLocator.c',
+            sheenbidi .. '/Source/ScriptLookup.c',
+            sheenbidi .. '/Source/ScriptStack.c',
+            sheenbidi .. '/Source/StatusStack.c',
+        })
+    end
+    filter('configurations:release')
+    do
+        files({ sheenbidi .. '/Source/SheenBidi.c' })
+    end
+
+    filter('configurations:debug')
+    do
+        defines({ 'DEBUG' })
+        symbols('On')
+    end
+
+    filter('configurations:release')
+    do
+        buildoptions({ '-Oz' })
+        defines({ 'RELEASE', 'NDEBUG', 'SB_CONFIG_UNITY' })
+        optimize('On')
+    end
+
+    filter({ 'system:macosx', 'options:variant=runtime' })
+    do
+        buildoptions({
+            '-Wimplicit-float-conversion -fembed-bitcode -arch arm64 -arch x86_64 -isysroot '
+                .. (os.getenv('MACOS_SYSROOT') or ''),
+        })
+    end
+
+    filter({ 'system:macosx', 'configurations:release' })
+    do
+        buildoptions({ '-flto=full' })
+    end
+
+    filter({ 'system:ios' })
+    do
+        buildoptions({ '-flto=full' })
+    end
+
+    filter('system:windows')
+    do
+        architecture('x64')
+        defines({ '_USE_MATH_DEFINES' })
+    end
+
+    filter({ 'system:ios', 'options:variant=system' })
+    do
+        buildoptions({
+            '-mios-version-min=13.0 -fembed-bitcode -arch arm64 -isysroot '
+                .. (os.getenv('IOS_SYSROOT') or ''),
+        })
+    end
+
+    filter({ 'system:ios', 'options:variant=emulator' })
+    do
+        buildoptions({
+            '--target=arm64-apple-ios13.0.0-simulator',
+            '-mios-version-min=13.0 -arch arm64 -arch x86_64 -isysroot '
+                .. (os.getenv('IOS_SYSROOT') or ''),
+        })
+        targetdir('%{cfg.system}_sim/cache/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}_sim/cache/obj/%{cfg.buildcfg}')
+    end
+
+    filter({ 'system:android', 'configurations:release' })
+    do
+        buildoptions({ '-flto=full' })
+    end
+
+    -- Is there a way to pass 'arch' as a variable here?
+    filter({ 'system:android', 'options:arch=x86' })
+    do
+        targetdir('%{cfg.system}/cache/x86/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}/cache/x86/obj/%{cfg.buildcfg}')
+    end
+
+    filter({ 'system:android', 'options:arch=x64' })
+    do
+        targetdir('%{cfg.system}/cache/x64/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}/cache/x64/obj/%{cfg.buildcfg}')
+    end
+
+    filter({ 'system:android', 'options:arch=arm' })
+    do
+        targetdir('%{cfg.system}/cache/arm/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}/cache/arm/obj/%{cfg.buildcfg}')
+    end
+
+    filter({ 'system:android', 'options:arch=arm64' })
+    do
+        targetdir('%{cfg.system}/cache/arm64/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}/cache/arm64/obj/%{cfg.buildcfg}')
+    end
+end
diff --git a/dependencies/premake5_sheenbidi_v2.lua b/dependencies/premake5_sheenbidi_v2.lua
new file mode 100644
index 0000000..147f983
--- /dev/null
+++ b/dependencies/premake5_sheenbidi_v2.lua
@@ -0,0 +1,69 @@
+dofile('rive_build_config.lua')
+
+local dependency = require('dependency')
+sheenbidi = dependency.github('Tehreer/SheenBidi', 'v2.6')
+Headers = sheenbidi .. '/Headers'
+
+project('rive_sheenbidi')
+do
+    kind('StaticLib')
+    language('C')
+    warnings('Off')
+
+    includedirs({ Headers })
+
+    filter('action:xcode4')
+    do
+        -- xcode doesnt like angle brackets except for -isystem
+        -- should use externalincludedirs but GitHub runners dont have latest premake5 binaries
+        buildoptions({ '-isystem' .. Headers })
+    end
+    filter({})
+
+    buildoptions({ '-Wall', '-ansi', '-pedantic' })
+
+    linkoptions({ '-r' })
+
+    filter('options:config=debug')
+    do
+        files({
+            sheenbidi .. '/Source/BidiChain.c',
+            sheenbidi .. '/Source/BidiTypeLookup.c',
+            sheenbidi .. '/Source/BracketQueue.c',
+            sheenbidi .. '/Source/GeneralCategoryLookup.c',
+            sheenbidi .. '/Source/IsolatingRun.c',
+            sheenbidi .. '/Source/LevelRun.c',
+            sheenbidi .. '/Source/PairingLookup.c',
+            sheenbidi .. '/Source/RunQueue.c',
+            sheenbidi .. '/Source/SBAlgorithm.c',
+            sheenbidi .. '/Source/SBBase.c',
+            sheenbidi .. '/Source/SBCodepointSequence.c',
+            sheenbidi .. '/Source/SBLine.c',
+            sheenbidi .. '/Source/SBLog.c',
+            sheenbidi .. '/Source/SBMirrorLocator.c',
+            sheenbidi .. '/Source/SBParagraph.c',
+            sheenbidi .. '/Source/SBScriptLocator.c',
+            sheenbidi .. '/Source/ScriptLookup.c',
+            sheenbidi .. '/Source/ScriptStack.c',
+            sheenbidi .. '/Source/StatusStack.c',
+        })
+    end
+    filter('options:config=release')
+    do
+        files({ sheenbidi .. '/Source/SheenBidi.c' })
+    end
+
+    filter('options:config=release')
+    do
+        defines({ 'SB_CONFIG_UNITY' })
+        optimize('Size')
+    end
+
+    filter({ 'system:linux' })
+    do
+        buildoptions({
+            '-Wno-unused-function',
+            '-Wno-unused-variable',
+        })
+    end
+end
diff --git a/dependencies/premake5_yoga.lua b/dependencies/premake5_yoga.lua
new file mode 100644
index 0000000..bf83e1b
--- /dev/null
+++ b/dependencies/premake5_yoga.lua
@@ -0,0 +1,138 @@
+local dependency = require('dependency')
+yoga = dependency.github('rive-app/yoga', 'rive_changes_v2_0_1')
+
+workspace('rive')
+configurations({ 'debug', 'release' })
+
+project('rive_yoga')
+do
+    kind('StaticLib')
+    language('C++')
+    cppdialect('C++11')
+    targetdir('%{cfg.system}/cache/bin/%{cfg.buildcfg}/')
+    objdir('%{cfg.system}/cache/obj/%{cfg.buildcfg}/')
+    warnings('Off')
+
+    defines({ 'YOGA_EXPORT=' })
+
+    includedirs({ yoga })
+
+    files({
+        yoga .. '/yoga/Utils.cpp',
+        yoga .. '/yoga/YGConfig.cpp',
+        yoga .. '/yoga/YGLayout.cpp',
+        yoga .. '/yoga/YGEnums.cpp',
+        yoga .. '/yoga/YGNodePrint.cpp',
+        yoga .. '/yoga/YGNode.cpp',
+        yoga .. '/yoga/YGValue.cpp',
+        yoga .. '/yoga/YGStyle.cpp',
+        yoga .. '/yoga/Yoga.cpp',
+        yoga .. '/yoga/event/event.cpp',
+        yoga .. '/yoga/log.cpp',
+    })
+
+    buildoptions({ '-Wall', '-pedantic' })
+
+    linkoptions({ '-r' })
+
+    filter('system:emscripten')
+    do
+        buildoptions({ '-pthread' })
+    end
+
+    filter('configurations:debug')
+    do
+        defines({ 'DEBUG' })
+        symbols('On')
+    end
+
+    filter('toolset:clang')
+    do
+        flags({ 'FatalWarnings' })
+        buildoptions({
+            '-Werror=format',
+            '-Wimplicit-int-conversion',
+            '-Werror=vla',
+        })
+    end
+
+    filter('configurations:release')
+    do
+        buildoptions({ '-Oz' })
+        defines({ 'RELEASE', 'NDEBUG' })
+        optimize('On')
+    end
+
+    filter({ 'system:macosx', 'options:variant=runtime' })
+    do
+        buildoptions({
+            '-Wimplicit-float-conversion -fembed-bitcode -arch arm64 -arch x86_64 -isysroot'
+                .. (os.getenv('MACOS_SYSROOT') or ''),
+        })
+    end
+
+    filter({ 'system:macosx', 'configurations:release' })
+    do
+        buildoptions({ '-flto=full' })
+    end
+
+    filter({ 'system:ios' })
+    do
+        buildoptions({ '-flto=full' })
+    end
+
+    filter('system:windows')
+    do
+        architecture('x64')
+        defines({ '_USE_MATH_DEFINES' })
+    end
+
+    filter({ 'system:ios', 'options:variant=system' })
+    do
+        buildoptions({
+            '-mios-version-min=13.0 -fembed-bitcode -arch arm64 -isysroot '
+                .. (os.getenv('IOS_SYSROOT') or ''),
+        })
+    end
+
+    filter({ 'system:ios', 'options:variant=emulator' })
+    do
+        buildoptions({
+            '--target=arm64-apple-ios13.0.0-simulator',
+            '-mios-version-min=13.0 -arch arm64 -arch x86_64 -isysroot '
+                .. (os.getenv('IOS_SYSROOT') or ''),
+        })
+        targetdir('%{cfg.system}_sim/cache/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}_sim/cache/obj/%{cfg.buildcfg}')
+    end
+
+    filter({ 'system:android', 'configurations:release' })
+    do
+        buildoptions({ '-flto=full' })
+    end
+
+    -- Is there a way to pass 'arch' as a variable here?
+    filter({ 'system:android', 'options:arch=x86' })
+    do
+        targetdir('%{cfg.system}/cache/x86/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}/cache/x86/obj/%{cfg.buildcfg}')
+    end
+
+    filter({ 'system:android', 'options:arch=x64' })
+    do
+        targetdir('%{cfg.system}/cache/x64/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}/cache/x64/obj/%{cfg.buildcfg}')
+    end
+
+    filter({ 'system:android', 'options:arch=arm' })
+    do
+        targetdir('%{cfg.system}/cache/arm/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}/cache/arm/obj/%{cfg.buildcfg}')
+    end
+
+    filter({ 'system:android', 'options:arch=arm64' })
+    do
+        targetdir('%{cfg.system}/cache/arm64/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}/cache/arm64/obj/%{cfg.buildcfg}')
+    end
+end
diff --git a/dependencies/premake5_yoga_v2.lua b/dependencies/premake5_yoga_v2.lua
new file mode 100644
index 0000000..68de64a
--- /dev/null
+++ b/dependencies/premake5_yoga_v2.lua
@@ -0,0 +1,47 @@
+dofile('rive_build_config.lua')
+
+local dependency = require('dependency')
+yoga = dependency.github('rive-app/yoga', 'rive_changes_v2_0_1')
+
+newoption({
+    trigger = 'no-yoga-renames',
+    description = 'don\'t rename yoga symbols',
+})
+
+project('rive_yoga')
+do
+    kind('StaticLib')
+    warnings('Off')
+
+    defines({ 'YOGA_EXPORT=' })
+
+    includedirs({ yoga })
+
+    filter('action:xcode4')
+    do
+        -- xcode doesnt like angle brackets except for -isystem
+        -- should use externalincludedirs but GitHub runners dont have latest premake5 binaries
+        buildoptions({ '-isystem' .. yoga })
+    end
+    filter({})
+
+    files({
+        yoga .. '/yoga/Utils.cpp',
+        yoga .. '/yoga/YGConfig.cpp',
+        yoga .. '/yoga/YGLayout.cpp',
+        yoga .. '/yoga/YGEnums.cpp',
+        yoga .. '/yoga/YGNodePrint.cpp',
+        yoga .. '/yoga/YGNode.cpp',
+        yoga .. '/yoga/YGValue.cpp',
+        yoga .. '/yoga/YGStyle.cpp',
+        yoga .. '/yoga/Yoga.cpp',
+        yoga .. '/yoga/event/event.cpp',
+        yoga .. '/yoga/log.cpp',
+    })
+
+    filter({ 'options:not no-yoga-renames' })
+    do
+        includedirs({ './' })
+        forceincludes({ 'rive_yoga_renames.h' })
+    end
+end
diff --git a/dependencies/rive_harfbuzz_renames.h b/dependencies/rive_harfbuzz_renames.h
new file mode 100644
index 0000000..bb8ea90
--- /dev/null
+++ b/dependencies/rive_harfbuzz_renames.h
@@ -0,0 +1,1099 @@
+// clang-format off
+// hb_*
+#define hb_vector_t rive_hb_vector_t
+#define hb_hashmap_t rive_hb_hashmap_t
+#define hb_serialize_context_t rive_hb_serialize_context_t
+#define hb_array_t rive_hb_array_t
+#define hb_array rive_hb_array
+#define hb_barrier rive_hb_barrier
+#define hb_identity rive_hb_identity
+#define hb_ridentity rive_hb_ridentity
+#define hb_bit_storage rive_hb_bit_storage
+#define hb_object_fini rive_hb_object_fini
+#define hb_object_init rive_hb_object_init
+#define hb_unsigned_mul_overflows rive_hb_unsigned_mul_overflows
+#define hb_user_data_array_t rive_hb_user_data_array_t
+#define hb_get rive_hb_get
+#define hb_has rive_hb_has
+#define hb_map rive_hb_map
+#define hb_max rive_hb_max
+#define hb_hash rive_hb_hash
+#define hb_iter rive_hb_iter
+#define hb_swap rive_hb_swap
+#define hb_deref rive_hb_deref
+#define hb_concat rive_hb_concat
+#define hb_filter rive_hb_filter
+#define hb_invoke rive_hb_invoke
+#define hb_memset rive_hb_memset
+#define hb_mutex_t rive_hb_mutex_t
+#define hb_priority rive_hb_priority
+#define hb_map_iter_t rive_hb_map_iter_t
+#define hb_atomic_int_t rive_hb_atomic_int_t
+#define hb_atomic_ptr_t rive_hb_atomic_ptr_t
+#define hb_concat_iter_t rive_hb_concat_iter_t
+#define hb_filter_iter_t rive_hb_filter_iter_t
+#define hb_lockable_set_t rive_hb_lockable_set_t
+#define hb_object_header_t rive_hb_object_header_t
+#define hb_reference_count_t rive_hb_reference_count_t
+#define hb_reference_wrapper rive_hb_reference_wrapper
+#define hb_map_iter_factory_t rive_hb_map_iter_factory_t
+#define hb_filter_iter_factory_t rive_hb_filter_iter_factory_t
+#define hb_iter_t rive_hb_iter_t
+#define hb_iter_fallback_mixin_t rive_hb_iter_fallback_mixin_t
+#define hb_match_reference rive_hb_match_reference
+#define hb_sorted_array_t rive_hb_sorted_array_t
+#define hb_sorted_array rive_hb_sorted_array
+#define hb_aat_layout_track rive_hb_aat_layout_track
+#define hb_font_t rive_hb_font_t
+#define hb_buffer_t rive_hb_buffer_t
+#define hb_aat_layout_position rive_hb_aat_layout_position
+#define hb_aat_layout_substitute rive_hb_aat_layout_substitute
+#define hb_feature_t rive_hb_feature_t
+#define hb_aat_layout_compile_map rive_hb_aat_layout_compile_map
+#define hb_aat_map_t rive_hb_aat_map_t
+#define hb_aat_layout_find_feature_mapping rive_hb_aat_layout_find_feature_mapping
+#define hb_aat_layout_remove_deleted_glyphs rive_hb_aat_layout_remove_deleted_glyphs
+#define hb_aat_layout_zero_width_deleted_glyphs rive_hb_aat_layout_zero_width_deleted_glyphs
+#define hb_aat_map_builder_t rive_hb_aat_map_builder_t
+#define hb_aat_feature_mapping_t rive_hb_aat_feature_mapping_t
+#define hb_bsearch_impl rive_hb_bsearch_impl
+#define hb_aat_layout_feature_type_t rive_hb_aat_layout_feature_type_t
+#define hb_glyph_info_t rive_hb_glyph_info_t
+#define hb_set_digest_combiner_t rive_hb_set_digest_combiner_t
+#define hb_set_digest_bits_pattern_t rive_hb_set_digest_bits_pattern_t
+#define hb_aat_layout_feature_selector_info_t rive_hb_aat_layout_feature_selector_info_t
+#define hb_min rive_hb_min
+#define hb_none rive_hb_none
+#define hb_sink rive_hb_sink
+#define hb_clamp rive_hb_clamp
+#define hb_match rive_hb_match
+#define hb_memcpy rive_hb_memcpy
+#define hb_cache_t rive_hb_cache_t
+#define hb_blob_ptr_t rive_hb_blob_ptr_t
+#define hb_no_trace_t rive_hb_no_trace_t
+#define hb_lazy_loader_t rive_hb_lazy_loader_t
+#define hb_face_lazy_loader_t rive_hb_face_lazy_loader_t
+#define hb_face_t rive_hb_face_t
+#define hb_table_lazy_loader_t rive_hb_table_lazy_loader_t
+#define hb_blob_t rive_hb_blob_t
+#define glyf_accelerator_t rive_glyf_accelerator_t
+#define OT rive_OT
+#define hb_nonnull_ptr_t rive_hb_nonnull_ptr_t
+#define hb_atomic_short_t rive_hb_atomic_short_t
+#define hb_segment_properties_t rive_hb_segment_properties_t
+#define hb_dispatch_context_t rive_hb_dispatch_context_t
+#define hb_sanitize_context_t rive_hb_sanitize_context_t
+#define hb_sanitize_with_object_t rive_hb_sanitize_with_object_t
+#define hb_direction_t rive_hb_direction_t
+#define hb_sink_t rive_hb_sink_t
+#define hb_data_wrapper_t rive_hb_data_wrapper_t
+#define hb_not_found_t rive_hb_not_found_t
+#define hb_glyph_position_t rive_hb_glyph_position_t
+#define hb_buffer_flags_t rive_hb_buffer_flags_t
+#define hb_buffer_scratch_flags_t rive_hb_buffer_scratch_flags_t
+#define hb_glyph_flags_t rive_hb_glyph_flags_t
+#define hb_unicode_props_flags_t rive_hb_unicode_props_flags_t
+#define hb_equal rive_hb_equal
+#define hb_qsort rive_hb_qsort
+#define hb_object_trace rive_hb_object_trace
+#define hb_object_create rive_hb_object_create
+#define hb_object_destroy rive_hb_object_destroy
+#define hb_object_is_valid rive_hb_object_is_valid
+#define hb_object_reference rive_hb_object_reference
+#define hb_object_is_immutable rive_hb_object_is_immutable
+#define hb_object_get_user_data rive_hb_object_get_user_data
+#define hb_user_data_key_t rive_hb_user_data_key_t
+#define hb_object_set_user_data rive_hb_object_set_user_data
+#define hb_object_make_immutable rive_hb_object_make_immutable
+#define hb_parse_int rive_hb_parse_int
+#define hb_parse_uint rive_hb_parse_uint
+#define hb_buffer_serialize_format_t rive_hb_buffer_serialize_format_t
+#define hb_buffer_serialize_flags_t rive_hb_buffer_serialize_flags_t
+#define hb_in_range rive_hb_in_range
+#define hb_stable_sort rive_hb_stable_sort
+#define hb_buffer_add_utf rive_hb_buffer_add_utf
+#define hb_latin1_t rive_hb_latin1_t
+#define hb_utf16_xe_t rive_hb_utf16_xe_t
+#define hb_utf32_xe_t rive_hb_utf32_xe_t
+#define hb_utf8_t rive_hb_utf8_t
+#define hb_unicode_funcs_t rive_hb_unicode_funcs_t
+#define hb_buffer_diff_flags_t rive_hb_buffer_diff_flags_t
+#define hb_parse_double rive_hb_parse_double
+#define hb_variation_t rive_hb_variation_t
+#define hb_language_item_t rive_hb_language_item_t
+#define hb_language_impl_t rive_hb_language_impl_t
+#define hb_language_get_default rive_hb_language_get_default
+#define hb_draw_funcs_t rive_hb_draw_funcs_t
+#define hb_draw_line_to_nil rive_hb_draw_line_to_nil
+#define hb_draw_state_t rive_hb_draw_state_t
+#define hb_draw_move_to_nil rive_hb_draw_move_to_nil
+#define hb_draw_cubic_to_nil rive_hb_draw_cubic_to_nil
+#define hb_draw_close_path_nil rive_hb_draw_close_path_nil
+#define hb_draw_quadratic_to_nil rive_hb_draw_quadratic_to_nil
+#define hb_fill rive_hb_fill
+#define hb_bsearch rive_hb_bsearch
+#define hb_bit_set_t rive_hb_bit_set_t
+#define hb_bit_page_t rive_hb_bit_page_t
+#define hb_ot_face_t rive_hb_ot_face_t
+#define hb_sparseset_t rive_hb_sparseset_t
+#define hb_shaper_lazy_loader_t rive_hb_shaper_lazy_loader_t
+#define hb_ot_face_data_t rive_hb_ot_face_data_t
+#define hb_vector_size_t rive_hb_vector_size_t
+#define hb_bit_set_invertible_t rive_hb_bit_set_invertible_t
+#define hb_shaper_object_dataset_t rive_hb_shaper_object_dataset_t
+#define hb_set_t rive_hb_set_t
+#define hb_map_t rive_hb_map_t
+#define hb_font_funcs_t rive_hb_font_funcs_t
+#define hb_trampoline_t rive_hb_trampoline_t
+#define hb_codepoint_parse rive_hb_codepoint_parse
+#define hb_font_draw_glyph_nil rive_hb_font_draw_glyph_nil
+#define hb_draw_line_to_default rive_hb_draw_line_to_default
+#define hb_draw_move_to_default rive_hb_draw_move_to_default
+#define hb_font_paint_glyph_nil rive_hb_font_paint_glyph_nil
+#define hb_paint_funcs_t rive_hb_paint_funcs_t
+#define hb_draw_cubic_to_default rive_hb_draw_cubic_to_default
+#define hb_draw_close_path_default rive_hb_draw_close_path_default
+#define hb_font_draw_glyph_default rive_hb_font_draw_glyph_default
+#define hb_font_get_glyph_name_nil rive_hb_font_get_glyph_name_nil
+#define hb_font_paint_glyph_default rive_hb_font_paint_glyph_default
+#define hb_draw_quadratic_to_default rive_hb_draw_quadratic_to_default
+#define hb_font_get_glyph_extents_nil rive_hb_font_get_glyph_extents_nil
+#define hb_glyph_extents_t rive_hb_glyph_extents_t
+#define hb_font_get_nominal_glyph_nil rive_hb_font_get_nominal_glyph_nil
+#define hb_font_get_font_h_extents_nil rive_hb_font_get_font_h_extents_nil
+#define hb_font_extents_t rive_hb_font_extents_t
+#define hb_font_get_font_v_extents_nil rive_hb_font_get_font_v_extents_nil
+#define hb_font_get_glyph_h_origin_nil rive_hb_font_get_glyph_h_origin_nil
+#define hb_font_get_glyph_name_default rive_hb_font_get_glyph_name_default
+#define hb_font_get_glyph_v_origin_nil rive_hb_font_get_glyph_v_origin_nil
+#define hb_font_get_glyph_from_name_nil rive_hb_font_get_glyph_from_name_nil
+#define hb_font_get_glyph_h_advance_nil rive_hb_font_get_glyph_h_advance_nil
+#define hb_font_get_glyph_h_kerning_nil rive_hb_font_get_glyph_h_kerning_nil
+#define hb_font_get_glyph_v_advance_nil rive_hb_font_get_glyph_v_advance_nil
+#define hb_font_get_glyph_v_kerning_nil rive_hb_font_get_glyph_v_kerning_nil
+#define hb_font_get_variation_glyph_nil rive_hb_font_get_variation_glyph_nil
+#define hb_font_get_glyph_extents_default rive_hb_font_get_glyph_extents_default
+#define hb_font_get_nominal_glyph_default rive_hb_font_get_nominal_glyph_default
+#define hb_font_get_font_h_extents_default rive_hb_font_get_font_h_extents_default
+#define hb_font_get_font_v_extents_default rive_hb_font_get_font_v_extents_default
+#define hb_font_get_glyph_h_origin_default rive_hb_font_get_glyph_h_origin_default
+#define hb_font_get_glyph_v_origin_default rive_hb_font_get_glyph_v_origin_default
+#define hb_font_get_nominal_glyphs_default rive_hb_font_get_nominal_glyphs_default
+#define hb_font_get_glyph_contour_point_nil rive_hb_font_get_glyph_contour_point_nil
+#define hb_font_get_glyph_from_name_default rive_hb_font_get_glyph_from_name_default
+#define hb_font_get_glyph_h_advance_default rive_hb_font_get_glyph_h_advance_default
+#define hb_font_get_glyph_h_kerning_default rive_hb_font_get_glyph_h_kerning_default
+#define hb_font_get_glyph_v_advance_default rive_hb_font_get_glyph_v_advance_default
+#define hb_font_get_glyph_v_kerning_default rive_hb_font_get_glyph_v_kerning_default
+#define hb_font_get_variation_glyph_default rive_hb_font_get_variation_glyph_default
+#define hb_font_get_glyph_h_advances_default rive_hb_font_get_glyph_h_advances_default
+#define hb_font_get_glyph_v_advances_default rive_hb_font_get_glyph_v_advances_default
+#define hb_font_get_nominal_glyph_trampoline rive_hb_font_get_nominal_glyph_trampoline
+#define hb_font_get_variation_glyph_trampoline rive_hb_font_get_variation_glyph_trampoline
+#define hb_font_get_glyph_contour_point_default rive_hb_font_get_glyph_contour_point_default
+#define hb_ot_font_data_t rive_hb_ot_font_data_t
+#define hb_copy rive_hb_copy
+#define hb_reduce rive_hb_reduce
+#define hb_reduce_t rive_hb_reduce_t
+#define hb_pair_t rive_hb_pair_t
+#define hb_draw_session_t rive_hb_draw_session_t
+#define hb_ot_color_layer_t rive_hb_ot_color_layer_t
+#define hb_paint_extents_get_funcs rive_hb_paint_extents_get_funcs
+#define hb_ot_metrics_tag_t rive_hb_ot_metrics_tag_t
+#define hb_outline_recording_pen_get_funcs rive_hb_outline_recording_pen_get_funcs
+#define hb_ot_draw_glyph rive_hb_ot_draw_glyph
+#define hb_ot_paint_glyph rive_hb_ot_paint_glyph
+#define hb_ot_get_glyph_name rive_hb_ot_get_glyph_name
+#define hb_ot_get_glyph_extents rive_hb_ot_get_glyph_extents
+#define hb_ot_get_nominal_glyph rive_hb_ot_get_nominal_glyph
+#define hb_ot_get_font_h_extents rive_hb_ot_get_font_h_extents
+#define hb_ot_get_font_v_extents rive_hb_ot_get_font_v_extents
+#define hb_ot_get_glyph_v_origin rive_hb_ot_get_glyph_v_origin
+#define hb_ot_get_nominal_glyphs rive_hb_ot_get_nominal_glyphs
+#define hb_ot_get_glyph_from_name rive_hb_ot_get_glyph_from_name
+#define hb_ot_get_variation_glyph rive_hb_ot_get_variation_glyph
+#define hb_ot_get_glyph_h_advances rive_hb_ot_get_glyph_h_advances
+#define hb_ot_get_glyph_v_advances rive_hb_ot_get_glyph_v_advances
+#define hb_ot_font_cmap_cache_user_data_key rive_hb_ot_font_cmap_cache_user_data_key
+#define hb_bounds_t rive_hb_bounds_t
+#define hb_transform_t rive_hb_transform_t
+#define hb_memcmp rive_hb_memcmp
+#define hb_extents_t rive_hb_extents_t
+#define hb_outline_t rive_hb_outline_t
+#define hb_empty_t rive_hb_empty_t
+#define hb_ot_font_funcs_lazy_loader_t rive_hb_ot_font_funcs_lazy_loader_t
+#define hb_color_line_t rive_hb_color_line_t
+#define hb_paint_composite_mode_t rive_hb_paint_composite_mode_t
+#define hb_paint_extents_context_t rive_hb_paint_extents_context_t
+#define hb_font_funcs_lazy_loader_t rive_hb_font_funcs_lazy_loader_t
+#define hb_color_stop_t rive_hb_color_stop_t
+#define hb_partial rive_hb_partial
+#define hb_ot_layout_kern rive_hb_ot_layout_kern
+#define hb_ot_layout_has_kerning rive_hb_ot_layout_has_kerning
+#define hb_ot_layout_position_start rive_hb_ot_layout_position_start
+#define hb_ot_layout_substitute_start rive_hb_ot_layout_substitute_start
+#define hb_ot_layout_has_cross_kerning rive_hb_ot_layout_has_cross_kerning
+#define hb_ot_layout_substitute_lookup rive_hb_ot_layout_substitute_lookup
+#define hb_ot_layout_table_find_feature rive_hb_ot_layout_table_find_feature
+#define hb_ot_layout_has_machine_kerning rive_hb_ot_layout_has_machine_kerning
+#define hb_ot_layout_position_finish_offsets rive_hb_ot_layout_position_finish_offsets
+#define hb_ot_layout_position_finish_advances rive_hb_ot_layout_position_finish_advances
+#define hb_popcount rive_hb_popcount
+#define hb_enumerate rive_hb_enumerate
+#define hb_bitwise_gt rive_hb_bitwise_gt
+#define hb_bitwise_lt rive_hb_bitwise_lt
+#define hb_bitwise_or rive_hb_bitwise_or
+#define hb_bitwise_and rive_hb_bitwise_and
+#define hb_bitwise_neg rive_hb_bitwise_neg
+#define hb_unicode_general_category_t rive_hb_unicode_general_category_t
+#define hb_add rive_hb_add
+#define hb_all rive_hb_all
+#define hb_any rive_hb_any
+#define hb_ctz rive_hb_ctz
+#define hb_zip rive_hb_zip
+#define hb_iota rive_hb_iota
+#define hb_apply rive_hb_apply
+#define hb_first rive_hb_first
+#define hb_range rive_hb_range
+#define hb_second rive_hb_second
+#define hb_apply_t rive_hb_apply_t
+#define hb_partial_t rive_hb_partial_t
+#define hb_range_iter_t rive_hb_range_iter_t
+#define hb_zip_iter_t rive_hb_zip_iter_t
+#define hb_iota_iter_t rive_hb_iota_iter_t
+#define hb_collect_features_context_t rive_hb_collect_features_context_t
+#define hb_position_single_dispatch_t rive_hb_position_single_dispatch_t
+#define hb_get_glyph_alternates_dispatch_t rive_hb_get_glyph_alternates_dispatch_t
+#define hb_ot_map_t rive_hb_ot_map_t
+#define hb_ot_shape_plan_t rive_hb_ot_shape_plan_t
+#define hb_ot_map_builder_t rive_hb_ot_map_builder_t
+#define hb_ot_map_feature_flags_t rive_hb_ot_map_feature_flags_t
+#define hb_ot_shape_plan_key_t rive_hb_ot_shape_plan_key_t
+#define hb_ot_math_glyph_part_t rive_hb_ot_math_glyph_part_t
+#define hb_ot_math_glyph_variant_t rive_hb_ot_math_glyph_variant_t
+#define hb_ot_math_kern_t rive_hb_ot_math_kern_t
+#define hb_ot_math_kern_entry_t rive_hb_ot_math_kern_entry_t
+#define hb_ot_meta_tag_t rive_hb_ot_meta_tag_t
+#define hb_ot_name_entry_t rive_hb_ot_name_entry_t
+#define hb_ot_name_get_utf rive_hb_ot_name_get_utf
+#define hb_ascii_t rive_hb_ascii_t
+#define hb_ot_shape_fallback_kern_driver_t rive_hb_ot_shape_fallback_kern_driver_t
+#define hb_in_ranges rive_hb_in_ranges
+#define hb_ot_position rive_hb_ot_position
+#define hb_form_clusters rive_hb_form_clusters
+#define hb_vert_char_for rive_hb_vert_char_for
+#define hb_ot_rotate_chars rive_hb_ot_rotate_chars
+#define hb_propagate_flags rive_hb_propagate_flags
+#define hb_ot_position_plan rive_hb_ot_position_plan
+#define hb_ot_shape_internal rive_hb_ot_shape_internal
+#define hb_ot_substitute_pre rive_hb_ot_substitute_pre
+#define hb_set_unicode_props rive_hb_set_unicode_props
+#define hb_ot_map_glyphs_fast rive_hb_ot_map_glyphs_fast
+#define hb_ot_substitute_plan rive_hb_ot_substitute_plan
+#define hb_ot_substitute_post rive_hb_ot_substitute_post
+#define hb_ot_position_default rive_hb_ot_position_default
+#define hb_insert_dotted_circle rive_hb_insert_dotted_circle
+#define hb_ot_shape_setup_masks rive_hb_ot_shape_setup_masks
+#define hb_ot_shaper_categorize rive_hb_ot_shaper_categorize
+#define hb_ot_substitute_default rive_hb_ot_substitute_default
+#define hb_ensure_native_direction rive_hb_ensure_native_direction
+#define hb_synthesize_glyph_classes rive_hb_synthesize_glyph_classes
+#define hb_ot_shape_collect_features rive_hb_ot_shape_collect_features
+#define hb_ot_shape_initialize_masks rive_hb_ot_shape_initialize_masks
+#define hb_ot_hide_default_ignorables rive_hb_ot_hide_default_ignorables
+#define hb_ot_shape_setup_masks_fraction rive_hb_ot_shape_setup_masks_fraction
+#define hb_ot_zero_width_default_ignorables rive_hb_ot_zero_width_default_ignorables
+#define hb_shape_plan_key_t rive_hb_shape_plan_key_t
+#define hb_ot_shape_planner_t rive_hb_ot_shape_planner_t
+#define hb_script_t rive_hb_script_t
+#define hb_map_retains_sorting rive_hb_map_retains_sorting
+#define hb_pool_t rive_hb_pool_t
+#define hb_len rive_hb_len
+#define hb_serialize_error_t rive_hb_serialize_error_t
+#define hb_indic_get_categories rive_hb_indic_get_categories
+#define hb_syllabic_clear_var rive_hb_syllabic_clear_var
+#define hb_syllabic_insert_dotted_circles rive_hb_syllabic_insert_dotted_circles
+#define hb_options rive_hb_options
+#define hb_options_initv rive_hb_options_initv
+#define minus_1 rive_minus_1
+#define hb_aat_apply_context_t rive_hb_aat_apply_context_t
+#define _hb_unicode_is_emoji_Extended_Pictographic rive__hb_unicode_is_emoji_Extended_Pictographic
+#define endchar_str rive_endchar_str
+#define _hb_ot_name_language_for_mac_code rive__hb_ot_name_language_for_mac_code
+#define _hb_ot_name_language_for_ms_code rive__hb_ot_name_language_for_ms_code
+#define hb_indic_would_substitute_feature_t rive_hb_indic_would_substitute_feature_t
+#define hb_ot_layout_glyph_props_flags_t rive_hb_ot_layout_glyph_props_flags_t
+#define hb_use_u16 rive_hb_use_u16
+#define lookup_expert_subset_charset_for_glyph rive_lookup_expert_subset_charset_for_glyph
+#define lookup_expert_charset_for_glyph rive_lookup_expert_charset_for_glyph
+#define _hb_options_init rive__hb_options_init
+#define hb_use_get_category rive_hb_use_get_category
+#define hb_use_b4 rive_hb_use_b4
+#define hb_use_u8 rive_hb_use_u8
+#define hb_ot_new_tag_to_script rive_hb_ot_new_tag_to_script
+#define hb_ot_old_tag_to_script rive_hb_ot_old_tag_to_script
+#define hb_ot_tags_from_language rive_hb_ot_tags_from_language
+#define hb_ot_new_tag_from_script rive_hb_ot_new_tag_from_script
+#define hb_ot_old_tag_from_script rive_hb_ot_old_tag_from_script
+#define hb_ot_all_tags_from_script rive_hb_ot_all_tags_from_script
+#define hb_ot_ambiguous_tag_to_language rive_hb_ot_ambiguous_tag_to_language
+#define hb_ot_tags_from_complex_language rive_hb_ot_tags_from_complex_language
+#define hb_ot_var_axis_info_t rive_hb_ot_var_axis_info_t
+#define hb_ot_var_axis_t rive_hb_ot_var_axis_t
+#define hb_outline_recording_pen_line_to rive_hb_outline_recording_pen_line_to
+#define hb_outline_recording_pen_move_to rive_hb_outline_recording_pen_move_to
+#define hb_outline_recording_pen_cubic_to rive_hb_outline_recording_pen_cubic_to
+#define hb_outline_recording_pen_close_path rive_hb_outline_recording_pen_close_path
+#define hb_outline_recording_pen_quadratic_to rive_hb_outline_recording_pen_quadratic_to
+#define hb_outline_point_t rive_hb_outline_point_t
+#define hb_outline_recording_pen_funcs_lazy_loader_t rive_hb_outline_recording_pen_funcs_lazy_loader_t
+#define hb_outline_vector_t rive_hb_outline_vector_t
+#define hb_draw_funcs_lazy_loader_t rive_hb_draw_funcs_lazy_loader_t
+#define hb_draw_extents_line_to rive_hb_draw_extents_line_to
+#define hb_draw_extents_move_to rive_hb_draw_extents_move_to
+#define hb_draw_extents_cubic_to rive_hb_draw_extents_cubic_to
+#define hb_draw_extents_get_funcs rive_hb_draw_extents_get_funcs
+#define hb_paint_extents_pop_clip rive_hb_paint_extents_pop_clip
+#define hb_paint_extents_pop_group rive_hb_paint_extents_pop_group
+#define hb_paint_extents_push_group rive_hb_paint_extents_push_group
+#define hb_draw_extents_quadratic_to rive_hb_draw_extents_quadratic_to
+#define hb_paint_extents_paint_color rive_hb_paint_extents_paint_color
+#define hb_paint_extents_paint_image rive_hb_paint_extents_paint_image
+#define hb_paint_extents_pop_transform rive_hb_paint_extents_pop_transform
+#define hb_paint_extents_push_transform rive_hb_paint_extents_push_transform
+#define hb_paint_extents_push_clip_glyph rive_hb_paint_extents_push_clip_glyph
+#define hb_paint_extents_push_clip_rectangle rive_hb_paint_extents_push_clip_rectangle
+#define hb_paint_extents_paint_sweep_gradient rive_hb_paint_extents_paint_sweep_gradient
+#define hb_paint_extents_paint_linear_gradient rive_hb_paint_extents_paint_linear_gradient
+#define hb_paint_extents_paint_radial_gradient rive_hb_paint_extents_paint_radial_gradient
+#define hb_draw_extents_funcs_lazy_loader_t rive_hb_draw_extents_funcs_lazy_loader_t
+#define hb_paint_extents_funcs_lazy_loader_t rive_hb_paint_extents_funcs_lazy_loader_t
+#define hb_paint_funcs_lazy_loader_t rive_hb_paint_funcs_lazy_loader_t
+#define hb_paint_color_nil rive_hb_paint_color_nil
+#define hb_paint_image_nil rive_hb_paint_image_nil
+#define hb_paint_pop_clip_nil rive_hb_paint_pop_clip_nil
+#define hb_paint_pop_group_nil rive_hb_paint_pop_group_nil
+#define hb_paint_push_group_nil rive_hb_paint_push_group_nil
+#define hb_paint_color_glyph_nil rive_hb_paint_color_glyph_nil
+#define hb_paint_pop_transform_nil rive_hb_paint_pop_transform_nil
+#define hb_paint_push_transform_nil rive_hb_paint_push_transform_nil
+#define hb_paint_sweep_gradient_nil rive_hb_paint_sweep_gradient_nil
+#define hb_paint_linear_gradient_nil rive_hb_paint_linear_gradient_nil
+#define hb_paint_push_clip_glyph_nil rive_hb_paint_push_clip_glyph_nil
+#define hb_paint_radial_gradient_nil rive_hb_paint_radial_gradient_nil
+#define hb_paint_push_clip_rectangle_nil rive_hb_paint_push_clip_rectangle_nil
+#define hb_paint_custom_palette_color_nil rive_hb_paint_custom_palette_color_nil
+#define hb_bitwise_xor rive_hb_bitwise_xor
+#define hb_shape_plan_t rive_hb_shape_plan_t
+#define hb_shaper_list_lazy_loader_t rive_hb_shaper_list_lazy_loader_t
+#define hb_shapers_lazy_loader_t rive_hb_shapers_lazy_loader_t
+#define hb_shaper_entry_t rive_hb_shaper_entry_t
+#define hb_ot_language_map_t rive_hb_ot_language_map_t
+#define hb_serialize_cff_fdselect rive_hb_serialize_cff_fdselect
+#define hb_plan_subset_cff_fdselect rive_hb_plan_subset_cff_fdselect
+#define hb_inc_bimap_t rive_hb_inc_bimap_t
+#define hb_subset_plan_t rive_hb_subset_plan_t
+#define hb_subset_input_t rive_hb_subset_input_t
+#define hb_subset_flags_t rive_hb_subset_flags_t
+#define hb_bool rive_hb_bool
+#define hb_pair rive_hb_pair
+#define hb_multimap_t rive_hb_multimap_t
+#define hb_subset_accelerator_t rive_hb_subset_accelerator_t
+#define hb_lock_t rive_hb_lock_t
+#define hb_repeat_iter_t rive_hb_repeat_iter_t
+#define hb_resolve_overflows rive_hb_resolve_overflows
+#define hb_resolve_graph_overflows rive_hb_resolve_graph_overflows
+#define hb_subset_context_t rive_hb_subset_context_t
+#define hb_ceil_to_4 rive_hb_ceil_to_4
+#define hb_take rive_hb_take
+#define hb_drain rive_hb_drain
+#define hb_repeat rive_hb_repeat
+#define hb_priority_queue_t rive_hb_priority_queue_t
+#define hb_ucd_script rive_hb_ucd_script
+#define hb_ucd_compose rive_hb_ucd_compose
+#define hb_ucd_decompose rive_hb_ucd_decompose
+#define hb_ucd_mirroring rive_hb_ucd_mirroring
+#define hb_ucd_combining_class rive_hb_ucd_combining_class
+#define hb_ucd_general_category rive_hb_ucd_general_category
+#define hb_ucd_unicode_funcs_lazy_loader_t rive_hb_ucd_unicode_funcs_lazy_loader_t
+#define hb_unicode_funcs_lazy_loader_t rive_hb_unicode_funcs_lazy_loader_t
+#define hb_unicode_script_nil rive_hb_unicode_script_nil
+#define hb_unicode_compose_nil rive_hb_unicode_compose_nil
+#define hb_unicode_decompose_nil rive_hb_unicode_decompose_nil
+#define hb_unicode_mirroring_nil rive_hb_unicode_mirroring_nil
+#define hb_unicode_combining_class_nil rive_hb_unicode_combining_class_nil
+#define hb_unicode_eastasian_width_nil rive_hb_unicode_eastasian_width_nil
+#define hb_unicode_general_category_nil rive_hb_unicode_general_category_nil
+#define hb_unicode_decompose_compatibility_nil rive_hb_unicode_decompose_compatibility_nil
+// _hb_*
+#define hb_compiler_memory_r_barrier rive_hb_compiler_memory_r_barrier
+#define hb_head_t rive_hb_head_t
+#define hb_roundf rive_hb_roundf
+#define hb_debug_msg rive_hb_debug_msg
+#define hb_cmp_method rive_hb_cmp_method
+#define hb_glyph_info_is_zwj rive_hb_glyph_info_is_zwj
+#define hb_glyph_info_is_zwnj rive_hb_glyph_info_is_zwnj
+#define hb_grapheme_group_func rive_hb_grapheme_group_func
+#define hb_glyph_info_substituted rive_hb_glyph_info_substituted
+#define hb_atomic_ptr_impl_cmplexch rive_hb_atomic_ptr_impl_cmplexch
+#define hb_glyph_info_get_glyph_props rive_hb_glyph_info_get_glyph_props
+#define hb_glyph_info_is_continuation rive_hb_glyph_info_is_continuation
+#define hb_glyph_info_set_glyph_props rive_hb_glyph_info_set_glyph_props
+#define hb_glyph_info_is_unicode_format rive_hb_glyph_info_is_unicode_format
+#define hb_glyph_info_get_general_category rive_hb_glyph_info_get_general_category
+#define hb_glyph_info_is_default_ignorable_and_not_hidden rive_hb_glyph_info_is_default_ignorable_and_not_hidden
+#define hb_aat_layout_feature_type_get_name_id rive_hb_aat_layout_feature_type_get_name_id
+#define hb_aat_layout_feature_type_get_selector_infos rive_hb_aat_layout_feature_type_get_selector_infos
+#define hb_aat_layout_get_feature_types rive_hb_aat_layout_get_feature_types
+#define hb_aat_layout_has_positioning rive_hb_aat_layout_has_positioning
+#define hb_aat_layout_has_substitution rive_hb_aat_layout_has_substitution
+#define hb_aat_layout_has_tracking rive_hb_aat_layout_has_tracking
+#define hb_blob_destroy rive_hb_blob_destroy
+#define hb_blob_get_data_writable rive_hb_blob_get_data_writable
+#define hb_blob_get_empty rive_hb_blob_get_empty
+#define hb_blob_make_immutable rive_hb_blob_make_immutable
+#define hb_blob_reference rive_hb_blob_reference
+#define hb_face_get_glyph_count rive_hb_face_get_glyph_count
+#define hb_face_reference_table rive_hb_face_reference_table
+#define hb_language_from_string rive_hb_language_from_string
+#define hb_language_matches rive_hb_language_matches
+#define hb_blob_copy_writable_or_fail rive_hb_blob_copy_writable_or_fail
+#define hb_blob_create rive_hb_blob_create
+#define hb_blob_create_from_file rive_hb_blob_create_from_file
+#define hb_blob_create_from_file_or_fail rive_hb_blob_create_from_file_or_fail
+#define hb_blob_create_or_fail rive_hb_blob_create_or_fail
+#define hb_blob_create_sub_blob rive_hb_blob_create_sub_blob
+#define hb_blob_get_data rive_hb_blob_get_data
+#define hb_blob_get_length rive_hb_blob_get_length
+#define hb_blob_get_user_data rive_hb_blob_get_user_data
+#define hb_blob_is_immutable rive_hb_blob_is_immutable
+#define hb_blob_set_user_data rive_hb_blob_set_user_data
+#define hb_buffer_deserialize_json rive_hb_buffer_deserialize_json
+#define hb_buffer_serialize_formats rive_hb_buffer_serialize_formats
+#define hb_buffer_serialize_invalid rive_hb_buffer_serialize_invalid
+#define hb_buffer_serialize_glyphs_json rive_hb_buffer_serialize_glyphs_json
+#define hb_buffer_serialize_glyphs_text rive_hb_buffer_serialize_glyphs_text
+#define hb_buffer_serialize_unicode_json rive_hb_buffer_serialize_unicode_json
+#define hb_buffer_serialize_unicode_text rive_hb_buffer_serialize_unicode_text
+#define hb_buffer_deserialize_text_glyphs rive_hb_buffer_deserialize_text_glyphs
+#define hb_buffer_deserialize_text_unicode rive_hb_buffer_deserialize_text_unicode
+#define hb_buffer_deserialize_glyphs rive_hb_buffer_deserialize_glyphs
+#define hb_buffer_deserialize_unicode rive_hb_buffer_deserialize_unicode
+#define hb_buffer_get_glyph_infos rive_hb_buffer_get_glyph_infos
+#define hb_buffer_get_glyph_positions rive_hb_buffer_get_glyph_positions
+#define hb_buffer_serialize rive_hb_buffer_serialize
+#define hb_buffer_serialize_format_from_string rive_hb_buffer_serialize_format_from_string
+#define hb_buffer_serialize_format_to_string rive_hb_buffer_serialize_format_to_string
+#define hb_buffer_serialize_glyphs rive_hb_buffer_serialize_glyphs
+#define hb_buffer_serialize_list_formats rive_hb_buffer_serialize_list_formats
+#define hb_buffer_serialize_unicode rive_hb_buffer_serialize_unicode
+#define hb_buffer_set_content_type rive_hb_buffer_set_content_type
+#define hb_font_get_empty rive_hb_font_get_empty
+#define hb_font_get_glyph_extents rive_hb_font_get_glyph_extents
+#define hb_font_glyph_from_string rive_hb_font_glyph_from_string
+#define hb_font_glyph_to_string rive_hb_font_glyph_to_string
+#define hb_tag_from_string rive_hb_tag_from_string
+#define hb_buffer_append rive_hb_buffer_append
+#define hb_buffer_clear_contents rive_hb_buffer_clear_contents
+#define hb_buffer_create_similar rive_hb_buffer_create_similar
+#define hb_buffer_destroy rive_hb_buffer_destroy
+#define hb_buffer_diff rive_hb_buffer_diff
+#define hb_buffer_get_direction rive_hb_buffer_get_direction
+#define hb_buffer_get_flags rive_hb_buffer_get_flags
+#define hb_buffer_get_segment_properties rive_hb_buffer_get_segment_properties
+#define hb_buffer_reverse rive_hb_buffer_reverse
+#define hb_buffer_set_flags rive_hb_buffer_set_flags
+#define hb_buffer_set_length rive_hb_buffer_set_length
+#define hb_buffer_set_segment_properties rive_hb_buffer_set_segment_properties
+#define hb_shape_full rive_hb_shape_full
+#define hb_buffer_add rive_hb_buffer_add
+#define hb_buffer_add_codepoints rive_hb_buffer_add_codepoints
+#define hb_buffer_add_latin1 rive_hb_buffer_add_latin1
+#define hb_buffer_add_utf16 rive_hb_buffer_add_utf16
+#define hb_buffer_add_utf32 rive_hb_buffer_add_utf32
+#define hb_buffer_add_utf8 rive_hb_buffer_add_utf8
+#define hb_buffer_allocation_successful rive_hb_buffer_allocation_successful
+#define hb_buffer_create rive_hb_buffer_create
+#define hb_buffer_get_cluster_level rive_hb_buffer_get_cluster_level
+#define hb_buffer_get_content_type rive_hb_buffer_get_content_type
+#define hb_buffer_get_empty rive_hb_buffer_get_empty
+#define hb_buffer_get_invisible_glyph rive_hb_buffer_get_invisible_glyph
+#define hb_buffer_get_language rive_hb_buffer_get_language
+#define hb_buffer_get_length rive_hb_buffer_get_length
+#define hb_buffer_get_not_found_glyph rive_hb_buffer_get_not_found_glyph
+#define hb_buffer_get_replacement_codepoint rive_hb_buffer_get_replacement_codepoint
+#define hb_buffer_get_script rive_hb_buffer_get_script
+#define hb_buffer_get_unicode_funcs rive_hb_buffer_get_unicode_funcs
+#define hb_buffer_get_user_data rive_hb_buffer_get_user_data
+#define hb_buffer_guess_segment_properties rive_hb_buffer_guess_segment_properties
+#define hb_buffer_has_positions rive_hb_buffer_has_positions
+#define hb_buffer_normalize_glyphs rive_hb_buffer_normalize_glyphs
+#define hb_buffer_pre_allocate rive_hb_buffer_pre_allocate
+#define hb_buffer_reference rive_hb_buffer_reference
+#define hb_buffer_reset rive_hb_buffer_reset
+#define hb_buffer_reverse_clusters rive_hb_buffer_reverse_clusters
+#define hb_buffer_reverse_range rive_hb_buffer_reverse_range
+#define hb_buffer_set_cluster_level rive_hb_buffer_set_cluster_level
+#define hb_buffer_set_direction rive_hb_buffer_set_direction
+#define hb_buffer_set_invisible_glyph rive_hb_buffer_set_invisible_glyph
+#define hb_buffer_set_language rive_hb_buffer_set_language
+#define hb_buffer_set_message_func rive_hb_buffer_set_message_func
+#define hb_buffer_set_not_found_glyph rive_hb_buffer_set_not_found_glyph
+#define hb_buffer_set_replacement_codepoint rive_hb_buffer_set_replacement_codepoint
+#define hb_buffer_set_script rive_hb_buffer_set_script
+#define hb_buffer_set_unicode_funcs rive_hb_buffer_set_unicode_funcs
+#define hb_buffer_set_user_data rive_hb_buffer_set_user_data
+#define hb_script_get_horizontal_direction rive_hb_script_get_horizontal_direction
+#define hb_segment_properties_equal rive_hb_segment_properties_equal
+#define hb_segment_properties_hash rive_hb_segment_properties_hash
+#define hb_segment_properties_overlay rive_hb_segment_properties_overlay
+#define hb_unicode_funcs_destroy rive_hb_unicode_funcs_destroy
+#define hb_unicode_funcs_get_default rive_hb_unicode_funcs_get_default
+#define hb_unicode_funcs_reference rive_hb_unicode_funcs_reference
+#define hb_options_init rive_hb_options_init
+#define hb_direction_from_string rive_hb_direction_from_string
+#define hb_direction_to_string rive_hb_direction_to_string
+#define hb_feature_from_string rive_hb_feature_from_string
+#define hb_feature_to_string rive_hb_feature_to_string
+#define hb_language_to_string rive_hb_language_to_string
+#define hb_script_from_iso15924_tag rive_hb_script_from_iso15924_tag
+#define hb_script_from_string rive_hb_script_from_string
+#define hb_script_to_iso15924_tag rive_hb_script_to_iso15924_tag
+#define hb_tag_to_string rive_hb_tag_to_string
+#define hb_variation_from_string rive_hb_variation_from_string
+#define hb_variation_to_string rive_hb_variation_to_string
+#define hb_version rive_hb_version
+#define hb_version_atleast rive_hb_version_atleast
+#define hb_version_string rive_hb_version_string
+#define hb_draw_funcs_set_middle rive_hb_draw_funcs_set_middle
+#define hb_draw_funcs_set_preamble rive_hb_draw_funcs_set_preamble
+#define hb_draw_close_path rive_hb_draw_close_path
+#define hb_draw_cubic_to rive_hb_draw_cubic_to
+#define hb_draw_funcs_create rive_hb_draw_funcs_create
+#define hb_draw_funcs_destroy rive_hb_draw_funcs_destroy
+#define hb_draw_funcs_get_empty rive_hb_draw_funcs_get_empty
+#define hb_draw_funcs_get_user_data rive_hb_draw_funcs_get_user_data
+#define hb_draw_funcs_is_immutable rive_hb_draw_funcs_is_immutable
+#define hb_draw_funcs_make_immutable rive_hb_draw_funcs_make_immutable
+#define hb_draw_funcs_reference rive_hb_draw_funcs_reference
+#define hb_draw_funcs_set_close_path_func rive_hb_draw_funcs_set_close_path_func
+#define hb_draw_funcs_set_cubic_to_func rive_hb_draw_funcs_set_cubic_to_func
+#define hb_draw_funcs_set_line_to_func rive_hb_draw_funcs_set_line_to_func
+#define hb_draw_funcs_set_move_to_func rive_hb_draw_funcs_set_move_to_func
+#define hb_draw_funcs_set_quadratic_to_func rive_hb_draw_funcs_set_quadratic_to_func
+#define hb_draw_funcs_set_user_data rive_hb_draw_funcs_set_user_data
+#define hb_draw_line_to rive_hb_draw_line_to
+#define hb_draw_move_to rive_hb_draw_move_to
+#define hb_draw_quadratic_to rive_hb_draw_quadratic_to
+#define hb_arabic_b2 rive_hb_arabic_b2
+#define hb_arabic_b4 rive_hb_arabic_b4
+#define hb_arabic_u8 rive_hb_arabic_u8
+#define hb_arabic_u16 rive_hb_arabic_u16
+#define hb_arabic_pua_simp_map rive_hb_arabic_pua_simp_map
+#define hb_arabic_pua_trad_map rive_hb_arabic_pua_trad_map
+#define hb_face_for_data_closure_create rive_hb_face_for_data_closure_create
+#define hb_face_for_data_closure_destroy rive_hb_face_for_data_closure_destroy
+#define hb_face_for_data_reference_table rive_hb_face_for_data_reference_table
+#define hb_face_collect_nominal_glyph_mapping rive_hb_face_collect_nominal_glyph_mapping
+#define hb_face_collect_unicodes rive_hb_face_collect_unicodes
+#define hb_face_collect_variation_selectors rive_hb_face_collect_variation_selectors
+#define hb_face_collect_variation_unicodes rive_hb_face_collect_variation_unicodes
+#define hb_face_count rive_hb_face_count
+#define hb_face_create rive_hb_face_create
+#define hb_face_create_for_tables rive_hb_face_create_for_tables
+#define hb_face_destroy rive_hb_face_destroy
+#define hb_face_get_empty rive_hb_face_get_empty
+#define hb_face_get_index rive_hb_face_get_index
+#define hb_face_get_table_tags rive_hb_face_get_table_tags
+#define hb_face_get_upem rive_hb_face_get_upem
+#define hb_face_get_user_data rive_hb_face_get_user_data
+#define hb_face_is_immutable rive_hb_face_is_immutable
+#define hb_face_make_immutable rive_hb_face_make_immutable
+#define hb_face_reference rive_hb_face_reference
+#define hb_face_reference_blob rive_hb_face_reference_blob
+#define hb_face_set_glyph_count rive_hb_face_set_glyph_count
+#define hb_face_set_index rive_hb_face_set_index
+#define hb_face_set_upem rive_hb_face_set_upem
+#define hb_face_set_user_data rive_hb_face_set_user_data
+#define hb_shape_plan_destroy rive_hb_shape_plan_destroy
+#define hb_font_create rive_hb_font_create
+#define hb_draw_funcs_default rive_hb_draw_funcs_default
+#define hb_font_funcs_default rive_hb_font_funcs_default
+#define hb_font_adopt_var_coords rive_hb_font_adopt_var_coords
+#define hb_font_funcs_set_middle rive_hb_font_funcs_set_middle
+#define hb_font_funcs_set_preamble rive_hb_font_funcs_set_preamble
+#define hb_font_add_glyph_origin_for_direction rive_hb_font_add_glyph_origin_for_direction
+#define hb_font_changed rive_hb_font_changed
+#define hb_font_create_sub_font rive_hb_font_create_sub_font
+#define hb_font_destroy rive_hb_font_destroy
+#define hb_font_draw_glyph rive_hb_font_draw_glyph
+#define hb_font_funcs_create rive_hb_font_funcs_create
+#define hb_font_funcs_destroy rive_hb_font_funcs_destroy
+#define hb_font_funcs_get_empty rive_hb_font_funcs_get_empty
+#define hb_font_funcs_get_user_data rive_hb_font_funcs_get_user_data
+#define hb_font_funcs_is_immutable rive_hb_font_funcs_is_immutable
+#define hb_font_funcs_make_immutable rive_hb_font_funcs_make_immutable
+#define hb_font_funcs_reference rive_hb_font_funcs_reference
+#define hb_font_funcs_set_draw_glyph_func rive_hb_font_funcs_set_draw_glyph_func
+#define hb_font_funcs_set_font_h_extents_func rive_hb_font_funcs_set_font_h_extents_func
+#define hb_font_funcs_set_font_v_extents_func rive_hb_font_funcs_set_font_v_extents_func
+#define hb_font_funcs_set_glyph_contour_point_func rive_hb_font_funcs_set_glyph_contour_point_func
+#define hb_font_funcs_set_glyph_extents_func rive_hb_font_funcs_set_glyph_extents_func
+#define hb_font_funcs_set_glyph_from_name_func rive_hb_font_funcs_set_glyph_from_name_func
+#define hb_font_funcs_set_glyph_func rive_hb_font_funcs_set_glyph_func
+#define hb_font_funcs_set_glyph_h_advance_func rive_hb_font_funcs_set_glyph_h_advance_func
+#define hb_font_funcs_set_glyph_h_advances_func rive_hb_font_funcs_set_glyph_h_advances_func
+#define hb_font_funcs_set_glyph_h_kerning_func rive_hb_font_funcs_set_glyph_h_kerning_func
+#define hb_font_funcs_set_glyph_h_origin_func rive_hb_font_funcs_set_glyph_h_origin_func
+#define hb_font_funcs_set_glyph_name_func rive_hb_font_funcs_set_glyph_name_func
+#define hb_font_funcs_set_glyph_shape_func rive_hb_font_funcs_set_glyph_shape_func
+#define hb_font_funcs_set_glyph_v_advance_func rive_hb_font_funcs_set_glyph_v_advance_func
+#define hb_font_funcs_set_glyph_v_advances_func rive_hb_font_funcs_set_glyph_v_advances_func
+#define hb_font_funcs_set_glyph_v_kerning_func rive_hb_font_funcs_set_glyph_v_kerning_func
+#define hb_font_funcs_set_glyph_v_origin_func rive_hb_font_funcs_set_glyph_v_origin_func
+#define hb_font_funcs_set_nominal_glyph_func rive_hb_font_funcs_set_nominal_glyph_func
+#define hb_font_funcs_set_nominal_glyphs_func rive_hb_font_funcs_set_nominal_glyphs_func
+#define hb_font_funcs_set_paint_glyph_func rive_hb_font_funcs_set_paint_glyph_func
+#define hb_font_funcs_set_user_data rive_hb_font_funcs_set_user_data
+#define hb_font_funcs_set_variation_glyph_func rive_hb_font_funcs_set_variation_glyph_func
+#define hb_font_get_extents_for_direction rive_hb_font_get_extents_for_direction
+#define hb_font_get_face rive_hb_font_get_face
+#define hb_font_get_glyph rive_hb_font_get_glyph
+#define hb_font_get_glyph_advance_for_direction rive_hb_font_get_glyph_advance_for_direction
+#define hb_font_get_glyph_advances_for_direction rive_hb_font_get_glyph_advances_for_direction
+#define hb_font_get_glyph_contour_point rive_hb_font_get_glyph_contour_point
+#define hb_font_get_glyph_contour_point_for_origin rive_hb_font_get_glyph_contour_point_for_origin
+#define hb_font_get_glyph_extents_for_origin rive_hb_font_get_glyph_extents_for_origin
+#define hb_font_get_glyph_from_name rive_hb_font_get_glyph_from_name
+#define hb_font_get_glyph_h_advance rive_hb_font_get_glyph_h_advance
+#define hb_font_get_glyph_h_advances rive_hb_font_get_glyph_h_advances
+#define hb_font_get_glyph_h_kerning rive_hb_font_get_glyph_h_kerning
+#define hb_font_get_glyph_h_origin rive_hb_font_get_glyph_h_origin
+#define hb_font_get_glyph_kerning_for_direction rive_hb_font_get_glyph_kerning_for_direction
+#define hb_font_get_glyph_name rive_hb_font_get_glyph_name
+#define hb_font_get_glyph_origin_for_direction rive_hb_font_get_glyph_origin_for_direction
+#define hb_font_get_glyph_shape rive_hb_font_get_glyph_shape
+#define hb_font_get_glyph_v_advance rive_hb_font_get_glyph_v_advance
+#define hb_font_get_glyph_v_advances rive_hb_font_get_glyph_v_advances
+#define hb_font_get_glyph_v_kerning rive_hb_font_get_glyph_v_kerning
+#define hb_font_get_glyph_v_origin rive_hb_font_get_glyph_v_origin
+#define hb_font_get_h_extents rive_hb_font_get_h_extents
+#define hb_font_get_nominal_glyph rive_hb_font_get_nominal_glyph
+#define hb_font_get_nominal_glyphs rive_hb_font_get_nominal_glyphs
+#define hb_font_get_parent rive_hb_font_get_parent
+#define hb_font_get_ppem rive_hb_font_get_ppem
+#define hb_font_get_ptem rive_hb_font_get_ptem
+#define hb_font_get_scale rive_hb_font_get_scale
+#define hb_font_get_serial rive_hb_font_get_serial
+#define hb_font_get_synthetic_bold rive_hb_font_get_synthetic_bold
+#define hb_font_get_synthetic_slant rive_hb_font_get_synthetic_slant
+#define hb_font_get_user_data rive_hb_font_get_user_data
+#define hb_font_get_v_extents rive_hb_font_get_v_extents
+#define hb_font_get_var_coords_design rive_hb_font_get_var_coords_design
+#define hb_font_get_var_coords_normalized rive_hb_font_get_var_coords_normalized
+#define hb_font_get_var_named_instance rive_hb_font_get_var_named_instance
+#define hb_font_get_variation_glyph rive_hb_font_get_variation_glyph
+#define hb_font_is_immutable rive_hb_font_is_immutable
+#define hb_font_make_immutable rive_hb_font_make_immutable
+#define hb_font_paint_glyph rive_hb_font_paint_glyph
+#define hb_font_reference rive_hb_font_reference
+#define hb_font_set_face rive_hb_font_set_face
+#define hb_font_set_funcs rive_hb_font_set_funcs
+#define hb_font_set_funcs_data rive_hb_font_set_funcs_data
+#define hb_font_set_parent rive_hb_font_set_parent
+#define hb_font_set_ppem rive_hb_font_set_ppem
+#define hb_font_set_ptem rive_hb_font_set_ptem
+#define hb_font_set_scale rive_hb_font_set_scale
+#define hb_font_set_synthetic_bold rive_hb_font_set_synthetic_bold
+#define hb_font_set_synthetic_slant rive_hb_font_set_synthetic_slant
+#define hb_font_set_user_data rive_hb_font_set_user_data
+#define hb_font_set_var_coords_design rive_hb_font_set_var_coords_design
+#define hb_font_set_var_coords_normalized rive_hb_font_set_var_coords_normalized
+#define hb_font_set_var_named_instance rive_hb_font_set_var_named_instance
+#define hb_font_set_variation rive_hb_font_set_variation
+#define hb_font_set_variations rive_hb_font_set_variations
+#define hb_font_subtract_glyph_origin_for_direction rive_hb_font_subtract_glyph_origin_for_direction
+#define hb_ot_font_set_funcs rive_hb_ot_font_set_funcs
+#define hb_ot_var_named_instance_get_design_coords rive_hb_ot_var_named_instance_get_design_coords
+#define hb_ot_var_normalize_coords rive_hb_ot_var_normalize_coords
+#define hb_map_allocation_successful rive_hb_map_allocation_successful
+#define hb_map_clear rive_hb_map_clear
+#define hb_map_copy rive_hb_map_copy
+#define hb_map_create rive_hb_map_create
+#define hb_map_del rive_hb_map_del
+#define hb_map_destroy rive_hb_map_destroy
+#define hb_map_get rive_hb_map_get
+#define hb_map_get_empty rive_hb_map_get_empty
+#define hb_map_get_population rive_hb_map_get_population
+#define hb_map_get_user_data rive_hb_map_get_user_data
+#define hb_map_has rive_hb_map_has
+#define hb_map_hash rive_hb_map_hash
+#define hb_map_is_empty rive_hb_map_is_empty
+#define hb_map_is_equal rive_hb_map_is_equal
+#define hb_map_keys rive_hb_map_keys
+#define hb_map_next rive_hb_map_next
+#define hb_map_reference rive_hb_map_reference
+#define hb_map_set rive_hb_map_set
+#define hb_map_set_user_data rive_hb_map_set_user_data
+#define hb_map_update rive_hb_map_update
+#define hb_map_values rive_hb_map_values
+#define hb_ot_color_glyph_get_layers rive_hb_ot_color_glyph_get_layers
+#define hb_ot_color_glyph_has_paint rive_hb_ot_color_glyph_has_paint
+#define hb_ot_color_glyph_reference_png rive_hb_ot_color_glyph_reference_png
+#define hb_ot_color_glyph_reference_svg rive_hb_ot_color_glyph_reference_svg
+#define hb_ot_color_has_layers rive_hb_ot_color_has_layers
+#define hb_ot_color_has_paint rive_hb_ot_color_has_paint
+#define hb_ot_color_has_palettes rive_hb_ot_color_has_palettes
+#define hb_ot_color_has_png rive_hb_ot_color_has_png
+#define hb_ot_color_has_svg rive_hb_ot_color_has_svg
+#define hb_ot_color_palette_color_get_name_id rive_hb_ot_color_palette_color_get_name_id
+#define hb_ot_color_palette_get_colors rive_hb_ot_color_palette_get_colors
+#define hb_ot_color_palette_get_count rive_hb_ot_color_palette_get_count
+#define hb_ot_color_palette_get_flags rive_hb_ot_color_palette_get_flags
+#define hb_ot_color_palette_get_name_id rive_hb_ot_color_palette_get_name_id
+#define hb_ot_metrics_get_position_common rive_hb_ot_metrics_get_position_common
+#define hb_ot_font_create rive_hb_ot_font_create
+#define hb_ot_font_destroy rive_hb_ot_font_destroy
+#define hb_ot_get_font_funcs rive_hb_ot_get_font_funcs
+#define hb_allocate_lig_id rive_hb_allocate_lig_id
+#define hb_glyph_info_is_mark rive_hb_glyph_info_is_mark
+#define hb_glyph_info_get_lig_id rive_hb_glyph_info_get_lig_id
+#define hb_glyph_info_multiplied rive_hb_glyph_info_multiplied
+#define hb_glyph_info_is_ligature rive_hb_glyph_info_is_ligature
+#define hb_glyph_info_get_lig_comp rive_hb_glyph_info_get_lig_comp
+#define hb_glyph_info_is_base_glyph rive_hb_glyph_info_is_base_glyph
+#define hb_ot_layout_set_glyph_props rive_hb_ot_layout_set_glyph_props
+#define hb_glyph_info_clear_lig_props rive_hb_glyph_info_clear_lig_props
+#define hb_buffer_assert_gsubgpos_vars rive_hb_buffer_assert_gsubgpos_vars
+#define hb_glyph_info_ligated_internal rive_hb_glyph_info_ligated_internal
+#define hb_glyph_info_get_lig_num_comps rive_hb_glyph_info_get_lig_num_comps
+#define hb_glyph_info_set_general_category rive_hb_glyph_info_set_general_category
+#define hb_glyph_info_set_lig_props_for_mark rive_hb_glyph_info_set_lig_props_for_mark
+#define hb_glyph_info_set_lig_props_for_ligature rive_hb_glyph_info_set_lig_props_for_ligature
+#define hb_glyph_info_set_lig_props_for_component rive_hb_glyph_info_set_lig_props_for_component
+#define hb_ot_layout_collect_features rive_hb_ot_layout_collect_features
+#define hb_ot_layout_collect_features_map rive_hb_ot_layout_collect_features_map
+#define hb_ot_layout_collect_lookups rive_hb_ot_layout_collect_lookups
+#define hb_ot_layout_feature_get_characters rive_hb_ot_layout_feature_get_characters
+#define hb_ot_layout_feature_get_lookups rive_hb_ot_layout_feature_get_lookups
+#define hb_ot_layout_feature_get_name_ids rive_hb_ot_layout_feature_get_name_ids
+#define hb_ot_layout_feature_with_variations_get_lookups rive_hb_ot_layout_feature_with_variations_get_lookups
+#define hb_ot_layout_get_attach_points rive_hb_ot_layout_get_attach_points
+#define hb_ot_layout_get_baseline rive_hb_ot_layout_get_baseline
+#define hb_ot_layout_get_baseline2 rive_hb_ot_layout_get_baseline2
+#define hb_ot_layout_get_baseline_with_fallback rive_hb_ot_layout_get_baseline_with_fallback
+#define hb_ot_layout_get_baseline_with_fallback2 rive_hb_ot_layout_get_baseline_with_fallback2
+#define hb_ot_layout_get_font_extents rive_hb_ot_layout_get_font_extents
+#define hb_ot_layout_get_font_extents2 rive_hb_ot_layout_get_font_extents2
+#define hb_ot_layout_get_glyph_class rive_hb_ot_layout_get_glyph_class
+#define hb_ot_layout_get_glyphs_in_class rive_hb_ot_layout_get_glyphs_in_class
+#define hb_ot_layout_get_horizontal_baseline_tag_for_script rive_hb_ot_layout_get_horizontal_baseline_tag_for_script
+#define hb_ot_layout_get_ligature_carets rive_hb_ot_layout_get_ligature_carets
+#define hb_ot_layout_get_size_params rive_hb_ot_layout_get_size_params
+#define hb_ot_layout_has_glyph_classes rive_hb_ot_layout_has_glyph_classes
+#define hb_ot_layout_has_positioning rive_hb_ot_layout_has_positioning
+#define hb_ot_layout_has_substitution rive_hb_ot_layout_has_substitution
+#define hb_ot_layout_language_find_feature rive_hb_ot_layout_language_find_feature
+#define hb_ot_layout_language_get_feature_indexes rive_hb_ot_layout_language_get_feature_indexes
+#define hb_ot_layout_language_get_feature_tags rive_hb_ot_layout_language_get_feature_tags
+#define hb_ot_layout_language_get_required_feature rive_hb_ot_layout_language_get_required_feature
+#define hb_ot_layout_language_get_required_feature_index rive_hb_ot_layout_language_get_required_feature_index
+#define hb_ot_layout_lookup_collect_glyphs rive_hb_ot_layout_lookup_collect_glyphs
+#define hb_ot_layout_lookup_get_glyph_alternates rive_hb_ot_layout_lookup_get_glyph_alternates
+#define hb_ot_layout_lookup_get_optical_bound rive_hb_ot_layout_lookup_get_optical_bound
+#define hb_ot_layout_lookup_substitute_closure rive_hb_ot_layout_lookup_substitute_closure
+#define hb_ot_layout_lookup_would_substitute rive_hb_ot_layout_lookup_would_substitute
+#define hb_ot_layout_lookups_substitute_closure rive_hb_ot_layout_lookups_substitute_closure
+#define hb_ot_layout_script_find_language rive_hb_ot_layout_script_find_language
+#define hb_ot_layout_script_get_language_tags rive_hb_ot_layout_script_get_language_tags
+#define hb_ot_layout_script_select_language rive_hb_ot_layout_script_select_language
+#define hb_ot_layout_script_select_language2 rive_hb_ot_layout_script_select_language2
+#define hb_ot_layout_table_choose_script rive_hb_ot_layout_table_choose_script
+#define hb_ot_layout_table_find_feature_variations rive_hb_ot_layout_table_find_feature_variations
+#define hb_ot_layout_table_find_script rive_hb_ot_layout_table_find_script
+#define hb_ot_layout_table_get_feature_tags rive_hb_ot_layout_table_get_feature_tags
+#define hb_ot_layout_table_get_lookup_count rive_hb_ot_layout_table_get_lookup_count
+#define hb_ot_layout_table_get_script_tags rive_hb_ot_layout_table_get_script_tags
+#define hb_ot_layout_table_select_script rive_hb_ot_layout_table_select_script
+#define hb_ot_metrics_get_position_with_fallback rive_hb_ot_metrics_get_position_with_fallback
+#define hb_ot_tags_from_script_and_language rive_hb_ot_tags_from_script_and_language
+#define hb_set_add_range rive_hb_set_add_range
+#define hb_set_create rive_hb_set_create
+#define hb_set_destroy rive_hb_set_destroy
+#define hb_set_get_empty rive_hb_set_get_empty
+#define hb_set_get_user_data rive_hb_set_get_user_data
+#define hb_set_reference rive_hb_set_reference
+#define hb_set_set_user_data rive_hb_set_set_user_data
+#define hb_ot_math_get_constant rive_hb_ot_math_get_constant
+#define hb_ot_math_get_glyph_assembly rive_hb_ot_math_get_glyph_assembly
+#define hb_ot_math_get_glyph_italics_correction rive_hb_ot_math_get_glyph_italics_correction
+#define hb_ot_math_get_glyph_kerning rive_hb_ot_math_get_glyph_kerning
+#define hb_ot_math_get_glyph_kernings rive_hb_ot_math_get_glyph_kernings
+#define hb_ot_math_get_glyph_top_accent_attachment rive_hb_ot_math_get_glyph_top_accent_attachment
+#define hb_ot_math_get_glyph_variants rive_hb_ot_math_get_glyph_variants
+#define hb_ot_math_get_min_connector_overlap rive_hb_ot_math_get_min_connector_overlap
+#define hb_ot_math_has_data rive_hb_ot_math_has_data
+#define hb_ot_math_is_glyph_extended_shape rive_hb_ot_math_is_glyph_extended_shape
+#define hb_ot_meta_get_entry_tags rive_hb_ot_meta_get_entry_tags
+#define hb_ot_meta_reference_entry rive_hb_ot_meta_reference_entry
+#define hb_ot_metrics_get_position rive_hb_ot_metrics_get_position
+#define hb_ot_metrics_get_variation rive_hb_ot_metrics_get_variation
+#define hb_ot_metrics_get_x_variation rive_hb_ot_metrics_get_x_variation
+#define hb_ot_metrics_get_y_variation rive_hb_ot_metrics_get_y_variation
+#define hb_ot_name_language_for_ms_code rive_hb_ot_name_language_for_ms_code
+#define hb_ot_name_language_for_mac_code rive_hb_ot_name_language_for_mac_code
+#define hb_ot_name_get_utf16 rive_hb_ot_name_get_utf16
+#define hb_ot_name_get_utf32 rive_hb_ot_name_get_utf32
+#define hb_ot_name_get_utf8 rive_hb_ot_name_get_utf8
+#define hb_ot_name_list_names rive_hb_ot_name_list_names
+#define hb_ot_shape_fallback_kern rive_hb_ot_shape_fallback_kern
+#define hb_ot_shape_fallback_spaces rive_hb_ot_shape_fallback_spaces
+#define hb_ot_shape_fallback_mark_position rive_hb_ot_shape_fallback_mark_position
+#define hb_ot_shape_fallback_mark_position_recategorize_marks rive_hb_ot_shape_fallback_mark_position_recategorize_marks
+#define hb_glyph_info_ligated rive_hb_glyph_info_ligated
+#define hb_glyph_info_is_unicode_mark rive_hb_glyph_info_is_unicode_mark
+#define hb_glyph_info_is_unicode_space rive_hb_glyph_info_is_unicode_space
+#define hb_glyph_info_get_modified_combining_class rive_hb_glyph_info_get_modified_combining_class
+#define hb_glyph_info_set_modified_combining_class rive_hb_glyph_info_set_modified_combining_class
+#define hb_glyph_info_get_unicode_space_fallback_type rive_hb_glyph_info_get_unicode_space_fallback_type
+#define hb_ot_shape_normalize rive_hb_ot_shape_normalize
+#define hb_glyph_info_unhide rive_hb_glyph_info_unhide
+#define hb_buffer_assert_unicode_vars rive_hb_buffer_assert_unicode_vars
+#define hb_glyph_info_set_unicode_props rive_hb_glyph_info_set_unicode_props
+#define hb_glyph_info_set_unicode_space_fallback_type rive_hb_glyph_info_set_unicode_space_fallback_type
+#define hb_unicode_is_emoji_Extended_Pictographic rive_hb_unicode_is_emoji_Extended_Pictographic
+#define hb_apply_morx rive_hb_apply_morx
+#define hb_glyph_info_set_continuation rive_hb_glyph_info_set_continuation
+#define hb_ot_layout_reverse_graphemes rive_hb_ot_layout_reverse_graphemes
+#define hb_buffer_allocate_unicode_vars rive_hb_buffer_allocate_unicode_vars
+#define hb_buffer_allocate_gsubgpos_vars rive_hb_buffer_allocate_gsubgpos_vars
+#define hb_buffer_deallocate_unicode_vars rive_hb_buffer_deallocate_unicode_vars
+#define hb_buffer_deallocate_gsubgpos_vars rive_hb_buffer_deallocate_gsubgpos_vars
+#define hb_codepoint_is_regional_indicator rive_hb_codepoint_is_regional_indicator
+#define hb_glyph_info_is_default_ignorable rive_hb_glyph_info_is_default_ignorable
+#define hb_ot_shape_glyphs_closure rive_hb_ot_shape_glyphs_closure
+#define hb_ot_shape_plan_collect_lookups rive_hb_ot_shape_plan_collect_lookups
+#define hb_shape_plan_create_cached rive_hb_shape_plan_create_cached
+#define hb_preprocess_text_vowel_constraints rive_hb_preprocess_text_vowel_constraints
+#define hb_next_syllable rive_hb_next_syllable
+#define hb_glyph_info_ligated_and_didnt_multiply rive_hb_glyph_info_ligated_and_didnt_multiply
+#define hb_glyph_info_clear_ligated_and_multiplied rive_hb_glyph_info_clear_ligated_and_multiplied
+#define hb_clear_substitution_flags rive_hb_clear_substitution_flags
+#define hb_glyph_info_clear_substituted rive_hb_glyph_info_clear_substituted
+#define hb_glyph_info_reset_continuation rive_hb_glyph_info_reset_continuation
+#define hb_ot_tag_from_language rive_hb_ot_tag_from_language
+#define hb_ot_tag_to_language rive_hb_ot_tag_to_language
+#define hb_ot_tag_to_script rive_hb_ot_tag_to_script
+#define hb_ot_tags_from_script rive_hb_ot_tags_from_script
+#define hb_ot_tags_to_script_and_language rive_hb_ot_tags_to_script_and_language
+#define hb_ot_var_find_axis rive_hb_ot_var_find_axis
+#define hb_ot_var_find_axis_info rive_hb_ot_var_find_axis_info
+#define hb_ot_var_get_axes rive_hb_ot_var_get_axes
+#define hb_ot_var_get_axis_count rive_hb_ot_var_get_axis_count
+#define hb_ot_var_get_axis_infos rive_hb_ot_var_get_axis_infos
+#define hb_ot_var_get_named_instance_count rive_hb_ot_var_get_named_instance_count
+#define hb_ot_var_has_data rive_hb_ot_var_has_data
+#define hb_ot_var_named_instance_get_postscript_name_id rive_hb_ot_var_named_instance_get_postscript_name_id
+#define hb_ot_var_named_instance_get_subfamily_name_id rive_hb_ot_var_named_instance_get_subfamily_name_id
+#define hb_ot_var_normalize_variations rive_hb_ot_var_normalize_variations
+#define hb_paint_funcs_create rive_hb_paint_funcs_create
+#define hb_paint_funcs_destroy rive_hb_paint_funcs_destroy
+#define hb_paint_funcs_get_empty rive_hb_paint_funcs_get_empty
+#define hb_paint_funcs_make_immutable rive_hb_paint_funcs_make_immutable
+#define hb_paint_funcs_set_color_func rive_hb_paint_funcs_set_color_func
+#define hb_paint_funcs_set_image_func rive_hb_paint_funcs_set_image_func
+#define hb_paint_funcs_set_linear_gradient_func rive_hb_paint_funcs_set_linear_gradient_func
+#define hb_paint_funcs_set_pop_clip_func rive_hb_paint_funcs_set_pop_clip_func
+#define hb_paint_funcs_set_pop_group_func rive_hb_paint_funcs_set_pop_group_func
+#define hb_paint_funcs_set_pop_transform_func rive_hb_paint_funcs_set_pop_transform_func
+#define hb_paint_funcs_set_push_clip_glyph_func rive_hb_paint_funcs_set_push_clip_glyph_func
+#define hb_paint_funcs_set_push_clip_rectangle_func rive_hb_paint_funcs_set_push_clip_rectangle_func
+#define hb_paint_funcs_set_push_group_func rive_hb_paint_funcs_set_push_group_func
+#define hb_paint_funcs_set_push_transform_func rive_hb_paint_funcs_set_push_transform_func
+#define hb_paint_funcs_set_radial_gradient_func rive_hb_paint_funcs_set_radial_gradient_func
+#define hb_paint_funcs_set_sweep_gradient_func rive_hb_paint_funcs_set_sweep_gradient_func
+#define hb_paint_funcs_set_middle rive_hb_paint_funcs_set_middle
+#define hb_paint_funcs_set_preamble rive_hb_paint_funcs_set_preamble
+#define hb_color_line_get_color_stops rive_hb_color_line_get_color_stops
+#define hb_color_line_get_extend rive_hb_color_line_get_extend
+#define hb_paint_color rive_hb_paint_color
+#define hb_paint_color_glyph rive_hb_paint_color_glyph
+#define hb_paint_custom_palette_color rive_hb_paint_custom_palette_color
+#define hb_paint_funcs_get_user_data rive_hb_paint_funcs_get_user_data
+#define hb_paint_funcs_is_immutable rive_hb_paint_funcs_is_immutable
+#define hb_paint_funcs_reference rive_hb_paint_funcs_reference
+#define hb_paint_funcs_set_color_glyph_func rive_hb_paint_funcs_set_color_glyph_func
+#define hb_paint_funcs_set_custom_palette_color_func rive_hb_paint_funcs_set_custom_palette_color_func
+#define hb_paint_funcs_set_user_data rive_hb_paint_funcs_set_user_data
+#define hb_paint_image rive_hb_paint_image
+#define hb_paint_linear_gradient rive_hb_paint_linear_gradient
+#define hb_paint_pop_clip rive_hb_paint_pop_clip
+#define hb_paint_pop_group rive_hb_paint_pop_group
+#define hb_paint_pop_transform rive_hb_paint_pop_transform
+#define hb_paint_push_clip_glyph rive_hb_paint_push_clip_glyph
+#define hb_paint_push_clip_rectangle rive_hb_paint_push_clip_rectangle
+#define hb_paint_push_group rive_hb_paint_push_group
+#define hb_paint_push_transform rive_hb_paint_push_transform
+#define hb_paint_radial_gradient rive_hb_paint_radial_gradient
+#define hb_paint_sweep_gradient rive_hb_paint_sweep_gradient
+#define hb_set_add rive_hb_set_add
+#define hb_set_add_sorted_array rive_hb_set_add_sorted_array
+#define hb_set_allocation_successful rive_hb_set_allocation_successful
+#define hb_set_clear rive_hb_set_clear
+#define hb_set_copy rive_hb_set_copy
+#define hb_set_del rive_hb_set_del
+#define hb_set_del_range rive_hb_set_del_range
+#define hb_set_get_max rive_hb_set_get_max
+#define hb_set_get_min rive_hb_set_get_min
+#define hb_set_get_population rive_hb_set_get_population
+#define hb_set_has rive_hb_set_has
+#define hb_set_hash rive_hb_set_hash
+#define hb_set_intersect rive_hb_set_intersect
+#define hb_set_invert rive_hb_set_invert
+#define hb_set_is_empty rive_hb_set_is_empty
+#define hb_set_is_equal rive_hb_set_is_equal
+#define hb_set_is_inverted rive_hb_set_is_inverted
+#define hb_set_is_subset rive_hb_set_is_subset
+#define hb_set_next rive_hb_set_next
+#define hb_set_next_many rive_hb_set_next_many
+#define hb_set_next_range rive_hb_set_next_range
+#define hb_set_previous rive_hb_set_previous
+#define hb_set_previous_range rive_hb_set_previous_range
+#define hb_set_set rive_hb_set_set
+#define hb_set_subtract rive_hb_set_subtract
+#define hb_set_symmetric_difference rive_hb_set_symmetric_difference
+#define hb_set_union rive_hb_set_union
+#define hb_shapers_get rive_hb_shapers_get
+#define hb_shape_plan_execute_internal rive_hb_shape_plan_execute_internal
+#define hb_shape_plan_create rive_hb_shape_plan_create
+#define hb_shape_plan_create2 rive_hb_shape_plan_create2
+#define hb_shape_plan_create_cached2 rive_hb_shape_plan_create_cached2
+#define hb_shape_plan_execute rive_hb_shape_plan_execute
+#define hb_shape_plan_get_empty rive_hb_shape_plan_get_empty
+#define hb_shape_plan_get_shaper rive_hb_shape_plan_get_shaper
+#define hb_shape_plan_get_user_data rive_hb_shape_plan_get_user_data
+#define hb_shape_plan_reference rive_hb_shape_plan_reference
+#define hb_shape_plan_set_user_data rive_hb_shape_plan_set_user_data
+#define hb_shape rive_hb_shape
+#define hb_shape_list_shapers rive_hb_shape_list_shapers
+#define hb_all_shapers rive_hb_all_shapers
+#define hb_ms_language_map rive_hb_ms_language_map
+#define hb_mac_language_map rive_hb_mac_language_map
+#define hb_ot_name_language_for rive_hb_ot_name_language_for
+#define hb_subset_input_create_or_fail rive_hb_subset_input_create_or_fail
+#define hb_subset_input_destroy rive_hb_subset_input_destroy
+#define hb_subset_input_get_flags rive_hb_subset_input_get_flags
+#define hb_subset_input_get_user_data rive_hb_subset_input_get_user_data
+#define hb_subset_input_glyph_set rive_hb_subset_input_glyph_set
+#define hb_subset_input_keep_everything rive_hb_subset_input_keep_everything
+#define hb_subset_input_old_to_new_glyph_mapping rive_hb_subset_input_old_to_new_glyph_mapping
+#define hb_subset_input_pin_axis_location rive_hb_subset_input_pin_axis_location
+#define hb_subset_input_pin_axis_to_default rive_hb_subset_input_pin_axis_to_default
+#define hb_subset_input_reference rive_hb_subset_input_reference
+#define hb_subset_input_set rive_hb_subset_input_set
+#define hb_subset_input_set_flags rive_hb_subset_input_set_flags
+#define hb_subset_input_set_user_data rive_hb_subset_input_set_user_data
+#define hb_subset_input_unicode_set rive_hb_subset_input_unicode_set
+#define hb_subset_or_fail rive_hb_subset_or_fail
+#define hb_subset_preprocess rive_hb_subset_preprocess
+#define hb_face_builder_create rive_hb_face_builder_create
+#define hb_subset_plan_create_or_fail rive_hb_subset_plan_create_or_fail
+#define hb_subset_plan_destroy rive_hb_subset_plan_destroy
+#define hb_subset_plan_get_user_data rive_hb_subset_plan_get_user_data
+#define hb_subset_plan_new_to_old_glyph_mapping rive_hb_subset_plan_new_to_old_glyph_mapping
+#define hb_subset_plan_old_to_new_glyph_mapping rive_hb_subset_plan_old_to_new_glyph_mapping
+#define hb_subset_plan_reference rive_hb_subset_plan_reference
+#define hb_subset_plan_set_user_data rive_hb_subset_plan_set_user_data
+#define hb_subset_plan_unicode_to_old_glyph_mapping rive_hb_subset_plan_unicode_to_old_glyph_mapping
+#define hb_debug rive_hb_debug
+#define hb_face_builder_add_table rive_hb_face_builder_add_table
+#define hb_subset_plan_execute_or_fail rive_hb_subset_plan_execute_or_fail
+#define hb_ucd_b4 rive_hb_ucd_b4
+#define hb_ucd_dm rive_hb_ucd_dm
+#define hb_ucd_gc rive_hb_ucd_gc
+#define hb_ucd_sc rive_hb_ucd_sc
+#define hb_ucd_u8 rive_hb_ucd_u8
+#define hb_ucd_bmg rive_hb_ucd_bmg
+#define hb_ucd_ccc rive_hb_ucd_ccc
+#define hb_ucd_i16 rive_hb_ucd_i16
+#define hb_ucd_u16 rive_hb_ucd_u16
+#define hb_ucd_sc_map rive_hb_ucd_sc_map
+#define hb_ucd_dm1_p0_map rive_hb_ucd_dm1_p0_map
+#define hb_ucd_dm1_p2_map rive_hb_ucd_dm1_p2_map
+#define hb_ucd_dm2_u32_map rive_hb_ucd_dm2_u32_map
+#define hb_ucd_dm2_u64_map rive_hb_ucd_dm2_u64_map
+#define hb_ucd_compose_hangul rive_hb_ucd_compose_hangul
+#define hb_ucd_decompose_hangul rive_hb_ucd_decompose_hangul
+#define hb_ucd_get_unicode_funcs rive_hb_ucd_get_unicode_funcs
+#define hb_unicode_funcs_create rive_hb_unicode_funcs_create
+#define hb_unicode_funcs_get_empty rive_hb_unicode_funcs_get_empty
+#define hb_unicode_funcs_make_immutable rive_hb_unicode_funcs_make_immutable
+#define hb_unicode_funcs_set_combining_class_func rive_hb_unicode_funcs_set_combining_class_func
+#define hb_unicode_funcs_set_compose_func rive_hb_unicode_funcs_set_compose_func
+#define hb_unicode_funcs_set_decompose_func rive_hb_unicode_funcs_set_decompose_func
+#define hb_unicode_funcs_set_general_category_func rive_hb_unicode_funcs_set_general_category_func
+#define hb_unicode_funcs_set_mirroring_func rive_hb_unicode_funcs_set_mirroring_func
+#define hb_unicode_funcs_set_script_func rive_hb_unicode_funcs_set_script_func
+#define hb_emoji_b1 rive_hb_emoji_b1
+#define hb_emoji_b4 rive_hb_emoji_b4
+#define hb_emoji_u8 rive_hb_emoji_u8
+#define hb_emoji_is_Extended_Pictographic rive_hb_emoji_is_Extended_Pictographic
+#define hb_unicode_combining_class rive_hb_unicode_combining_class
+#define hb_unicode_compose rive_hb_unicode_compose
+#define hb_unicode_decompose rive_hb_unicode_decompose
+#define hb_unicode_decompose_compatibility rive_hb_unicode_decompose_compatibility
+#define hb_unicode_eastasian_width rive_hb_unicode_eastasian_width
+#define hb_unicode_funcs_get_parent rive_hb_unicode_funcs_get_parent
+#define hb_unicode_funcs_get_user_data rive_hb_unicode_funcs_get_user_data
+#define hb_unicode_funcs_is_immutable rive_hb_unicode_funcs_is_immutable
+#define hb_unicode_funcs_set_decompose_compatibility_func rive_hb_unicode_funcs_set_decompose_compatibility_func
+#define hb_unicode_funcs_set_eastasian_width_func rive_hb_unicode_funcs_set_eastasian_width_func
+#define hb_unicode_funcs_set_user_data rive_hb_unicode_funcs_set_user_data
+#define hb_unicode_general_category rive_hb_unicode_general_category
+#define hb_unicode_mirroring rive_hb_unicode_mirroring
+#define hb_unicode_script rive_hb_unicode_script
+// __hb_*
+#define _hb_CrapPool rive__hb_CrapPool
+#define _hb_NullPool rive__hb_NullPool
+#define _hb_Null_AAT_Lookup rive__hb_Null_AAT_Lookup
+#define _hb_Null_AAT_SettingName rive__hb_Null_AAT_SettingName
+#define _hb_Null_OT_RangeRecord rive__hb_Null_OT_RangeRecord
+#define _hb_Null_hb_buffer_t rive__hb_Null_hb_buffer_t
+#define _hb_Null_hb_unicode_funcs_t rive__hb_Null_hb_unicode_funcs_t
+#define _hb_options rive__hb_options
+#define _hb_Null_hb_draw_funcs_t rive__hb_Null_hb_draw_funcs_t
+#define _hb_Null_OT_CmapSubtableLongGroup rive__hb_Null_OT_CmapSubtableLongGroup
+#define _hb_Null_hb_face_t rive__hb_Null_hb_face_t
+#define _hb_ot_shaper_face_data_destroy rive__hb_ot_shaper_face_data_destroy
+#define _hb_Null_hb_font_funcs_t rive__hb_Null_hb_font_funcs_t
+#define _hb_Null_hb_font_t rive__hb_Null_hb_font_t
+#define _hb_ot_shaper_font_data_destroy rive__hb_ot_shaper_font_data_destroy
+#define _hb_Null_OT_Index rive__hb_Null_OT_Index
+#define _hb_Null_OT_LangSys rive__hb_Null_OT_LangSys
+#define _hb_modified_combining_class rive__hb_modified_combining_class
+#define _hb_ot_shape rive__hb_ot_shape
+#define _hb_ot_shaper_arabic rive__hb_ot_shaper_arabic
+#define _hb_ot_shaper_default rive__hb_ot_shaper_default
+#define _hb_ot_shaper_dumber rive__hb_ot_shaper_dumber
+#define _hb_ot_shaper_face_data_create rive__hb_ot_shaper_face_data_create
+#define _hb_ot_shaper_font_data_create rive__hb_ot_shaper_font_data_create
+#define _hb_ot_shaper_hangul rive__hb_ot_shaper_hangul
+#define _hb_ot_shaper_hebrew rive__hb_ot_shaper_hebrew
+#define _hb_ot_shaper_indic rive__hb_ot_shaper_indic
+#define _hb_ot_shaper_khmer rive__hb_ot_shaper_khmer
+#define _hb_ot_shaper_myanmar rive__hb_ot_shaper_myanmar
+#define _hb_ot_shaper_myanmar_zawgyi rive__hb_ot_shaper_myanmar_zawgyi
+#define _hb_ot_shaper_thai rive__hb_ot_shaper_thai
+#define _hb_ot_shaper_use rive__hb_ot_shaper_use
+#define _hb_Null_hb_paint_funcs_t rive__hb_Null_hb_paint_funcs_t
+#define _hb_Null_OT_ClipRecord rive__hb_Null_OT_ClipRecord
+#define _hb_Null_OT_VarIdx rive__hb_Null_OT_VarIdx
+#define _hb_subset_accelerator_user_data_key rive__hb_subset_accelerator_user_data_key
+#define lookup_expert_subset_charset_for_sid rive_lookup_expert_subset_charset_for_sid
+#define lookup_standard_encoding_for_sid rive_lookup_standard_encoding_for_sid
+#define data_destroy_arabic rive_data_destroy_arabic
+#define lookup_expert_charset_for_sid rive_lookup_expert_charset_for_sid
+#define lookup_standard_encoding_for_code rive_lookup_standard_encoding_for_code
+#define accelerator_t rive_accelerator_t
+#define lookup_expert_encoding_for_code rive_lookup_expert_encoding_for_code
+#define get_seac_components rive_get_seac_components
diff --git a/dependencies/rive_libjpeg_renames.h b/dependencies/rive_libjpeg_renames.h
new file mode 100644
index 0000000..9e1ad74
--- /dev/null
+++ b/dependencies/rive_libjpeg_renames.h
@@ -0,0 +1,204 @@
+// clang-format off
+// jpeg_*
+#define jpeg_aritab rive_jpeg_aritab
+#define jpeg_CreateCompress rive_jpeg_CreateCompress
+#define jpeg_abort rive_jpeg_abort
+#define jpeg_abort_compress rive_jpeg_abort_compress
+#define jpeg_destroy rive_jpeg_destroy
+#define jpeg_destroy_compress rive_jpeg_destroy_compress
+#define jpeg_finish_compress rive_jpeg_finish_compress
+#define jpeg_natural_order rive_jpeg_natural_order
+#define jpeg_suppress_tables rive_jpeg_suppress_tables
+#define jpeg_write_m_byte rive_jpeg_write_m_byte
+#define jpeg_write_m_header rive_jpeg_write_m_header
+#define jpeg_write_marker rive_jpeg_write_marker
+#define jpeg_write_tables rive_jpeg_write_tables
+#define jpeg_start_compress rive_jpeg_start_compress
+#define jpeg_write_raw_data rive_jpeg_write_raw_data
+#define jpeg_write_scanlines rive_jpeg_write_scanlines
+#define jpeg_fdct_10x10 rive_jpeg_fdct_10x10
+#define jpeg_fdct_10x5 rive_jpeg_fdct_10x5
+#define jpeg_fdct_11x11 rive_jpeg_fdct_11x11
+#define jpeg_fdct_12x12 rive_jpeg_fdct_12x12
+#define jpeg_fdct_12x6 rive_jpeg_fdct_12x6
+#define jpeg_fdct_13x13 rive_jpeg_fdct_13x13
+#define jpeg_fdct_14x14 rive_jpeg_fdct_14x14
+#define jpeg_fdct_14x7 rive_jpeg_fdct_14x7
+#define jpeg_fdct_15x15 rive_jpeg_fdct_15x15
+#define jpeg_fdct_16x16 rive_jpeg_fdct_16x16
+#define jpeg_fdct_16x8 rive_jpeg_fdct_16x8
+#define jpeg_fdct_1x1 rive_jpeg_fdct_1x1
+#define jpeg_fdct_1x2 rive_jpeg_fdct_1x2
+#define jpeg_fdct_2x1 rive_jpeg_fdct_2x1
+#define jpeg_fdct_2x2 rive_jpeg_fdct_2x2
+#define jpeg_fdct_2x4 rive_jpeg_fdct_2x4
+#define jpeg_fdct_3x3 rive_jpeg_fdct_3x3
+#define jpeg_fdct_3x6 rive_jpeg_fdct_3x6
+#define jpeg_fdct_4x2 rive_jpeg_fdct_4x2
+#define jpeg_fdct_4x4 rive_jpeg_fdct_4x4
+#define jpeg_fdct_4x8 rive_jpeg_fdct_4x8
+#define jpeg_fdct_5x10 rive_jpeg_fdct_5x10
+#define jpeg_fdct_5x5 rive_jpeg_fdct_5x5
+#define jpeg_fdct_6x12 rive_jpeg_fdct_6x12
+#define jpeg_fdct_6x3 rive_jpeg_fdct_6x3
+#define jpeg_fdct_6x6 rive_jpeg_fdct_6x6
+#define jpeg_fdct_7x14 rive_jpeg_fdct_7x14
+#define jpeg_fdct_7x7 rive_jpeg_fdct_7x7
+#define jpeg_fdct_8x16 rive_jpeg_fdct_8x16
+#define jpeg_fdct_8x4 rive_jpeg_fdct_8x4
+#define jpeg_fdct_9x9 rive_jpeg_fdct_9x9
+#define jpeg_fdct_float rive_jpeg_fdct_float
+#define jpeg_fdct_ifast rive_jpeg_fdct_ifast
+#define jpeg_fdct_islow rive_jpeg_fdct_islow
+#define jpeg_alloc_huff_table rive_jpeg_alloc_huff_table
+#define jpeg_gen_optimal_table rive_jpeg_gen_optimal_table
+#define jpeg_make_c_derived_tbl rive_jpeg_make_c_derived_tbl
+#define jpeg_std_huff_table rive_jpeg_std_huff_table
+#define jpeg_calc_jpeg_dimensions rive_jpeg_calc_jpeg_dimensions
+#define jpeg_natural_order2 rive_jpeg_natural_order2
+#define jpeg_natural_order3 rive_jpeg_natural_order3
+#define jpeg_natural_order4 rive_jpeg_natural_order4
+#define jpeg_natural_order5 rive_jpeg_natural_order5
+#define jpeg_natural_order6 rive_jpeg_natural_order6
+#define jpeg_natural_order7 rive_jpeg_natural_order7
+#define jpeg_alloc_quant_table rive_jpeg_alloc_quant_table
+#define jpeg_add_quant_table rive_jpeg_add_quant_table
+#define jpeg_default_colorspace rive_jpeg_default_colorspace
+#define jpeg_default_qtables rive_jpeg_default_qtables
+#define jpeg_quality_scaling rive_jpeg_quality_scaling
+#define jpeg_set_colorspace rive_jpeg_set_colorspace
+#define jpeg_set_defaults rive_jpeg_set_defaults
+#define jpeg_set_linear_quality rive_jpeg_set_linear_quality
+#define jpeg_set_quality rive_jpeg_set_quality
+#define jpeg_simple_progression rive_jpeg_simple_progression
+#define jpeg_copy_critical_parameters rive_jpeg_copy_critical_parameters
+#define jpeg_write_coefficients rive_jpeg_write_coefficients
+#define jpeg_CreateDecompress rive_jpeg_CreateDecompress
+#define jpeg_abort_decompress rive_jpeg_abort_decompress
+#define jpeg_consume_input rive_jpeg_consume_input
+#define jpeg_destroy_decompress rive_jpeg_destroy_decompress
+#define jpeg_finish_decompress rive_jpeg_finish_decompress
+#define jpeg_has_multiple_scans rive_jpeg_has_multiple_scans
+#define jpeg_input_complete rive_jpeg_input_complete
+#define jpeg_read_header rive_jpeg_read_header
+#define jpeg_finish_output rive_jpeg_finish_output
+#define jpeg_read_raw_data rive_jpeg_read_raw_data
+#define jpeg_read_scanlines rive_jpeg_read_scanlines
+#define jpeg_start_decompress rive_jpeg_start_decompress
+#define jpeg_start_output rive_jpeg_start_output
+#define jpeg_mem_dest rive_jpeg_mem_dest
+#define jpeg_stdio_dest rive_jpeg_stdio_dest
+#define jpeg_mem_src rive_jpeg_mem_src
+#define jpeg_resync_to_restart rive_jpeg_resync_to_restart
+#define jpeg_stdio_src rive_jpeg_stdio_src
+#define jpeg_idct_10x10 rive_jpeg_idct_10x10
+#define jpeg_idct_10x5 rive_jpeg_idct_10x5
+#define jpeg_idct_11x11 rive_jpeg_idct_11x11
+#define jpeg_idct_12x12 rive_jpeg_idct_12x12
+#define jpeg_idct_12x6 rive_jpeg_idct_12x6
+#define jpeg_idct_13x13 rive_jpeg_idct_13x13
+#define jpeg_idct_14x14 rive_jpeg_idct_14x14
+#define jpeg_idct_14x7 rive_jpeg_idct_14x7
+#define jpeg_idct_15x15 rive_jpeg_idct_15x15
+#define jpeg_idct_16x16 rive_jpeg_idct_16x16
+#define jpeg_idct_16x8 rive_jpeg_idct_16x8
+#define jpeg_idct_1x1 rive_jpeg_idct_1x1
+#define jpeg_idct_1x2 rive_jpeg_idct_1x2
+#define jpeg_idct_2x1 rive_jpeg_idct_2x1
+#define jpeg_idct_2x2 rive_jpeg_idct_2x2
+#define jpeg_idct_2x4 rive_jpeg_idct_2x4
+#define jpeg_idct_3x3 rive_jpeg_idct_3x3
+#define jpeg_idct_3x6 rive_jpeg_idct_3x6
+#define jpeg_idct_4x2 rive_jpeg_idct_4x2
+#define jpeg_idct_4x4 rive_jpeg_idct_4x4
+#define jpeg_idct_4x8 rive_jpeg_idct_4x8
+#define jpeg_idct_5x10 rive_jpeg_idct_5x10
+#define jpeg_idct_5x5 rive_jpeg_idct_5x5
+#define jpeg_idct_6x12 rive_jpeg_idct_6x12
+#define jpeg_idct_6x3 rive_jpeg_idct_6x3
+#define jpeg_idct_6x6 rive_jpeg_idct_6x6
+#define jpeg_idct_7x14 rive_jpeg_idct_7x14
+#define jpeg_idct_7x7 rive_jpeg_idct_7x7
+#define jpeg_idct_8x16 rive_jpeg_idct_8x16
+#define jpeg_idct_8x4 rive_jpeg_idct_8x4
+#define jpeg_idct_9x9 rive_jpeg_idct_9x9
+#define jpeg_idct_float rive_jpeg_idct_float
+#define jpeg_idct_ifast rive_jpeg_idct_ifast
+#define jpeg_idct_islow rive_jpeg_idct_islow
+#define jpeg_fill_bit_buffer rive_jpeg_fill_bit_buffer
+#define jpeg_huff_decode rive_jpeg_huff_decode
+#define jpeg_make_d_derived_tbl rive_jpeg_make_d_derived_tbl
+#define jpeg_zigzag_order rive_jpeg_zigzag_order
+#define jpeg_zigzag_order2 rive_jpeg_zigzag_order2
+#define jpeg_zigzag_order3 rive_jpeg_zigzag_order3
+#define jpeg_zigzag_order4 rive_jpeg_zigzag_order4
+#define jpeg_zigzag_order5 rive_jpeg_zigzag_order5
+#define jpeg_zigzag_order6 rive_jpeg_zigzag_order6
+#define jpeg_zigzag_order7 rive_jpeg_zigzag_order7
+#define jpeg_core_output_dimensions rive_jpeg_core_output_dimensions
+#define jpeg_save_markers rive_jpeg_save_markers
+#define jpeg_set_marker_processor rive_jpeg_set_marker_processor
+#define jpeg_calc_output_dimensions rive_jpeg_calc_output_dimensions
+#define jpeg_new_colormap rive_jpeg_new_colormap
+#define jpeg_read_coefficients rive_jpeg_read_coefficients
+#define jpeg_std_error rive_jpeg_std_error
+#define jpeg_std_message_table rive_jpeg_std_message_table
+#define jpeg_free_large rive_jpeg_free_large
+#define jpeg_free_small rive_jpeg_free_small
+#define jpeg_get_large rive_jpeg_get_large
+#define jpeg_get_small rive_jpeg_get_small
+#define jpeg_mem_available rive_jpeg_mem_available
+#define jpeg_mem_init rive_jpeg_mem_init
+#define jpeg_mem_term rive_jpeg_mem_term
+#define jpeg_open_backing_store rive_jpeg_open_backing_store
+// jinit_*
+#define jinit_marker_writer rive_jinit_marker_writer
+#define jinit_memory_mgr rive_jinit_memory_mgr
+#define jinit_compress_master rive_jinit_compress_master
+#define jinit_arith_encoder rive_jinit_arith_encoder
+#define jinit_c_coef_controller rive_jinit_c_coef_controller
+#define jinit_color_converter rive_jinit_color_converter
+#define jinit_forward_dct rive_jinit_forward_dct
+#define jinit_huff_encoder rive_jinit_huff_encoder
+#define jinit_c_main_controller rive_jinit_c_main_controller
+#define jinit_c_master_control rive_jinit_c_master_control
+#define jinit_c_prep_controller rive_jinit_c_prep_controller
+#define jinit_downsampler rive_jinit_downsampler
+#define jinit_input_controller rive_jinit_input_controller
+#define jinit_marker_reader rive_jinit_marker_reader
+#define jinit_master_decompress rive_jinit_master_decompress
+#define jinit_arith_decoder rive_jinit_arith_decoder
+#define jinit_d_coef_controller rive_jinit_d_coef_controller
+#define jinit_color_deconverter rive_jinit_color_deconverter
+#define jinit_inverse_dct rive_jinit_inverse_dct
+#define jinit_huff_decoder rive_jinit_huff_decoder
+#define jinit_d_main_controller rive_jinit_d_main_controller
+#define jinit_1pass_quantizer rive_jinit_1pass_quantizer
+#define jinit_2pass_quantizer rive_jinit_2pass_quantizer
+#define jinit_d_post_controller rive_jinit_d_post_controller
+#define jinit_merged_upsampler rive_jinit_merged_upsampler
+#define jinit_upsampler rive_jinit_upsampler
+// _j extras
+#define jtransform_execute_transform rive_jtransform_execute_transform
+#define read_scan_script rive_read_scan_script
+#define jcopy_markers_execute rive_jcopy_markers_execute
+#define jcopy_markers_setup rive_jcopy_markers_setup
+#define enable_signal_catcher rive_enable_signal_catcher
+#define set_quant_slots rive_set_quant_slots
+#define read_stdin rive_read_stdin
+#define set_quality_ratings rive_set_quality_ratings
+#define write_stdout rive_write_stdout
+#define set_sample_factors rive_set_sample_factors
+#define jtransform_perfect_transform rive_jtransform_perfect_transform
+#define jcopy_sample_rows rive_jcopy_sample_rows
+#define jdiv_round_up rive_jdiv_round_up
+#define jtransform_request_workspace rive_jtransform_request_workspace
+#define jcopy_block_row rive_jcopy_block_row
+#define end_progress_monitor rive_end_progress_monitor
+#define read_quant_tables rive_read_quant_tables
+#define jzero_far rive_jzero_far
+#define read_color_map rive_read_color_map
+#define jtransform_adjust_parameters rive_jtransform_adjust_parameters
+#define jround_up rive_jround_up
+#define start_progress_monitor rive_start_progress_monitor
+#define jtransform_parse_crop_spec rive_jtransform_parse_crop_spec
diff --git a/dependencies/rive_png_renames.h b/dependencies/rive_png_renames.h
new file mode 100644
index 0000000..5122b8c
--- /dev/null
+++ b/dependencies/rive_png_renames.h
@@ -0,0 +1,452 @@
+#ifndef RIVE_PNG_RENAMES_H
+#define RIVE_PNG_RENAMES_H
+#define PNGPREFIX_H
+#define PNG_PREFIX rive_
+#define png_image_write_to_memory rive_png_image_write_to_memory
+#define png_check_keyword rive_png_check_keyword
+#define png_sRGB_table rive_png_sRGB_table
+#define png_sRGB_base rive_png_sRGB_base
+#define png_sRGB_delta rive_png_sRGB_delta
+#define png_zstream_error rive_png_zstream_error
+#define png_free_buffer_list rive_png_free_buffer_list
+#define png_fixed rive_png_fixed
+#define png_user_version_check rive_png_user_version_check
+#define png_malloc_base rive_png_malloc_base
+#define png_malloc_array rive_png_malloc_array
+#define png_realloc_array rive_png_realloc_array
+#define png_create_png_struct rive_png_create_png_struct
+#define png_destroy_png_struct rive_png_destroy_png_struct
+#define png_free_jmpbuf rive_png_free_jmpbuf
+#define png_zalloc rive_png_zalloc
+#define png_zfree rive_png_zfree
+#define png_default_read_data rive_png_default_read_data
+#define png_push_fill_buffer rive_png_push_fill_buffer
+#define png_default_write_data rive_png_default_write_data
+#define png_default_flush rive_png_default_flush
+#define png_reset_crc rive_png_reset_crc
+#define png_write_data rive_png_write_data
+#define png_read_sig rive_png_read_sig
+#define png_read_chunk_header rive_png_read_chunk_header
+#define png_read_data rive_png_read_data
+#define png_crc_read rive_png_crc_read
+#define png_crc_finish rive_png_crc_finish
+#define png_crc_error rive_png_crc_error
+#define png_calculate_crc rive_png_calculate_crc
+#define png_flush rive_png_flush
+#define png_write_IHDR rive_png_write_IHDR
+#define png_write_PLTE rive_png_write_PLTE
+#define png_compress_IDAT rive_png_compress_IDAT
+#define png_write_IEND rive_png_write_IEND
+#define png_write_gAMA_fixed rive_png_write_gAMA_fixed
+#define png_write_sBIT rive_png_write_sBIT
+#define png_write_cHRM_fixed rive_png_write_cHRM_fixed
+#define png_write_sRGB rive_png_write_sRGB
+#define png_write_iCCP rive_png_write_iCCP
+#define png_write_sPLT rive_png_write_sPLT
+#define png_write_tRNS rive_png_write_tRNS
+#define png_write_bKGD rive_png_write_bKGD
+#define png_write_hIST rive_png_write_hIST
+#define png_write_tEXt rive_png_write_tEXt
+#define png_write_zTXt rive_png_write_zTXt
+#define png_write_iTXt rive_png_write_iTXt
+#define png_set_text_2 rive_png_set_text_2
+#define png_write_oFFs rive_png_write_oFFs
+#define png_write_pCAL rive_png_write_pCAL
+#define png_write_pHYs rive_png_write_pHYs
+#define png_write_tIME rive_png_write_tIME
+#define png_write_sCAL_s rive_png_write_sCAL_s
+#define png_write_finish_row rive_png_write_finish_row
+#define png_write_start_row rive_png_write_start_row
+#define png_combine_row rive_png_combine_row
+#define png_do_read_interlace rive_png_do_read_interlace
+#define png_do_write_interlace rive_png_do_write_interlace
+#define png_read_filter_row rive_png_read_filter_row
+#define png_read_filter_row_up_neon rive_png_read_filter_row_up_neon
+#define png_read_filter_row_sub3_neon rive_png_read_filter_row_sub3_neon
+#define png_read_filter_row_sub4_neon rive_png_read_filter_row_sub4_neon
+#define png_read_filter_row_avg3_neon rive_png_read_filter_row_avg3_neon
+#define png_read_filter_row_avg4_neon rive_png_read_filter_row_avg4_neon
+#define png_read_filter_row_paeth3_neon rive_png_read_filter_row_paeth3_neon
+#define png_read_filter_row_paeth4_neon rive_png_read_filter_row_paeth4_neon
+#define png_read_filter_row_sub3_sse2 rive_png_read_filter_row_sub3_sse2
+#define png_read_filter_row_sub4_sse2 rive_png_read_filter_row_sub4_sse2
+#define png_read_filter_row_avg3_sse2 rive_png_read_filter_row_avg3_sse2
+#define png_read_filter_row_avg4_sse2 rive_png_read_filter_row_avg4_sse2
+#define png_read_filter_row_paeth3_sse2 rive_png_read_filter_row_paeth3_sse2
+#define png_read_filter_row_paeth4_sse2 rive_png_read_filter_row_paeth4_sse2
+#define png_write_find_filter rive_png_write_find_filter
+#define png_read_IDAT_data rive_png_read_IDAT_data
+#define png_read_finish_IDAT rive_png_read_finish_IDAT
+#define png_read_finish_row rive_png_read_finish_row
+#define png_read_start_row rive_png_read_start_row
+#define png_read_transform_info rive_png_read_transform_info
+#define png_do_read_filler rive_png_do_read_filler
+#define png_do_read_swap_alpha rive_png_do_read_swap_alpha
+#define png_do_write_swap_alpha rive_png_do_write_swap_alpha
+#define png_do_read_invert_alpha rive_png_do_read_invert_alpha
+#define png_do_write_invert_alpha rive_png_do_write_invert_alpha
+#define png_do_strip_channel rive_png_do_strip_channel
+#define png_do_swap rive_png_do_swap
+#define png_do_packswap rive_png_do_packswap
+#define png_do_rgb_to_gray rive_png_do_rgb_to_gray
+#define png_do_gray_to_rgb rive_png_do_gray_to_rgb
+#define png_do_unpack rive_png_do_unpack
+#define png_do_unshift rive_png_do_unshift
+#define png_do_invert rive_png_do_invert
+#define png_do_scale_16_to_8 rive_png_do_scale_16_to_8
+#define png_do_chop rive_png_do_chop
+#define png_do_quantize rive_png_do_quantize
+#define png_do_bgr rive_png_do_bgr
+#define png_do_pack rive_png_do_pack
+#define png_do_shift rive_png_do_shift
+#define png_do_compose rive_png_do_compose
+#define png_do_gamma rive_png_do_gamma
+#define png_do_encode_alpha rive_png_do_encode_alpha
+#define png_do_expand_palette rive_png_do_expand_palette
+#define png_do_expand rive_png_do_expand
+#define png_do_expand_16 rive_png_do_expand_16
+#define png_handle_IHDR rive_png_handle_IHDR
+#define png_handle_PLTE rive_png_handle_PLTE
+#define png_handle_IEND rive_png_handle_IEND
+#define png_handle_bKGD rive_png_handle_bKGD
+#define png_handle_cHRM rive_png_handle_cHRM
+#define png_handle_gAMA rive_png_handle_gAMA
+#define png_handle_hIST rive_png_handle_hIST
+#define png_handle_iCCP rive_png_handle_iCCP
+#define png_handle_iTXt rive_png_handle_iTXt
+#define png_handle_oFFs rive_png_handle_oFFs
+#define png_handle_pCAL rive_png_handle_pCAL
+#define png_handle_pHYs rive_png_handle_pHYs
+#define png_handle_sBIT rive_png_handle_sBIT
+#define png_handle_sCAL rive_png_handle_sCAL
+#define png_handle_sPLT rive_png_handle_sPLT
+#define png_handle_sRGB rive_png_handle_sRGB
+#define png_handle_tEXt rive_png_handle_tEXt
+#define png_handle_tIME rive_png_handle_tIME
+#define png_handle_tRNS rive_png_handle_tRNS
+#define png_handle_zTXt rive_png_handle_zTXt
+#define png_check_chunk_name rive_png_check_chunk_name
+#define png_handle_unknown rive_png_handle_unknown
+#define png_chunk_unknown_handling rive_png_chunk_unknown_handling
+#define png_do_read_transformations rive_png_do_read_transformations
+#define png_do_write_transformations rive_png_do_write_transformations
+#define png_init_read_transformations rive_png_init_read_transformations
+#define png_push_read_chunk rive_png_push_read_chunk
+#define png_push_read_sig rive_png_push_read_sig
+#define png_push_check_crc rive_png_push_check_crc
+#define png_push_crc_skip rive_png_push_crc_skip
+#define png_push_crc_finish rive_png_push_crc_finish
+#define png_push_save_buffer rive_png_push_save_buffer
+#define png_push_restore_buffer rive_png_push_restore_buffer
+#define png_push_read_IDAT rive_png_push_read_IDAT
+#define png_process_IDAT_data rive_png_process_IDAT_data
+#define png_push_process_row rive_png_push_process_row
+#define png_push_handle_unknown rive_png_push_handle_unknown
+#define png_push_have_info rive_png_push_have_info
+#define png_push_have_end rive_png_push_have_end
+#define png_push_have_row rive_png_push_have_row
+#define png_push_read_end rive_png_push_read_end
+#define png_process_some_data rive_png_process_some_data
+#define png_read_push_finish_row rive_png_read_push_finish_row
+#define png_push_handle_tEXt rive_png_push_handle_tEXt
+#define png_push_read_tEXt rive_png_push_read_tEXt
+#define png_push_handle_zTXt rive_png_push_handle_zTXt
+#define png_push_read_zTXt rive_png_push_read_zTXt
+#define png_push_handle_iTXt rive_png_push_handle_iTXt
+#define png_push_read_iTXt rive_png_push_read_iTXt
+#define png_do_read_intrapixel rive_png_do_read_intrapixel
+#define png_do_write_intrapixel rive_png_do_write_intrapixel
+#define png_colorspace_set_gamma rive_png_colorspace_set_gamma
+#define png_colorspace_sync_info rive_png_colorspace_sync_info
+#define png_colorspace_sync rive_png_colorspace_sync
+#define png_colorspace_set_chromaticities rive_png_colorspace_set_chromaticities
+#define png_colorspace_set_endpoints rive_png_colorspace_set_endpoints
+#define png_colorspace_set_sRGB rive_png_colorspace_set_sRGB
+#define png_colorspace_set_ICC rive_png_colorspace_set_ICC
+#define png_icc_check_length rive_png_icc_check_length
+#define png_icc_check_header rive_png_icc_check_header
+#define png_icc_check_tag_table rive_png_icc_check_tag_table
+#define png_icc_set_sRGB rive_png_icc_set_sRGB
+#define png_colorspace_set_rgb_coefficients rive_png_colorspace_set_rgb_coefficients
+#define png_check_IHDR rive_png_check_IHDR
+#define png_do_check_palette_indexes rive_png_do_check_palette_indexes
+#define png_fixed_error rive_png_fixed_error
+#define png_safecat rive_png_safecat
+#define png_format_number rive_png_format_number
+#define png_warning_parameter rive_png_warning_parameter
+#define png_warning_parameter_unsigned rive_png_warning_parameter_unsigned
+#define png_warning_parameter_signed rive_png_warning_parameter_signed
+#define png_formatted_warning rive_png_formatted_warning
+#define png_app_warning rive_png_app_warning
+#define png_app_error rive_png_app_error
+#define png_chunk_report rive_png_chunk_report
+#define png_ascii_from_fp rive_png_ascii_from_fp
+#define png_ascii_from_fixed rive_png_ascii_from_fixed
+#define png_check_fp_number rive_png_check_fp_number
+#define png_check_fp_string rive_png_check_fp_string
+#define png_muldiv rive_png_muldiv
+#define png_muldiv_warn rive_png_muldiv_warn
+#define png_reciprocal rive_png_reciprocal
+#define png_reciprocal2 rive_png_reciprocal2
+#define png_gamma_significant rive_png_gamma_significant
+#define png_gamma_correct rive_png_gamma_correct
+#define png_gamma_16bit_correct rive_png_gamma_16bit_correct
+#define png_gamma_8bit_correct rive_png_gamma_8bit_correct
+#define png_destroy_gamma_table rive_png_destroy_gamma_table
+#define png_build_gamma_table rive_png_build_gamma_table
+#define png_safe_error rive_png_safe_error
+#define png_safe_warning rive_png_safe_warning
+#define png_safe_execute rive_png_safe_execute
+#define png_image_error rive_png_image_error
+#define png_access_version_number rive_png_access_version_number
+#define png_build_grayscale_palette rive_png_build_grayscale_palette
+#define png_convert_to_rfc1123 rive_png_convert_to_rfc1123
+#define png_convert_to_rfc1123_buffer rive_png_convert_to_rfc1123_buffer
+#define png_create_info_struct rive_png_create_info_struct
+#define png_data_freer rive_png_data_freer
+#define png_destroy_info_struct rive_png_destroy_info_struct
+#define png_free_data rive_png_free_data
+#define png_get_copyright rive_png_get_copyright
+#define png_get_header_ver rive_png_get_header_ver
+#define png_get_header_version rive_png_get_header_version
+#define png_get_io_ptr rive_png_get_io_ptr
+#define png_get_libpng_ver rive_png_get_libpng_ver
+#define png_handle_as_unknown rive_png_handle_as_unknown
+#define png_image_free rive_png_image_free
+#define png_info_init_3 rive_png_info_init_3
+#define png_init_io rive_png_init_io
+#define png_reset_zstream rive_png_reset_zstream
+#define png_save_int_32 rive_png_save_int_32
+#define png_set_option rive_png_set_option
+#define png_set_sig_bytes rive_png_set_sig_bytes
+#define png_sig_cmp rive_png_sig_cmp
+#define png_benign_error rive_png_benign_error
+#define png_chunk_benign_error rive_png_chunk_benign_error
+#define png_chunk_error rive_png_chunk_error
+#define png_chunk_warning rive_png_chunk_warning
+#define png_error rive_png_error
+#define png_get_error_ptr rive_png_get_error_ptr
+#define png_longjmp rive_png_longjmp
+#define png_set_error_fn rive_png_set_error_fn
+#define png_set_longjmp_fn rive_png_set_longjmp_fn
+#define png_warning rive_png_warning
+#define png_get_bit_depth rive_png_get_bit_depth
+#define png_get_bKGD rive_png_get_bKGD
+#define png_get_channels rive_png_get_channels
+#define png_get_cHRM rive_png_get_cHRM
+#define png_get_cHRM_fixed rive_png_get_cHRM_fixed
+#define png_get_cHRM_XYZ rive_png_get_cHRM_XYZ
+#define png_get_cHRM_XYZ_fixed rive_png_get_cHRM_XYZ_fixed
+#define png_get_chunk_cache_max rive_png_get_chunk_cache_max
+#define png_get_chunk_malloc_max rive_png_get_chunk_malloc_max
+#define png_get_color_type rive_png_get_color_type
+#define png_get_compression_buffer_size rive_png_get_compression_buffer_size
+#define png_get_compression_type rive_png_get_compression_type
+#define png_get_filter_type rive_png_get_filter_type
+#define png_get_gAMA rive_png_get_gAMA
+#define png_get_gAMA_fixed rive_png_get_gAMA_fixed
+#define png_get_hIST rive_png_get_hIST
+#define png_get_iCCP rive_png_get_iCCP
+#define png_get_IHDR rive_png_get_IHDR
+#define png_get_image_height rive_png_get_image_height
+#define png_get_image_width rive_png_get_image_width
+#define png_get_interlace_type rive_png_get_interlace_type
+#define png_get_io_chunk_type rive_png_get_io_chunk_type
+#define png_get_io_state rive_png_get_io_state
+#define png_get_oFFs rive_png_get_oFFs
+#define png_get_palette_max rive_png_get_palette_max
+#define png_get_pCAL rive_png_get_pCAL
+#define png_get_pHYs rive_png_get_pHYs
+#define png_get_pHYs_dpi rive_png_get_pHYs_dpi
+#define png_get_pixel_aspect_ratio rive_png_get_pixel_aspect_ratio
+#define png_get_pixel_aspect_ratio_fixed rive_png_get_pixel_aspect_ratio_fixed
+#define png_get_pixels_per_inch rive_png_get_pixels_per_inch
+#define png_get_pixels_per_meter rive_png_get_pixels_per_meter
+#define png_get_PLTE rive_png_get_PLTE
+#define png_get_rgb_to_gray_status rive_png_get_rgb_to_gray_status
+#define png_get_rowbytes rive_png_get_rowbytes
+#define png_get_rows rive_png_get_rows
+#define png_get_sBIT rive_png_get_sBIT
+#define png_get_sCAL rive_png_get_sCAL
+#define png_get_sCAL_fixed rive_png_get_sCAL_fixed
+#define png_get_sCAL_s rive_png_get_sCAL_s
+#define png_get_signature rive_png_get_signature
+#define png_get_sPLT rive_png_get_sPLT
+#define png_get_sRGB rive_png_get_sRGB
+#define png_get_text rive_png_get_text
+#define png_get_tIME rive_png_get_tIME
+#define png_get_tRNS rive_png_get_tRNS
+#define png_get_unknown_chunks rive_png_get_unknown_chunks
+#define png_get_user_chunk_ptr rive_png_get_user_chunk_ptr
+#define png_get_user_height_max rive_png_get_user_height_max
+#define png_get_user_width_max rive_png_get_user_width_max
+#define png_get_valid rive_png_get_valid
+#define png_get_x_offset_inches rive_png_get_x_offset_inches
+#define png_get_x_offset_inches_fixed rive_png_get_x_offset_inches_fixed
+#define png_get_x_offset_microns rive_png_get_x_offset_microns
+#define png_get_x_offset_pixels rive_png_get_x_offset_pixels
+#define png_get_x_pixels_per_inch rive_png_get_x_pixels_per_inch
+#define png_get_x_pixels_per_meter rive_png_get_x_pixels_per_meter
+#define png_get_y_offset_inches rive_png_get_y_offset_inches
+#define png_get_y_offset_inches_fixed rive_png_get_y_offset_inches_fixed
+#define png_get_y_offset_microns rive_png_get_y_offset_microns
+#define png_get_y_offset_pixels rive_png_get_y_offset_pixels
+#define png_get_y_pixels_per_inch rive_png_get_y_pixels_per_inch
+#define png_get_y_pixels_per_meter rive_png_get_y_pixels_per_meter
+#define png_calloc rive_png_calloc
+#define png_free rive_png_free
+#define png_free_default rive_png_free_default
+#define png_get_mem_ptr rive_png_get_mem_ptr
+#define png_malloc rive_png_malloc
+#define png_malloc_default rive_png_malloc_default
+#define png_malloc_warn rive_png_malloc_warn
+#define png_set_mem_fn rive_png_set_mem_fn
+#define png_get_progressive_ptr rive_png_get_progressive_ptr
+#define png_process_data rive_png_process_data
+#define png_process_data_pause rive_png_process_data_pause
+#define png_process_data_skip rive_png_process_data_skip
+#define png_progressive_combine_row rive_png_progressive_combine_row
+#define png_set_progressive_read_fn rive_png_set_progressive_read_fn
+#define png_create_read_struct rive_png_create_read_struct
+#define png_create_read_struct_2 rive_png_create_read_struct_2
+#define png_destroy_read_struct rive_png_destroy_read_struct
+#define png_image_begin_read_from_file rive_png_image_begin_read_from_file
+#define png_image_begin_read_from_memory rive_png_image_begin_read_from_memory
+#define png_image_begin_read_from_stdio rive_png_image_begin_read_from_stdio
+#define png_image_finish_read rive_png_image_finish_read
+#define png_read_end rive_png_read_end
+#define png_read_image rive_png_read_image
+#define png_read_info rive_png_read_info
+#define png_read_png rive_png_read_png
+#define png_read_row rive_png_read_row
+#define png_read_rows rive_png_read_rows
+#define png_read_update_info rive_png_read_update_info
+#define png_set_read_status_fn rive_png_set_read_status_fn
+#define png_start_read_image rive_png_start_read_image
+#define png_set_read_fn rive_png_set_read_fn
+#define png_set_alpha_mode rive_png_set_alpha_mode
+#define png_set_alpha_mode_fixed rive_png_set_alpha_mode_fixed
+#define png_set_background rive_png_set_background
+#define png_set_background_fixed rive_png_set_background_fixed
+#define png_set_crc_action rive_png_set_crc_action
+#define png_set_expand rive_png_set_expand
+#define png_set_expand_16 rive_png_set_expand_16
+#define png_set_expand_gray_1_2_4_to_8 rive_png_set_expand_gray_1_2_4_to_8
+#define png_set_gamma rive_png_set_gamma
+#define png_set_gamma_fixed rive_png_set_gamma_fixed
+#define png_set_gray_to_rgb rive_png_set_gray_to_rgb
+#define png_set_palette_to_rgb rive_png_set_palette_to_rgb
+#define png_set_quantize rive_png_set_quantize
+#define png_set_read_user_transform_fn rive_png_set_read_user_transform_fn
+#define png_set_rgb_to_gray rive_png_set_rgb_to_gray
+#define png_set_rgb_to_gray_fixed rive_png_set_rgb_to_gray_fixed
+#define png_set_scale_16 rive_png_set_scale_16
+#define png_set_strip_16 rive_png_set_strip_16
+#define png_set_strip_alpha rive_png_set_strip_alpha
+#define png_set_tRNS_to_alpha rive_png_set_tRNS_to_alpha
+#define png_get_int_32 rive_png_get_int_32
+#define png_get_uint_16 rive_png_get_uint_16
+#define png_get_uint_31 rive_png_get_uint_31
+#define png_get_uint_32 rive_png_get_uint_32
+#define png_permit_mng_features rive_png_permit_mng_features
+#define png_set_benign_errors rive_png_set_benign_errors
+#define png_set_bKGD rive_png_set_bKGD
+#define png_set_check_for_invalid_index rive_png_set_check_for_invalid_index
+#define png_set_cHRM rive_png_set_cHRM
+#define png_set_cHRM_fixed rive_png_set_cHRM_fixed
+#define png_set_cHRM_XYZ rive_png_set_cHRM_XYZ
+#define png_set_cHRM_XYZ_fixed rive_png_set_cHRM_XYZ_fixed
+#define png_set_chunk_cache_max rive_png_set_chunk_cache_max
+#define png_set_chunk_malloc_max rive_png_set_chunk_malloc_max
+#define png_set_compression_buffer_size rive_png_set_compression_buffer_size
+#define png_set_gAMA rive_png_set_gAMA
+#define png_set_gAMA_fixed rive_png_set_gAMA_fixed
+#define png_set_hIST rive_png_set_hIST
+#define png_set_iCCP rive_png_set_iCCP
+#define png_set_IHDR rive_png_set_IHDR
+#define png_set_invalid rive_png_set_invalid
+#define png_set_keep_unknown_chunks rive_png_set_keep_unknown_chunks
+#define png_set_oFFs rive_png_set_oFFs
+#define png_set_pCAL rive_png_set_pCAL
+#define png_set_pHYs rive_png_set_pHYs
+#define png_set_PLTE rive_png_set_PLTE
+#define png_set_read_user_chunk_fn rive_png_set_read_user_chunk_fn
+#define png_set_rows rive_png_set_rows
+#define png_set_sBIT rive_png_set_sBIT
+#define png_set_sCAL rive_png_set_sCAL
+#define png_set_sCAL_fixed rive_png_set_sCAL_fixed
+#define png_set_sCAL_s rive_png_set_sCAL_s
+#define png_set_sPLT rive_png_set_sPLT
+#define png_set_sRGB rive_png_set_sRGB
+#define png_set_sRGB_gAMA_and_cHRM rive_png_set_sRGB_gAMA_and_cHRM
+#define png_set_text rive_png_set_text
+#define png_set_tIME rive_png_set_tIME
+#define png_set_tRNS rive_png_set_tRNS
+#define png_set_unknown_chunk_location rive_png_set_unknown_chunk_location
+#define png_set_unknown_chunks rive_png_set_unknown_chunks
+#define png_set_user_limits rive_png_set_user_limits
+#define png_get_current_pass_number rive_png_get_current_pass_number
+#define png_get_current_row_number rive_png_get_current_row_number
+#define png_get_user_transform_ptr rive_png_get_user_transform_ptr
+#define png_set_add_alpha rive_png_set_add_alpha
+#define png_set_bgr rive_png_set_bgr
+#define png_set_filler rive_png_set_filler
+#define png_set_interlace_handling rive_png_set_interlace_handling
+#define png_set_invert_alpha rive_png_set_invert_alpha
+#define png_set_invert_mono rive_png_set_invert_mono
+#define png_set_packing rive_png_set_packing
+#define png_set_packswap rive_png_set_packswap
+#define png_set_shift rive_png_set_shift
+#define png_set_swap rive_png_set_swap
+#define png_set_swap_alpha rive_png_set_swap_alpha
+#define png_set_user_transform_info rive_png_set_user_transform_info
+#define png_set_write_fn rive_png_set_write_fn
+#define png_convert_from_struct_tm rive_png_convert_from_struct_tm
+#define png_convert_from_time_t rive_png_convert_from_time_t
+#define png_create_write_struct rive_png_create_write_struct
+#define png_create_write_struct_2 rive_png_create_write_struct_2
+#define png_destroy_write_struct rive_png_destroy_write_struct
+#define png_image_write_to_file rive_png_image_write_to_file
+#define png_image_write_to_stdio rive_png_image_write_to_stdio
+#define png_set_compression_level rive_png_set_compression_level
+#define png_set_compression_mem_level rive_png_set_compression_mem_level
+#define png_set_compression_method rive_png_set_compression_method
+#define png_set_compression_strategy rive_png_set_compression_strategy
+#define png_set_compression_window_bits rive_png_set_compression_window_bits
+#define png_set_filter rive_png_set_filter
+#define png_set_filter_heuristics rive_png_set_filter_heuristics
+#define png_set_filter_heuristics_fixed rive_png_set_filter_heuristics_fixed
+#define png_set_flush rive_png_set_flush
+#define png_set_text_compression_level rive_png_set_text_compression_level
+#define png_set_text_compression_mem_level rive_png_set_text_compression_mem_level
+#define png_set_text_compression_method rive_png_set_text_compression_method
+#define png_set_text_compression_strategy rive_png_set_text_compression_strategy
+#define png_set_text_compression_window_bits rive_png_set_text_compression_window_bits
+#define png_set_write_status_fn rive_png_set_write_status_fn
+#define png_set_write_user_transform_fn rive_png_set_write_user_transform_fn
+#define png_write_end rive_png_write_end
+#define png_write_flush rive_png_write_flush
+#define png_write_image rive_png_write_image
+#define png_write_info rive_png_write_info
+#define png_write_info_before_PLTE rive_png_write_info_before_PLTE
+#define png_write_png rive_png_write_png
+#define png_write_row rive_png_write_row
+#define png_write_rows rive_png_write_rows
+#define png_save_uint_16 rive_png_save_uint_16
+#define png_save_uint_32 rive_png_save_uint_32
+#define png_write_chunk rive_png_write_chunk
+#define png_write_chunk_data rive_png_write_chunk_data
+#define png_write_chunk_start rive_png_write_chunk_start
+#define png_write_chunk_end rive_png_write_chunk_end
+#define png_write_sig rive_png_write_sig
+#define png_init_filter_functions_neon rive_png_init_filter_functions_neon
+#define png_init_filter_functions_sse2 rive_png_init_filter_functions_sse2
+#define png_get_eXIf rive_png_get_eXIf
+#define png_get_eXIf_1 rive_png_get_eXIf_1
+#define png_handle_eXIf rive_png_handle_eXIf
+#define png_check_chunk_length rive_png_check_chunk_length
+#define png_set_eXIf rive_png_set_eXIf
+#define png_set_eXIf_1 rive_png_set_eXIf_1
+#define png_zlib_inflate rive_png_zlib_inflate
+#define png_write_eXIf rive_png_write_eXIf
+#endif // RIVE_PNGPREFIX_H
\ No newline at end of file
diff --git a/dependencies/rive_yoga_renames.h b/dependencies/rive_yoga_renames.h
new file mode 100644
index 0000000..f10efae
--- /dev/null
+++ b/dependencies/rive_yoga_renames.h
@@ -0,0 +1,241 @@
+// clang-format off
+// YG*
+#define YGFloatMax rive_YGFloatMax
+#define YGFloatMin rive_YGFloatMin
+#define YGValueEqual rive_YGValueEqual
+#define YGValue rive_YGValue
+#define YGDoubleEqual rive_YGDoubleEqual
+#define YGFloatsEqual rive_YGFloatsEqual
+#define YGFloatSanitize rive_YGFloatSanitize
+#define YGFloatOptionalMax rive_YGFloatOptionalMax
+#define YGFloatOptional rive_YGFloatOptional
+#define YGFlexDirectionCross rive_YGFlexDirectionCross
+#define YGDirection rive_YGDirection
+#define YGResolveFlexDirection rive_YGResolveFlexDirection
+#define YGFlexDirectionIsColumn rive_YGFlexDirectionIsColumn
+#define YGConfig rive_YGConfig
+#define YGNode rive_YGNode
+#define YGLogLevel rive_YGLogLevel
+#define YGErrata rive_YGErrata
+#define YGEnums rive_YGEnums
+#define YGLayout rive_YGLayout
+#define YGFloatArrayEqual rive_YGFloatArrayEqual
+#define YGCachedMeasurement rive_YGCachedMeasurement
+#define YGResolveValue rive_YGResolveValue
+#define YGFlexDirectionIsRow rive_YGFlexDirectionIsRow
+#define YGResolveValueMargin rive_YGResolveValueMargin
+#define YGMeasureMode rive_YGMeasureMode
+#define YGEdge rive_YGEdge
+#define YGStyle rive_YGStyle
+#define YGPositionType rive_YGPositionType
+#define YGFlexDirection rive_YGFlexDirection
+#define YGNodePrint rive_YGNodePrint
+#define YGPrintOptions rive_YGPrintOptions
+#define YGOverflow rive_YGOverflow
+#define YGWrap rive_YGWrap
+#define YGAlign rive_YGAlign
+#define YGDisplay rive_YGDisplay
+#define YGJustify rive_YGJustify
+#define YGLayoutNodeInternal rive_YGLayoutNodeInternal
+#define YGConfigGetDefault rive_YGConfigGetDefault
+#define YGBaseline rive_YGBaseline
+#define YGDefaultLog rive_YGDefaultLog
+#define YGNodeAlignItem rive_YGNodeAlignItem
+#define YGNodeBoundAxis rive_YGNodeBoundAxis
+#define YGNodelayoutImpl rive_YGNodelayoutImpl
+#define YGJustifyMainAxis rive_YGJustifyMainAxis
+#define YGCollectFlexItemsRowValues rive_YGCollectFlexItemsRowValues
+#define YGMeasureModeName rive_YGMeasureModeName
+#define YGIsBaselineLayout rive_YGIsBaselineLayout
+#define YGRoundToPixelGrid rive_YGRoundToPixelGrid
+#define YGDoubleIsUndefined rive_YGDoubleIsUndefined
+#define YGNodeDimWithMargin rive_YGNodeDimWithMargin
+#define YGNodePrintInternal rive_YGNodePrintInternal
+#define YGNodeIsStyleDimDefined rive_YGNodeIsStyleDimDefined
+#define YGResolveFlexibleLength rive_YGResolveFlexibleLength
+#define YGNodeIsLayoutDimDefined rive_YGNodeIsLayoutDimDefined
+#define YGConstrainMaxSizeForMode rive_YGConstrainMaxSizeForMode
+#define YGNodeAbsoluteLayoutChild rive_YGNodeAbsoluteLayoutChild
+#define YGZeroOutLayoutRecursively rive_YGZeroOutLayoutRecursively
+#define YGNodePaddingAndBorderForAxis rive_YGNodePaddingAndBorderForAxis
+#define YGDistributeFreeSpaceFirstPass rive_YGDistributeFreeSpaceFirstPass
+#define YGNodeBoundAxisWithinMinAndMax rive_YGNodeBoundAxisWithinMinAndMax
+#define YGNodeComputeFlexBasisForChild rive_YGNodeComputeFlexBasisForChild
+#define YGNodeSetChildTrailingPosition rive_YGNodeSetChildTrailingPosition
+#define YGDistributeFreeSpaceSecondPass rive_YGDistributeFreeSpaceSecondPass
+#define YGNodeCalculateAvailableInnerDim rive_YGNodeCalculateAvailableInnerDim
+#define YGDimension rive_YGDimension
+#define YGNodeComputeFlexBasisForChildren rive_YGNodeComputeFlexBasisForChildren
+#define YGCalculateCollectFlexItemsRowValues rive_YGCalculateCollectFlexItemsRowValues
+#define YGNodeFixedSizeSetMeasuredDimensions rive_YGNodeFixedSizeSetMeasuredDimensions
+#define YGNodeEmptyContainerSetMeasuredDimensions rive_YGNodeEmptyContainerSetMeasuredDimensions
+#define YGNodeWithMeasureFuncSetMeasuredDimensions rive_YGNodeWithMeasureFuncSetMeasuredDimensions
+#define YGMeasureModeOldSizeIsUnspecifiedAndStillFits rive_YGMeasureModeOldSizeIsUnspecifiedAndStillFits
+#define YGMeasureModeSizeIsExactAndMatchesOldMeasuredSize rive_YGMeasureModeSizeIsExactAndMatchesOldMeasuredSize
+#define YGMeasureModeNewMeasureSizeIsStricterAndStillValid rive_YGMeasureModeNewMeasureSizeIsStricterAndStillValid
+#define YGSpacer rive_YGSpacer
+#define YGGutter rive_YGGutter
+// _YG*
+#define YGNodeClone rive_YGNodeClone
+#define YGAlignToString rive_YGAlignToString
+#define YGDimensionToString rive_YGDimensionToString
+#define YGDirectionToString rive_YGDirectionToString
+#define YGDisplayToString rive_YGDisplayToString
+#define YGEdgeToString rive_YGEdgeToString
+#define YGErrataToString rive_YGErrataToString
+#define YGExperimentalFeatureToString rive_YGExperimentalFeatureToString
+#define YGFlexDirectionToString rive_YGFlexDirectionToString
+#define YGGutterToString rive_YGGutterToString
+#define YGJustifyToString rive_YGJustifyToString
+#define YGLogLevelToString rive_YGLogLevelToString
+#define YGMeasureModeToString rive_YGMeasureModeToString
+#define YGNodeTypeToString rive_YGNodeTypeToString
+#define YGOverflowToString rive_YGOverflowToString
+#define YGPositionTypeToString rive_YGPositionTypeToString
+#define YGPrintOptionsToString rive_YGPrintOptionsToString
+#define YGUnitToString rive_YGUnitToString
+#define YGWrapToString rive_YGWrapToString
+#define YGAssert rive_YGAssert
+#define YGAssertWithConfig rive_YGAssertWithConfig
+#define YGAssertWithNode rive_YGAssertWithNode
+#define YGValueAuto rive_YGValueAuto
+#define YGValueUndefined rive_YGValueUndefined
+#define YGValueZero rive_YGValueZero
+#define YGNodeGetChild rive_YGNodeGetChild
+#define YGConfigCopy rive_YGConfigCopy
+#define YGConfigFree rive_YGConfigFree
+#define YGConfigGetContext rive_YGConfigGetContext
+#define YGConfigGetErrata rive_YGConfigGetErrata
+#define YGConfigGetInstanceCount rive_YGConfigGetInstanceCount
+#define YGConfigGetPointScaleFactor rive_YGConfigGetPointScaleFactor
+#define YGConfigGetUseLegacyStretchBehaviour rive_YGConfigGetUseLegacyStretchBehaviour
+#define YGConfigGetUseWebDefaults rive_YGConfigGetUseWebDefaults
+#define YGConfigIsExperimentalFeatureEnabled rive_YGConfigIsExperimentalFeatureEnabled
+#define YGConfigNew rive_YGConfigNew
+#define YGConfigSetCloneNodeFunc rive_YGConfigSetCloneNodeFunc
+#define YGConfigSetContext rive_YGConfigSetContext
+#define YGConfigSetErrata rive_YGConfigSetErrata
+#define YGConfigSetExperimentalFeatureEnabled rive_YGConfigSetExperimentalFeatureEnabled
+#define YGConfigSetLogger rive_YGConfigSetLogger
+#define YGConfigSetPointScaleFactor rive_YGConfigSetPointScaleFactor
+#define YGConfigSetPrintTreeFlag rive_YGConfigSetPrintTreeFlag
+#define YGConfigSetUseLegacyStretchBehaviour rive_YGConfigSetUseLegacyStretchBehaviour
+#define YGConfigSetUseWebDefaults rive_YGConfigSetUseWebDefaults
+#define YGFloatIsUndefined rive_YGFloatIsUndefined
+#define YGNodeCalculateLayout rive_YGNodeCalculateLayout
+#define YGNodeCalculateLayoutWithContext rive_YGNodeCalculateLayoutWithContext
+#define YGNodeCanUseCachedMeasurement rive_YGNodeCanUseCachedMeasurement
+#define YGNodeCopyStyle rive_YGNodeCopyStyle
+#define YGNodeDeallocate rive_YGNodeDeallocate
+#define YGNodeFree rive_YGNodeFree
+#define YGNodeFreeRecursive rive_YGNodeFreeRecursive
+#define YGNodeFreeRecursiveWithCleanupFunc rive_YGNodeFreeRecursiveWithCleanupFunc
+#define YGNodeGetChildCount rive_YGNodeGetChildCount
+#define YGNodeGetConfig rive_YGNodeGetConfig
+#define YGNodeGetContext rive_YGNodeGetContext
+#define YGNodeGetDirtiedFunc rive_YGNodeGetDirtiedFunc
+#define YGNodeGetHasNewLayout rive_YGNodeGetHasNewLayout
+#define YGNodeGetNodeType rive_YGNodeGetNodeType
+#define YGNodeGetOwner rive_YGNodeGetOwner
+#define YGNodeGetParent rive_YGNodeGetParent
+#define YGNodeHasBaselineFunc rive_YGNodeHasBaselineFunc
+#define YGNodeHasMeasureFunc rive_YGNodeHasMeasureFunc
+#define YGNodeInsertChild rive_YGNodeInsertChild
+#define YGNodeIsDirty rive_YGNodeIsDirty
+#define YGNodeIsReferenceBaseline rive_YGNodeIsReferenceBaseline
+#define YGNodeLayoutGetBorder rive_YGNodeLayoutGetBorder
+#define YGNodeLayoutGetBottom rive_YGNodeLayoutGetBottom
+#define YGNodeLayoutGetDirection rive_YGNodeLayoutGetDirection
+#define YGNodeLayoutGetHadOverflow rive_YGNodeLayoutGetHadOverflow
+#define YGNodeLayoutGetHeight rive_YGNodeLayoutGetHeight
+#define YGNodeLayoutGetLeft rive_YGNodeLayoutGetLeft
+#define YGNodeLayoutGetMargin rive_YGNodeLayoutGetMargin
+#define YGNodeLayoutGetPadding rive_YGNodeLayoutGetPadding
+#define YGNodeLayoutGetRight rive_YGNodeLayoutGetRight
+#define YGNodeLayoutGetTop rive_YGNodeLayoutGetTop
+#define YGNodeLayoutGetWidth rive_YGNodeLayoutGetWidth
+#define YGNodeMarkDirty rive_YGNodeMarkDirty
+#define YGNodeMarkDirtyAndPropagateToDescendants rive_YGNodeMarkDirtyAndPropagateToDescendants
+#define YGNodeNew rive_YGNodeNew
+#define YGNodeNewWithConfig rive_YGNodeNewWithConfig
+#define YGNodeRemoveAllChildren rive_YGNodeRemoveAllChildren
+#define YGNodeRemoveChild rive_YGNodeRemoveChild
+#define YGNodeReset rive_YGNodeReset
+#define YGNodeSetBaselineFunc rive_YGNodeSetBaselineFunc
+#define YGNodeSetChildren rive_YGNodeSetChildren
+#define YGNodeSetConfig rive_YGNodeSetConfig
+#define YGNodeSetContext rive_YGNodeSetContext
+#define YGNodeSetDirtiedFunc rive_YGNodeSetDirtiedFunc
+#define YGNodeSetHasNewLayout rive_YGNodeSetHasNewLayout
+#define YGNodeSetIsReferenceBaseline rive_YGNodeSetIsReferenceBaseline
+#define YGNodeSetMeasureFunc rive_YGNodeSetMeasureFunc
+#define YGNodeSetNodeType rive_YGNodeSetNodeType
+#define YGNodeSetPrintFunc rive_YGNodeSetPrintFunc
+#define YGNodeStyleGetAlignContent rive_YGNodeStyleGetAlignContent
+#define YGNodeStyleGetAlignItems rive_YGNodeStyleGetAlignItems
+#define YGNodeStyleGetAlignSelf rive_YGNodeStyleGetAlignSelf
+#define YGNodeStyleGetAspectRatio rive_YGNodeStyleGetAspectRatio
+#define YGNodeStyleGetBorder rive_YGNodeStyleGetBorder
+#define YGNodeStyleGetDirection rive_YGNodeStyleGetDirection
+#define YGNodeStyleGetDisplay rive_YGNodeStyleGetDisplay
+#define YGNodeStyleGetFlex rive_YGNodeStyleGetFlex
+#define YGNodeStyleGetFlexBasis rive_YGNodeStyleGetFlexBasis
+#define YGNodeStyleGetFlexDirection rive_YGNodeStyleGetFlexDirection
+#define YGNodeStyleGetFlexGrow rive_YGNodeStyleGetFlexGrow
+#define YGNodeStyleGetFlexShrink rive_YGNodeStyleGetFlexShrink
+#define YGNodeStyleGetFlexWrap rive_YGNodeStyleGetFlexWrap
+#define YGNodeStyleGetGap rive_YGNodeStyleGetGap
+#define YGNodeStyleGetHeight rive_YGNodeStyleGetHeight
+#define YGNodeStyleGetJustifyContent rive_YGNodeStyleGetJustifyContent
+#define YGNodeStyleGetMargin rive_YGNodeStyleGetMargin
+#define YGNodeStyleGetMaxHeight rive_YGNodeStyleGetMaxHeight
+#define YGNodeStyleGetMaxWidth rive_YGNodeStyleGetMaxWidth
+#define YGNodeStyleGetMinHeight rive_YGNodeStyleGetMinHeight
+#define YGNodeStyleGetMinWidth rive_YGNodeStyleGetMinWidth
+#define YGNodeStyleGetOverflow rive_YGNodeStyleGetOverflow
+#define YGNodeStyleGetPadding rive_YGNodeStyleGetPadding
+#define YGNodeStyleGetPosition rive_YGNodeStyleGetPosition
+#define YGNodeStyleGetPositionType rive_YGNodeStyleGetPositionType
+#define YGNodeStyleGetWidth rive_YGNodeStyleGetWidth
+#define YGNodeStyleSetAlignContent rive_YGNodeStyleSetAlignContent
+#define YGNodeStyleSetAlignItems rive_YGNodeStyleSetAlignItems
+#define YGNodeStyleSetAlignSelf rive_YGNodeStyleSetAlignSelf
+#define YGNodeStyleSetAspectRatio rive_YGNodeStyleSetAspectRatio
+#define YGNodeStyleSetBorder rive_YGNodeStyleSetBorder
+#define YGNodeStyleSetDirection rive_YGNodeStyleSetDirection
+#define YGNodeStyleSetDisplay rive_YGNodeStyleSetDisplay
+#define YGNodeStyleSetFlex rive_YGNodeStyleSetFlex
+#define YGNodeStyleSetFlexBasis rive_YGNodeStyleSetFlexBasis
+#define YGNodeStyleSetFlexBasisAuto rive_YGNodeStyleSetFlexBasisAuto
+#define YGNodeStyleSetFlexBasisPercent rive_YGNodeStyleSetFlexBasisPercent
+#define YGNodeStyleSetFlexDirection rive_YGNodeStyleSetFlexDirection
+#define YGNodeStyleSetFlexGrow rive_YGNodeStyleSetFlexGrow
+#define YGNodeStyleSetFlexShrink rive_YGNodeStyleSetFlexShrink
+#define YGNodeStyleSetFlexWrap rive_YGNodeStyleSetFlexWrap
+#define YGNodeStyleSetGap rive_YGNodeStyleSetGap
+#define YGNodeStyleSetHeight rive_YGNodeStyleSetHeight
+#define YGNodeStyleSetHeightAuto rive_YGNodeStyleSetHeightAuto
+#define YGNodeStyleSetHeightPercent rive_YGNodeStyleSetHeightPercent
+#define YGNodeStyleSetJustifyContent rive_YGNodeStyleSetJustifyContent
+#define YGNodeStyleSetMargin rive_YGNodeStyleSetMargin
+#define YGNodeStyleSetMarginAuto rive_YGNodeStyleSetMarginAuto
+#define YGNodeStyleSetMarginPercent rive_YGNodeStyleSetMarginPercent
+#define YGNodeStyleSetMaxHeight rive_YGNodeStyleSetMaxHeight
+#define YGNodeStyleSetMaxHeightPercent rive_YGNodeStyleSetMaxHeightPercent
+#define YGNodeStyleSetMaxWidth rive_YGNodeStyleSetMaxWidth
+#define YGNodeStyleSetMaxWidthPercent rive_YGNodeStyleSetMaxWidthPercent
+#define YGNodeStyleSetMinHeight rive_YGNodeStyleSetMinHeight
+#define YGNodeStyleSetMinHeightPercent rive_YGNodeStyleSetMinHeightPercent
+#define YGNodeStyleSetMinWidth rive_YGNodeStyleSetMinWidth
+#define YGNodeStyleSetMinWidthPercent rive_YGNodeStyleSetMinWidthPercent
+#define YGNodeStyleSetOverflow rive_YGNodeStyleSetOverflow
+#define YGNodeStyleSetPadding rive_YGNodeStyleSetPadding
+#define YGNodeStyleSetPaddingPercent rive_YGNodeStyleSetPaddingPercent
+#define YGNodeStyleSetPosition rive_YGNodeStyleSetPosition
+#define YGNodeStyleSetPositionPercent rive_YGNodeStyleSetPositionPercent
+#define YGNodeStyleSetPositionType rive_YGNodeStyleSetPositionType
+#define YGNodeStyleSetWidth rive_YGNodeStyleSetWidth
+#define YGNodeStyleSetWidthAuto rive_YGNodeStyleSetWidthAuto
+#define YGNodeStyleSetWidthPercent rive_YGNodeStyleSetWidthPercent
+#define YGNodeSwapChild rive_YGNodeSwapChild
+#define YGRoundValueToPixelGrid rive_YGRoundValueToPixelGrid
diff --git a/dependencies/windows/config_directories.bat b/dependencies/windows/config_directories.bat
new file mode 100644
index 0000000..2dcdd50
--- /dev/null
+++ b/dependencies/windows/config_directories.bat
@@ -0,0 +1,6 @@
+set "SCRIPT_DIR=%~dp0"
+
+set "DEPENDENCIES_SCRIPTS=%SCRIPT_DIR%"
+set "DEPENDENCIES=%SCRIPT_DIR%cache"
+
+if not exist %DEPENDENCIES% mkdir %DEPENDENCIES%    
\ No newline at end of file
diff --git a/dependencies/windows/get_premake5.bat b/dependencies/windows/get_premake5.bat
new file mode 100644
index 0000000..77687b7
--- /dev/null
+++ b/dependencies/windows/get_premake5.bat
@@ -0,0 +1,12 @@
+@echo off
+pushd %DEPENDENCIES%
+if not exist ".\bin" mkdir bin
+echo Downloading Premake5
+curl https://github.com/premake/premake-core/releases/download/v5.0.0-beta1/premake-5.0.0-beta1-windows.zip -L -o .\bin\premake_windows.zip 
+pushd bin
+:: Export premake5 into bin
+tar -xf premake_windows.zip
+:: Delete downloaded archive
+del premake_windows.zip
+popd
+popd
\ No newline at end of file
diff --git a/dev/analyze.sh b/dev/analyze.sh
new file mode 100755
index 0000000..dfbbd48
--- /dev/null
+++ b/dev/analyze.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# install scan-build
+if ! command -v scan-build &> /dev/null
+then
+    if ! command -v pip &> /dev/null
+    then
+        curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
+        python3 get-pip.py
+    fi
+    pip install scan-build
+fi
+
+
+cd test
+premake5 clean || exit 1
+premake5 gmake || exit 1
+scan-build -o ../analysis_report/ make -j7 || exit 1
+premake5 clean
diff --git a/dev/core_generator/analysis_options.yaml b/dev/core_generator/analysis_options.yaml
new file mode 100644
index 0000000..40b7dee
--- /dev/null
+++ b/dev/core_generator/analysis_options.yaml
@@ -0,0 +1,123 @@
+analyzer:
+  strong-mode:
+    implicit-casts: false
+    implicit-dynamic: false
+  errors:
+    unused_import: error
+
+linter:
+  rules:
+  - always_put_required_named_parameters_first
+  - always_require_non_null_named_parameters
+  - annotate_overrides
+  # - avoid_annotating_with_dynamic
+  - avoid_bool_literals_in_conditional_expressions
+  - avoid_catches_without_on_clauses
+  - avoid_catching_errors
+  - avoid_classes_with_only_static_members
+  - avoid_double_and_int_checks
+  - avoid_empty_else
+  - avoid_field_initializers_in_const_classes
+  - avoid_implementing_value_types
+  - avoid_init_to_null
+  - avoid_js_rounded_ints
+  - avoid_null_checks_in_equality_operators
+  - avoid_relative_lib_imports
+  - avoid_return_types_on_setters
+  - avoid_returning_null
+  - avoid_returning_null_for_future
+  - avoid_returning_null_for_void
+  - avoid_returning_this
+  - avoid_setters_without_getters
+  - avoid_shadowing_type_parameters
+  - avoid_single_cascade_in_expression_statements
+  - avoid_slow_async_io
+  - avoid_types_as_parameter_names
+  - avoid_unused_constructor_parameters
+  - avoid_void_async
+  - await_only_futures
+  - camel_case_types
+  - cancel_subscriptions
+  - close_sinks
+  - constant_identifier_names
+  - control_flow_in_finally
+  - curly_braces_in_flow_control_structures
+  - directives_ordering
+  - empty_catches
+  - empty_constructor_bodies
+  - empty_statements
+  - file_names
+  - hash_and_equals
+  - implementation_imports
+#  - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811
+  - iterable_contains_unrelated_type
+  - join_return_with_assignment
+  - library_names
+  - library_prefixes
+  - lines_longer_than_80_chars
+  - list_remove_unrelated_type
+# - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181
+  - no_adjacent_strings_in_list
+  - no_duplicate_case_values
+  - non_constant_identifier_names
+  - null_closures
+  - one_member_abstracts
+  - only_throw_errors
+  - overridden_fields
+  - package_api_docs
+  - package_names
+  - package_prefixed_library_names
+  - parameter_assignments
+  - prefer_adjacent_string_concatenation
+  - prefer_asserts_in_initializer_lists
+  - prefer_collection_literals
+  - prefer_conditional_assignment
+  - prefer_const_constructors
+  - prefer_const_constructors_in_immutables
+  - prefer_const_declarations
+  - prefer_const_literals_to_create_immutables
+  - prefer_constructors_over_static_methods
+  - prefer_contains
+  - prefer_equal_for_default_values
+  - prefer_final_fields
+  - prefer_final_in_for_each
+  - prefer_foreach
+  - prefer_function_declarations_over_variables
+  - prefer_initializing_formals
+  - prefer_is_empty
+  - prefer_is_not_empty
+  - prefer_iterable_whereType
+  # - prefer_mixin
+  - prefer_null_aware_operators
+  - prefer_single_quotes
+  - prefer_typing_uninitialized_variables
+  - prefer_void_to_null
+  - recursive_getters
+  - slash_for_doc_comments
+  # - sort_pub_dependencies
+  - sort_unnamed_constructors_first
+  - test_types_in_equals
+  - throw_in_finally
+  - type_annotate_public_apis
+  - type_init_formals
+  - unawaited_futures
+  - unnecessary_await_in_return
+  - unnecessary_brace_in_string_interps
+  - unnecessary_const
+  - unnecessary_getters_setters
+  - unnecessary_lambdas
+  - unnecessary_new
+  - unnecessary_null_aware_assignments
+  - unnecessary_null_in_if_null_operators
+  - unnecessary_overrides
+  - unnecessary_parenthesis
+  - unnecessary_statements
+  - unnecessary_this
+  - unrelated_type_equality_checks
+  - use_full_hex_values_for_flutter_colors
+  - use_rethrow_when_possible
+  - use_setters_to_change_properties
+  - use_string_buffers
+  - use_to_and_as_if_applicable
+  - valid_regexps
+  - void_checks
\ No newline at end of file
diff --git a/dev/core_generator/lib/main.dart b/dev/core_generator/lib/main.dart
new file mode 100644
index 0000000..1d433d9
--- /dev/null
+++ b/dev/core_generator/lib/main.dart
@@ -0,0 +1,17 @@
+import 'dart:io';
+
+import 'package:core_generator/src/configuration.dart';
+import 'package:core_generator/src/definition.dart';
+import 'package:core_generator/src/field_types/initialize.dart';
+
+void main(List<String> arguments) {
+  initializeFields();
+  Directory(defsPath).list(recursive: true).listen(
+    (entity) {
+      if (entity is File && entity.path.toLowerCase().endsWith('.json')) {
+        Definition.make(entity.path.substring(defsPath.length));
+      }
+    },
+    onDone: Definition.generate,
+  );
+}
diff --git a/dev/core_generator/lib/src/comment.dart b/dev/core_generator/lib/src/comment.dart
new file mode 100644
index 0000000..acfde48
--- /dev/null
+++ b/dev/core_generator/lib/src/comment.dart
@@ -0,0 +1,23 @@
+RegExp _makeWrappingRegExp(int width) {
+  //return RegExp('(?![^\n]{1,$width}\$)([^\n]{1,$width})\s');
+  return RegExp('(.{1,$width})( +|\$\n?)|(.{1,$width})');
+}
+
+Map<int, RegExp> _indentRegexp = {};
+String comment(String s, {int indent = 0, bool doubleSlashes = false}) {
+  final slashes = doubleSlashes ? '//' : '///';
+  var reg = _indentRegexp[indent];
+  if (reg == null) {
+    _indentRegexp[indent] = reg = _makeWrappingRegExp(80 - 4 - 2 * indent);
+  }
+
+  return '$slashes ' +
+      s
+          .replaceAllMapped(reg, (Match m) => '${m[1]}${m[2]}\n')
+          .trim()
+          .split('\n')
+          .join('\n  $slashes ') +
+      '\n';
+}
+
+String capitalize(String s) => s[0].toUpperCase() + s.substring(1);
diff --git a/dev/core_generator/lib/src/configuration.dart b/dev/core_generator/lib/src/configuration.dart
new file mode 100644
index 0000000..56785e9
--- /dev/null
+++ b/dev/core_generator/lib/src/configuration.dart
@@ -0,0 +1,4 @@
+String defsPath = "./defs/";
+String generatedHppPath = "../include/rive/generated/";
+String concreteHppPath = "../include/";
+String generatedCppPath = "../src/generated/";
diff --git a/dev/core_generator/lib/src/cpp_formatter.dart b/dev/core_generator/lib/src/cpp_formatter.dart
new file mode 100644
index 0000000..60fd0c5
--- /dev/null
+++ b/dev/core_generator/lib/src/cpp_formatter.dart
@@ -0,0 +1,22 @@
+import 'dart:convert';
+import 'dart:io';
+
+class CppFormatter {
+  Future<String> format(String code) async {
+    var process = await Process.start('clang-format', []);
+    process.stdin.write(code);
+    await process.stdin.close();
+    return utf8.decodeStream(process.stdout);
+  }
+
+  Future<String> formatAndGuard(String name, String code) async {
+    String guardName = name
+        .replaceAllMapped(
+            RegExp('(.+?)([A-Z])'), (Match m) => '${m[1]}_${m[2]}')
+        .toUpperCase();
+    return format('''#ifndef _RIVE_${guardName}_HPP_
+        #define _RIVE_${guardName}_HPP_
+        $code
+        #endif''');
+  }
+}
diff --git a/dev/core_generator/lib/src/definition.dart b/dev/core_generator/lib/src/definition.dart
new file mode 100644
index 0000000..bf94a2b
--- /dev/null
+++ b/dev/core_generator/lib/src/definition.dart
@@ -0,0 +1,615 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:colorize/colorize.dart';
+import 'package:core_generator/src/comment.dart';
+import 'package:core_generator/src/configuration.dart';
+import 'package:core_generator/src/cpp_formatter.dart';
+import 'package:core_generator/src/field_type.dart';
+import 'package:core_generator/src/key.dart';
+import 'package:core_generator/src/property.dart';
+
+String stripExtension(String filename) {
+  var index = filename.lastIndexOf('.');
+  return index == -1 ? filename : filename.substring(0, index);
+}
+
+class Definition {
+  static final Map<String, Definition> definitions = <String, Definition>{};
+  final String _filename;
+  static final _formatter = CppFormatter();
+
+  String? _name;
+  final List<Property> _properties = [];
+
+  List<Property> get properties => _properties
+      .where((property) => property.isRuntime)
+      .toList(growable: false);
+
+  Iterable<Property> get storedProperties =>
+      properties.where((property) => property.getExportType().storesData);
+
+  Definition? _extensionOf;
+  Definition? _rawExtensionOf;
+  Key? _key;
+  bool _isAbstract = false;
+  bool _editorOnly = false;
+  bool _forRuntime = true;
+  bool get forRuntime => _forRuntime;
+
+  Definition? getRuntimeExtensionOf(Definition? definition) {
+    var extensionOf = definition;
+    if (extensionOf != null) {
+      if (extensionOf._forRuntime) {
+        return extensionOf;
+      }
+      return getRuntimeExtensionOf(extensionOf._extensionOf);
+    }
+    return extensionOf;
+  }
+
+  static Definition? make(String filename) {
+    var definition = definitions[filename];
+    if (definition != null) {
+      return definition;
+    }
+
+    var file = File(defsPath + filename);
+    var contents = file.readAsStringSync();
+    late Map<String, dynamic> definitionData;
+    try {
+      dynamic parsedJson = json.decode(contents);
+      if (parsedJson is Map<String, dynamic>) {
+        definitionData = parsedJson;
+      }
+    } on FormatException catch (error) {
+      color('Invalid json data in $filename: $error', front: Styles.RED);
+      return null;
+    }
+    definitions[filename] =
+        definition = Definition.fromFilename(filename, definitionData);
+    return definition;
+  }
+
+  Definition.fromFilename(this._filename, Map<String, dynamic> data) {
+    dynamic extendsFilename = data['extends'];
+    if (extendsFilename is String) {
+      _rawExtensionOf = Definition.make(extendsFilename);
+      _extensionOf = getRuntimeExtensionOf(_rawExtensionOf);
+    }
+    dynamic nameValue = data['name'];
+    if (nameValue is String) {
+      _name = nameValue;
+    }
+    dynamic forRuntime = data['runtime'];
+    if (forRuntime is bool) {
+      _forRuntime = forRuntime;
+    }
+    dynamic abstractValue = data['abstract'];
+    if (abstractValue is bool) {
+      _isAbstract = abstractValue;
+    }
+    dynamic editorOnlyValue = data['editorOnly'];
+    if (editorOnlyValue is bool) {
+      _editorOnly = editorOnlyValue;
+    }
+    _key = Key.fromJSON(data['key']) ?? Key.forDefinition(this);
+
+    dynamic properties = data['properties'];
+    if (properties is Map<String, dynamic>) {
+      for (final MapEntry<String, dynamic> entry in properties.entries) {
+        if (entry.value is Map<String, dynamic>) {
+          var property = Property.make(
+              this, entry.key, entry.value as Map<String, dynamic>);
+          if (property == null) {
+            continue;
+          }
+          _properties.add(property);
+        }
+      }
+    }
+  }
+  String get localFilename => _filename.indexOf(defsPath) == 0
+      ? _filename.substring(defsPath.length)
+      : _filename;
+
+  String? get name => _name;
+
+  String get localCodeFilename => '${stripExtension(_filename)}_base.hpp';
+  String get concreteCodeFilename => 'rive/${stripExtension(_filename)}.hpp';
+  String get localCppCodeFilename => '${stripExtension(_filename)}_base.cpp';
+
+  /// Generates cpp header code based on the Definition
+  Future<void> generateCode() async {
+    if (!_forRuntime) {
+      return;
+    }
+    bool defineContextExtension = _extensionOf?._name == null;
+    StringBuffer code = StringBuffer();
+
+    var includes = <String>{
+      defineContextExtension
+          ? 'rive/core.hpp'
+          : _extensionOf!.concreteCodeFilename
+    };
+    for (final property in properties) {
+      var include = property.type.include;
+      if (include != null) {
+        includes.add(include);
+      }
+      includes.add('rive/core/field_types/' +
+          property.type.snakeRuntimeCoreName +
+          '.hpp');
+    }
+
+    var sortedIncludes = includes.toList()..sort();
+    for (final include in sortedIncludes) {
+      code.write('#include ');
+      if (include[0] == '<') {
+        code.write(include);
+      } else {
+        code.write('\"$include\"');
+      }
+      code.write('\n');
+    }
+
+    code.writeln('namespace rive {');
+    var superTypeName = defineContextExtension ? 'Core' : _extensionOf?._name;
+    code.writeln('class ${_name}Base : public $superTypeName {');
+
+    code.writeln('protected:');
+    code.writeln('typedef $superTypeName Super;');
+    code.writeln('public:');
+    code.writeln('static const uint16_t typeKey = ${_key!.intValue};\n');
+
+    code.write(comment(
+        'Helper to quickly determine if a core object extends another '
+        'without RTTI at runtime.',
+        indent: 1));
+    code.writeln('bool isTypeOf(uint16_t typeKey) const override {');
+
+    code.writeln('switch(typeKey) {');
+    code.writeln('case ${_name}Base::typeKey:');
+    for (var p = _extensionOf; p != null; p = p._extensionOf) {
+      code.writeln('case ${p._name}Base::typeKey:');
+    }
+    code.writeln('return true;');
+    code.writeln('default: return false;}');
+
+    code.writeln('}\n');
+
+    code.writeln('uint16_t coreType() const override { return typeKey; }\n');
+    if (properties.isNotEmpty) {
+      for (final property in properties) {
+        code.writeln('static const uint16_t ${property.name}PropertyKey = '
+            '${property.key!.intValue};');
+        for (final altKey in property.key!.alternates) {
+          code.writeln(
+              'static const uint16_t ${altKey.stringValue}PropertyKey = '
+              '${altKey.intValue};');
+        }
+      }
+      if (storedProperties.any((prop) => !prop.isEncoded)) {
+        code.writeln('private:');
+      }
+
+      // Write fields.
+      for (final property in properties) {
+        if (property.isEncoded || !property.getExportType().storesData) {
+          // Encoded properties don't store data, it's up to the implementation
+          // to decode and store what it needs.
+          continue;
+        }
+        code.writeln('${property.type.cppName} m_${property.capitalizedName}');
+
+        var initialize = property.initialValueRuntime ??
+            property.initialValue ??
+            property.type.defaultValue;
+        if (initialize != null) {
+          var converted = property.type.convertCpp(initialize);
+          if (converted != null) {
+            code.write(' = $converted');
+          }
+        }
+        code.write(';');
+      }
+
+      // Write getter/setters.
+      code.writeln('public:');
+      for (final property in properties) {
+        if (!property.getExportType().storesData) {
+          code.writeln((property.isSetOverride ? '' : 'virtual ') +
+              'void ${property.name}' +
+              '(const ${property.type.cppName}& value) ' +
+              (property.isSetOverride ? 'override' : '') +
+              '= 0;');
+        } else if (property.isEncoded) {
+          // Encoded properties just have a pure virtual decoder that needs to
+          // be implemented. Also requires an implemention of copyPropertyName
+          // as that will no longer automatically be copied by the generated
+          // code.
+          code.writeln((property.isSetOverride ? '' : 'virtual ') +
+              'void decode${property.capitalizedName}' +
+              '(${property.type.cppName} value) ' +
+              (property.isSetOverride ? 'override' : '') +
+              '= 0;');
+          code.writeln((property.isSetOverride ? '' : 'virtual ') +
+              'void copy${property.capitalizedName}' +
+              '(const ${_name}Base& object) ' +
+              (property.isSetOverride ? 'override' : '') +
+              '= 0;');
+        } else {
+          code.writeln(((property.isVirtual || property.isPureVirtual)
+                  ? 'virtual'
+                  : 'inline') +
+              ' ${property.type.cppGetterName} ${property.name}() const ' +
+              (property.isGetOverride ? 'override' : '') +
+              '{ return m_${property.capitalizedName}; }');
+          if (!property.isPureVirtual) {
+            code.writeln(
+                'void ${property.name}(${property.type.cppName} value) ' +
+                    (property.isSetOverride ? 'override' : '') +
+                    '{'
+                        'if(m_${property.capitalizedName} == value)'
+                        '{return;}'
+                        'm_${property.capitalizedName} = value;'
+                        '${property.name}Changed();'
+                        '}');
+          } else {
+            code.writeln(
+                'virtual void ${property.name}(${property.type.cppName} value) = 0;');
+          }
+        }
+
+        code.writeln();
+      }
+    }
+
+    if (!_isAbstract) {
+      code.writeln('Core* clone() const override;');
+    }
+
+    if (storedProperties.isNotEmpty || _extensionOf == null) {
+      code.writeln('void copy(const ${_name}Base& object) {');
+      for (final property in storedProperties) {
+        if (property.isEncoded) {
+          code.writeln('copy${property.capitalizedName}(object);');
+        } else {
+          code.writeln('m_${property.capitalizedName} = '
+              'object.m_${property.capitalizedName};');
+        }
+      }
+      if (_extensionOf != null) {
+        code.writeln('${_extensionOf!.name}::'
+            'copy(object); ');
+      }
+      code.writeln('}');
+      code.writeln();
+
+      code.writeln('bool deserialize(uint16_t propertyKey, '
+          'BinaryReader& reader) override {');
+
+      if (storedProperties.isNotEmpty) {
+        code.writeln('switch (propertyKey){');
+        for (final property in properties) {
+          code.writeln('case ${property.name}PropertyKey:');
+          if (property.isEncoded) {
+            code.writeln('decode${property.capitalizedName}'
+                '(${property.type.runtimeCoreType}::deserialize(reader));');
+          } else {
+            code.writeln('m_${property.capitalizedName} = '
+                '${property.type.runtimeCoreType}::deserialize(reader);');
+          }
+          code.writeln('return true;');
+        }
+        code.writeln('}');
+      }
+      if (_extensionOf != null) {
+        code.writeln('return ${_extensionOf!.name}::'
+            'deserialize(propertyKey, reader); }');
+      } else {
+        code.writeln('return false; }');
+      }
+    }
+
+    code.writeln('protected:');
+    if (storedProperties.isNotEmpty) {
+      for (final property in storedProperties) {
+        code.writeln('virtual void ${property.name}Changed() {}');
+      }
+    }
+    code.writeln('};');
+    code.writeln('}');
+
+    var file = File('$generatedHppPath$localCodeFilename');
+    file.createSync(recursive: true);
+
+    var formattedCode =
+        await _formatter.formatAndGuard('${_name}Base', code.toString());
+    file.writeAsStringSync(formattedCode, flush: true);
+
+    // See if we need to stub out the concrete version...
+    var concreteFile = File('$concreteHppPath$concreteCodeFilename');
+    if (!concreteFile.existsSync()) {
+      StringBuffer concreteCode = StringBuffer();
+      concreteFile.createSync(recursive: true);
+      concreteCode.writeln('#include "rive/generated/$localCodeFilename"');
+      concreteCode.writeln('#include <stdio.h>');
+      concreteCode.writeln('namespace rive {');
+      concreteCode.writeln('''class $_name : public ${_name}Base {
+        public:
+      };''');
+      concreteCode.writeln('}');
+
+      var formattedCode =
+          await _formatter.formatAndGuard(_name!, concreteCode.toString());
+      concreteFile.writeAsStringSync(formattedCode, flush: true);
+    }
+    if (!_isAbstract) {
+      StringBuffer cppCode = StringBuffer();
+      cppCode.writeln('#include "rive/generated/$localCodeFilename"');
+      cppCode.writeln('#include "$concreteCodeFilename"');
+      cppCode.writeln();
+      cppCode.writeln('using namespace rive;');
+      cppCode.writeln();
+      cppCode.writeln('Core* ${_name}Base::clone() const { '
+          'auto cloned = new $_name(); '
+          'cloned->copy(*this); '
+          'return cloned; '
+          '}');
+      var cppFile = File('$generatedCppPath$localCppCodeFilename');
+      cppFile.createSync(recursive: true);
+      var formattedCode = await _formatter.format(cppCode.toString());
+      cppFile.writeAsStringSync(formattedCode, flush: true);
+    }
+  }
+
+  @override
+  String toString() {
+    return '$_name[${_key?.intValue ?? '-'}]';
+  }
+
+  static const int minPropertyId = 3;
+  static Future<bool> generate() async {
+    // Check dupe ids.
+    bool runGenerator = true;
+    Map<int, Definition> ids = {};
+    Map<int, Property> properties = {};
+    for (final definition in definitions.values) {
+      if (definition._key?.intValue != null) {
+        var other = ids[definition._key!.intValue];
+        if (other != null) {
+          color('Duplicate type ids for $definition and $other.',
+              front: Styles.RED);
+          runGenerator = false;
+        } else {
+          ids[definition._key!.intValue!] = definition;
+        }
+      }
+      for (final property in definition._properties) {
+        if (property.key!.isMissing) {
+          continue;
+        }
+        var other = properties[property.key!.intValue];
+        if (other != null) {
+          color(
+              '''Duplicate field ids for ${property.definition}.$property '''
+              '''and ${other.definition}.$other.''',
+              front: Styles.RED);
+          runGenerator = false;
+        } else if (property.key!.intValue! < minPropertyId) {
+          color(
+              '${property.definition}.$property: ids less than '
+              '$minPropertyId are reserved.',
+              front: Styles.RED);
+          runGenerator = false;
+        } else {
+          properties[property.key!.intValue!] = property;
+        }
+      }
+    }
+
+    // Find max id, we use this to assign to types that don't have ids yet.
+    int nextFieldId = minPropertyId - 1;
+    int nextId = 0;
+    for (final definition in definitions.values) {
+      var intValue = definition._key?.intValue;
+      if (intValue != null && intValue > nextId) {
+        nextId = intValue;
+      }
+      for (final field in definition._properties) {
+        var intValue = field.key?.intValue;
+        if (intValue != null && intValue > nextFieldId) {
+          nextFieldId = intValue;
+        }
+      }
+    }
+
+    if (!runGenerator) {
+      color('Not running generator due to previous errors.',
+          front: Styles.YELLOW);
+      return false;
+    }
+
+    definitions.removeWhere((key, definition) => definition._editorOnly);
+
+    // Clear out previous generated code.
+    var dir = Directory(generatedHppPath);
+    if (dir.existsSync()) {
+      dir.deleteSync(recursive: true);
+    }
+    dir.createSync(recursive: true);
+    // Generate core context.
+
+    for (final definition in definitions.values) {
+      await definition.generateCode();
+    }
+
+    StringBuffer ctxCode = StringBuffer('');
+    var includes = <String>{};
+    var runtimeDefinitions =
+        definitions.values.where((definition) => definition.forRuntime);
+    for (final definition in runtimeDefinitions) {
+      includes.add(definition.concreteCodeFilename);
+    }
+    var includeList = includes.toList()..sort();
+    for (final include in includeList) {
+      ctxCode.writeln('#include "$include"');
+    }
+    ctxCode.writeln('namespace rive {class CoreRegistry {'
+        'public:');
+    ctxCode.writeln('static Core* makeCoreInstance(int typeKey) {'
+        'switch(typeKey) {');
+    for (final definition in runtimeDefinitions) {
+      if (definition._isAbstract) {
+        continue;
+      }
+      ctxCode.writeln('case ${definition.name}Base::typeKey:');
+      ctxCode.writeln('return new ${definition.name}();');
+    }
+    ctxCode.writeln('} return nullptr; }');
+
+    var usedFieldTypes = <FieldType, List<Property>>{};
+    var getSetFieldTypes = <FieldType, List<Property>>{};
+    for (final definition in runtimeDefinitions) {
+      for (final property in definition.properties) {
+        usedFieldTypes[property.type] ??= [];
+        usedFieldTypes[property.type]!.add(property);
+        if (!property.isEncoded) {
+          getSetFieldTypes[property.type] ??= [];
+          getSetFieldTypes[property.type]!.add(property);
+        }
+      }
+    }
+    for (final fieldType in getSetFieldTypes.keys) {
+      ctxCode
+          .writeln('static void set${fieldType.capitalizedName}(Core* object, '
+              'int propertyKey, ${fieldType.cppName} value){');
+      ctxCode.writeln('switch (propertyKey) {');
+      var properties = getSetFieldTypes[fieldType];
+      if (properties != null) {
+        for (final property in properties) {
+          ctxCode.writeln('case ${property.definition.name}Base'
+              '::${property.name}PropertyKey:');
+          if (property.key != null) {
+            for (final altKey in property.key!.alternates) {
+              ctxCode.writeln('case ${property.definition.name}Base'
+                  '::${altKey.stringValue}PropertyKey:');
+            }
+          }
+          ctxCode.writeln('object->as<${property.definition.name}Base>()->'
+              '${property.name}(value);');
+          ctxCode.writeln('break;');
+        }
+      }
+      ctxCode.writeln('}}');
+    }
+    for (final fieldType in getSetFieldTypes.keys) {
+      if (!fieldType.storesData) {
+        continue;
+      }
+      ctxCode.writeln(
+          'static ${fieldType.cppName} get${fieldType.capitalizedName}('
+          'Core* object, int propertyKey){');
+      ctxCode.writeln('switch (propertyKey) {');
+      var properties = getSetFieldTypes[fieldType];
+      if (properties != null) {
+        for (final property in properties) {
+          ctxCode.writeln('case ${property.definition.name}Base'
+              '::${property.name}PropertyKey:');
+          for (final altKey in property.key!.alternates) {
+            ctxCode.writeln('case ${property.definition.name}Base'
+                '::${altKey.stringValue}PropertyKey:');
+          }
+          ctxCode
+              .writeln('return object->as<${property.definition.name}Base>()->'
+                  '${property.name}();');
+        }
+      }
+      ctxCode.writeln('}');
+      ctxCode.writeln('return ${fieldType.defaultValue ?? 'nullptr'};');
+      ctxCode.writeln('}');
+    }
+
+    ctxCode.writeln('static int propertyFieldId(int propertyKey) {');
+    ctxCode.writeln('switch(propertyKey) {');
+
+    for (final fieldType in usedFieldTypes.keys) {
+      if (!fieldType.storesData) {
+        continue;
+      }
+      var properties = usedFieldTypes[fieldType];
+      if (properties != null) {
+        for (final property in properties) {
+          ctxCode.writeln('case ${property.definition.name}Base'
+              '::${property.name}PropertyKey:');
+          for (final altKey in property.key!.alternates) {
+            ctxCode.writeln('case ${property.definition.name}Base'
+                '::${altKey.stringValue}PropertyKey:');
+          }
+        }
+      }
+      ctxCode.writeln('return Core${fieldType.capitalizedName}Type::id;');
+    }
+
+    ctxCode.writeln('default: return -1;}}');
+
+    ctxCode.writeln('''
+      static bool isCallback(uint32_t propertyKey) {
+        switch(propertyKey) {''');
+    for (final fieldType in usedFieldTypes.keys) {
+      var properties = usedFieldTypes[fieldType];
+      if (properties != null) {
+        bool found = false;
+        for (final property in properties) {
+          if (property.getExportType().name == 'callback') {
+            found = true;
+            ctxCode.write('case ${property.definition._name}Base');
+            ctxCode.write('::${property.name}PropertyKey:');
+          }
+        }
+        if (found) {
+          ctxCode.writeln('return true;');
+        }
+      }
+    }
+    ctxCode.writeln('default:return false;');
+    ctxCode.writeln('}}');
+
+    // static bool objectSupportsProperty(Core* object, uint32_t propertyKey) { return true; }
+    ctxCode.writeln('''
+      static bool objectSupportsProperty(Core* object, uint32_t propertyKey) {
+        switch(propertyKey) {''');
+    for (final fieldType in usedFieldTypes.keys) {
+      var properties = getSetFieldTypes[fieldType];
+      if (properties != null) {
+        for (final property in properties) {
+          ctxCode.writeln('case ${property.definition.name}Base'
+              '::${property.name}PropertyKey:');
+          for (final altKey in property.key!.alternates) {
+            ctxCode.writeln('case ${property.definition.name}Base'
+                '::${altKey.stringValue}PropertyKey:');
+          }
+          ctxCode
+              .writeln('return object->is<${property.definition.name}Base>();');
+        }
+      }
+    }
+    ctxCode.writeln('}return false;}');
+    ctxCode.writeln('};}');
+
+    var output = generatedHppPath;
+    var folder = output.isNotEmpty && output[output.length - 1] == '/'
+        ? output.substring(0, output.length - 1)
+        : output;
+
+    var file = File('$folder/core_registry.hpp');
+    file.createSync(recursive: true);
+
+    var formattedCode =
+        await _formatter.formatAndGuard('CoreRegistry', ctxCode.toString());
+    file.writeAsStringSync(formattedCode, flush: true);
+
+    return true;
+  }
+}
diff --git a/dev/core_generator/lib/src/field_type.dart b/dev/core_generator/lib/src/field_type.dart
new file mode 100644
index 0000000..49edcd0
--- /dev/null
+++ b/dev/core_generator/lib/src/field_type.dart
@@ -0,0 +1,67 @@
+export 'package:core_generator/src/field_types/bool_field_type.dart';
+export 'package:core_generator/src/field_types/color_field_type.dart';
+export 'package:core_generator/src/field_types/double_field_type.dart';
+export 'package:core_generator/src/field_types/string_field_type.dart';
+export 'package:core_generator/src/field_types/uint_field_type.dart';
+
+Map<String, FieldType> _types = <String, FieldType>{};
+
+abstract class FieldType {
+  final String name;
+  String? _cppName;
+  final String? include;
+  String? get cppName => _cppName;
+  String? get cppGetterName => _cppName;
+
+  final String _runtimeCoreType;
+  String get runtimeCoreType => _runtimeCoreType;
+
+  final bool storesData;
+
+  FieldType(
+    this.name,
+    this._runtimeCoreType, {
+    String? cppName,
+    this.include,
+    this.storesData = true,
+  }) {
+    _cppName = cppName ?? name;
+    _types[name] = this;
+  }
+
+  static FieldType? find(dynamic key) {
+    if (key is! String) {
+      return null;
+    }
+    return _types[key];
+  }
+
+  @override
+  String toString() {
+    return name;
+  }
+
+  String equalityCheck(String varAName, String varBName) {
+    return '$varAName == $varBName';
+  }
+
+  String? get defaultValue => null;
+
+  String get uncapitalizedName => '${name[0].toLowerCase()}${name.substring(1)}'
+      .replaceAll('<', '')
+      .replaceAll('>', '');
+
+  String get capitalizedName => '${name[0].toUpperCase()}${name.substring(1)}'
+      .replaceAll('<', '')
+      .replaceAll('>', '');
+
+  String get snakeName => name
+      .replaceAllMapped(RegExp('(.+?)([A-Z])'), (Match m) => '${m[1]}_${m[2]}')
+      .toLowerCase();
+
+  String get snakeRuntimeCoreName => _runtimeCoreType
+      .replaceAllMapped(RegExp('(.+?)([A-Z])'), (Match m) => '${m[1]}_${m[2]}')
+      .toLowerCase();
+
+  String? convertCpp(String value) => value;
+}
diff --git a/dev/core_generator/lib/src/field_types/bool_field_type.dart b/dev/core_generator/lib/src/field_types/bool_field_type.dart
new file mode 100644
index 0000000..04c710f
--- /dev/null
+++ b/dev/core_generator/lib/src/field_types/bool_field_type.dart
@@ -0,0 +1,12 @@
+import '../field_type.dart';
+
+class BoolFieldType extends FieldType {
+  BoolFieldType()
+      : super(
+          'bool',
+          'CoreBoolType',
+        );
+
+  @override
+  String get defaultValue => 'false';
+}
diff --git a/dev/core_generator/lib/src/field_types/bytes_field_type.dart b/dev/core_generator/lib/src/field_types/bytes_field_type.dart
new file mode 100644
index 0000000..3ff7f55
--- /dev/null
+++ b/dev/core_generator/lib/src/field_types/bytes_field_type.dart
@@ -0,0 +1,22 @@
+import '../field_type.dart';
+
+class BytesFieldType extends FieldType {
+  BytesFieldType()
+      : super(
+          'Bytes',
+          'CoreBytesType',
+          cppName: 'Span<const uint8_t>',
+          include: 'rive/span.hpp',
+        );
+
+  @override
+  String get defaultValue => 'Span()';
+
+  @override
+  String get cppGetterName => 'Span<const uint8_t>';
+
+  @override
+  String? convertCpp(String value) {
+    return null;
+  }
+}
diff --git a/dev/core_generator/lib/src/field_types/callback_field_type.dart b/dev/core_generator/lib/src/field_types/callback_field_type.dart
new file mode 100644
index 0000000..5c596e2
--- /dev/null
+++ b/dev/core_generator/lib/src/field_types/callback_field_type.dart
@@ -0,0 +1,14 @@
+import 'package:core_generator/src/field_type.dart';
+
+class CallbackFieldType extends FieldType {
+  CallbackFieldType()
+      : super(
+          'callback',
+          'CoreCallbackType',
+          cppName: 'CallbackData',
+          storesData: false,
+        );
+
+  @override
+  String get defaultValue => '0';
+}
diff --git a/dev/core_generator/lib/src/field_types/color_field_type.dart b/dev/core_generator/lib/src/field_types/color_field_type.dart
new file mode 100644
index 0000000..5d87cbb
--- /dev/null
+++ b/dev/core_generator/lib/src/field_types/color_field_type.dart
@@ -0,0 +1,13 @@
+import '../field_type.dart';
+
+class ColorFieldType extends FieldType {
+  ColorFieldType()
+      : super(
+          'Color',
+          'CoreColorType',
+          cppName: 'int',
+        );
+
+  @override
+  String get defaultValue => '0';
+}
diff --git a/dev/core_generator/lib/src/field_types/double_field_type.dart b/dev/core_generator/lib/src/field_types/double_field_type.dart
new file mode 100644
index 0000000..1f565bb
--- /dev/null
+++ b/dev/core_generator/lib/src/field_types/double_field_type.dart
@@ -0,0 +1,22 @@
+import '../field_type.dart';
+
+class DoubleFieldType extends FieldType {
+  DoubleFieldType() : super('double', 'CoreDoubleType', cppName: 'float');
+
+  @override
+  String get defaultValue => '0.0f';
+
+  @override
+  String? convertCpp(String value) {
+    var result = value;
+    if (result.isNotEmpty) {
+      if (result[result.length - 1] != 'f') {
+        if (!result.contains('.')) {
+          result += '.0';
+        }
+        result += 'f';
+      }
+    }
+    return result;
+  }
+}
diff --git a/dev/core_generator/lib/src/field_types/initialize.dart b/dev/core_generator/lib/src/field_types/initialize.dart
new file mode 100644
index 0000000..4c94698
--- /dev/null
+++ b/dev/core_generator/lib/src/field_types/initialize.dart
@@ -0,0 +1,19 @@
+// All supported field types.
+
+import 'package:core_generator/src/field_type.dart';
+import 'package:core_generator/src/field_types/bytes_field_type.dart';
+import 'package:core_generator/src/field_types/callback_field_type.dart';
+
+late List<FieldType> fields;
+
+void initializeFields() {
+  fields = [
+    StringFieldType(),
+    BytesFieldType(),
+    UintFieldType(),
+    DoubleFieldType(),
+    BoolFieldType(),
+    ColorFieldType(),
+    CallbackFieldType(),
+  ];
+}
diff --git a/dev/core_generator/lib/src/field_types/string_field_type.dart b/dev/core_generator/lib/src/field_types/string_field_type.dart
new file mode 100644
index 0000000..3fb6d9f
--- /dev/null
+++ b/dev/core_generator/lib/src/field_types/string_field_type.dart
@@ -0,0 +1,26 @@
+import '../field_type.dart';
+
+class StringFieldType extends FieldType {
+  StringFieldType()
+      : super('String', 'CoreStringType',
+            cppName: 'std::string', include: '<string>');
+  @override
+  String get defaultValue => '""';
+
+  @override
+  String get cppGetterName => 'const std::string&';
+
+  @override
+  String? convertCpp(String value) {
+    var result = value;
+    if (result.length > 1) {
+      if (result[0] == '\'') {
+        result = '"' + result.substring(1);
+      }
+      if (result[result.length - 1] == '\'') {
+        result = result.substring(0, result.length - 1) + '"';
+      }
+    }
+    return result;
+  }
+}
diff --git a/dev/core_generator/lib/src/field_types/uint_field_type.dart b/dev/core_generator/lib/src/field_types/uint_field_type.dart
new file mode 100644
index 0000000..df9b751
--- /dev/null
+++ b/dev/core_generator/lib/src/field_types/uint_field_type.dart
@@ -0,0 +1,18 @@
+import '../field_type.dart';
+
+class UintFieldType extends FieldType {
+  UintFieldType()
+      : super(
+          'uint',
+          'CoreUintType',
+          cppName: 'uint32_t',
+        );
+
+  @override
+  String get defaultValue => '0';
+
+  // We do this to fix up CoreContext.invalidProperyKey
+  @override
+  String? convertCpp(String value) =>
+      value.replaceAll('CoreContext.', 'Core::');
+}
diff --git a/dev/core_generator/lib/src/key.dart b/dev/core_generator/lib/src/key.dart
new file mode 100644
index 0000000..ca83684
--- /dev/null
+++ b/dev/core_generator/lib/src/key.dart
@@ -0,0 +1,59 @@
+import 'definition.dart';
+import 'property.dart';
+
+class Key {
+  final String? stringValue;
+  final int? intValue;
+  final List<Key> alternates = [];
+
+  bool get isMissing => intValue == null;
+
+  Key(this.stringValue, this.intValue);
+  Key.forDefinition(Definition def)
+      : stringValue = def.name?.toLowerCase(),
+        intValue = null;
+  Key.forProperty(Property field)
+      : stringValue = field.name.toLowerCase(),
+        intValue = null;
+
+  Key withIntValue(int id) => Key(stringValue, id);
+
+  static Key? fromJSON(dynamic data) {
+    if (data is! Map<String, dynamic>) {
+      return null;
+    }
+    dynamic iv = data['int'];
+    dynamic sv = data['string'];
+    dynamic av = data['alternates'];
+    if (iv is int && sv is String) {
+      final key = Key(sv, iv);
+      if (av is List) {
+        for (final a in av) {
+          if (a is Map<String, dynamic>) {
+            dynamic altiv = a['int'];
+            dynamic altsv = a['string'];
+            key.alternates.add(Key(altsv, altiv));
+          }
+        }
+      }
+      return key;
+    }
+    return null;
+  }
+
+  Map<String, dynamic> serialize() {
+    final json = <String, dynamic>{'int': intValue, 'string': stringValue};
+    final altsJson = [];
+    for (final alt in alternates) {
+      final altJson = <String, dynamic>{
+        'int': alt.intValue,
+        'string': alt.stringValue
+      };
+      altsJson.add(altJson);
+    }
+    if (altsJson.isNotEmpty) {
+      json['alternates'] = altsJson;
+    }
+    return json;
+  }
+}
diff --git a/dev/core_generator/lib/src/property.dart b/dev/core_generator/lib/src/property.dart
new file mode 100644
index 0000000..46993b1
--- /dev/null
+++ b/dev/core_generator/lib/src/property.dart
@@ -0,0 +1,117 @@
+import 'package:colorize/colorize.dart';
+import 'package:core_generator/src/definition.dart';
+import 'package:core_generator/src/field_type.dart';
+import 'package:core_generator/src/key.dart';
+
+class Property {
+  final String name;
+  final FieldType type;
+  final Definition definition;
+  String? initialValue;
+  String? initialValueRuntime;
+  bool isVirtual = false;
+  bool animates = false;
+  String? group;
+  Key? key;
+  String? description;
+  bool isNullable = false;
+  bool isRuntime = true;
+  bool isCoop = true;
+  bool isSetOverride = false;
+  bool isGetOverride = false;
+  bool isEncoded = false;
+  bool isBindable = false;
+  bool isPureVirtual = false;
+  FieldType? typeRuntime;
+
+  static Property? make(
+      Definition type, String name, Map<String, dynamic> data) {
+    if (data['runtime'] is bool && data['runtime'] == false) {
+      return null;
+    }
+
+    var fieldType =
+        FieldType.find(data['typeRuntime']) ?? FieldType.find(data['type']);
+
+    if (fieldType == null) {
+      color('Invalid field type ${data['type']} for $name.', front: Styles.RED);
+      return null;
+    }
+    return Property(type, name, fieldType, data);
+  }
+
+  Property(this.definition, this.name, this.type, Map<String, dynamic> data) {
+    dynamic encodedValue = data['encoded'];
+    if (encodedValue is bool) {
+      isEncoded = encodedValue;
+    }
+    dynamic descriptionValue = data['description'];
+    if (descriptionValue is String) {
+      description = descriptionValue;
+    }
+    dynamic nullableValue = data['nullable'];
+    if (nullableValue is bool) {
+      isNullable = nullableValue;
+    }
+    dynamic init = data['initialValue'];
+    if (init is String) {
+      initialValue = init;
+    }
+    dynamic initRuntime = data['initialValueRuntime'];
+    if (initRuntime is String) {
+      initialValueRuntime = initRuntime;
+    }
+    dynamic overrideSet = data['overrideSet'];
+    if (overrideSet is bool && overrideSet) {
+      isSetOverride = true;
+    }
+    dynamic overrideGet = data['overrideGet'];
+    if (overrideGet is bool && overrideGet) {
+      isGetOverride = true;
+    }
+    dynamic a = data['animates'];
+    if (a is bool) {
+      animates = a;
+    }
+    dynamic virtualValue = data['virtual'];
+    isVirtual = virtualValue is bool && virtualValue;
+    dynamic g = data['group'];
+    if (g is String) {
+      group = g;
+    }
+    dynamic e = data['editorOnly'];
+    if (e is bool && e) {
+      isCoop = false;
+    }
+    dynamic r = data['runtime'];
+    if (r is bool) {
+      isRuntime = r;
+    }
+    dynamic c = data['coop'];
+    if (c is bool) {
+      isCoop = c;
+    }
+    dynamic rt = data['typeRuntime'];
+    if (rt is String) {
+      typeRuntime = FieldType.find(rt);
+    }
+    dynamic b = data['bindable'];
+    if (b is bool) {
+      isBindable = b;
+    }
+    dynamic pv = data['pureVirtual'];
+    if (pv is bool) {
+      isPureVirtual = pv;
+    }
+    key = Key.fromJSON(data['key']) ?? Key.forProperty(this);
+  }
+
+  FieldType getExportType() => typeRuntime ?? type;
+
+  @override
+  String toString() => '$name(${key?.intValue})';
+
+  String get capitalizedName => '${name[0].toUpperCase()}${name.substring(1)}'
+      .replaceAll('<', '')
+      .replaceAll('>', '');
+}
diff --git a/dev/core_generator/pubspec.yaml b/dev/core_generator/pubspec.yaml
new file mode 100644
index 0000000..4fb6c60
--- /dev/null
+++ b/dev/core_generator/pubspec.yaml
@@ -0,0 +1,7 @@
+name: core_generator
+
+environment:
+  sdk: ">=2.17.0 <3.0.0"
+
+dependencies:
+  colorize: ^3.0.0
diff --git a/dev/defs/README.md b/dev/defs/README.md
new file mode 100644
index 0000000..e964498
--- /dev/null
+++ b/dev/defs/README.md
@@ -0,0 +1,2 @@
+# rive-core-defs
+Core definitions for Rive editor and runtimes.
diff --git a/dev/defs/animation/advanceable_state.json b/dev/defs/animation/advanceable_state.json
new file mode 100644
index 0000000..05d1d67
--- /dev/null
+++ b/dev/defs/animation/advanceable_state.json
@@ -0,0 +1,19 @@
+{
+  "name": "AdvanceableState",
+  "key": {
+    "int": 145,
+    "string": "advanceablestate"
+  },
+  "abstract": true,
+  "extends": "animation/layer_state.json",
+  "properties": {
+    "speed": {
+      "type": "double",
+      "initialValue": "1",
+      "key": {
+        "int": 292,
+        "string": "speed"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/animation.json b/dev/defs/animation/animation.json
new file mode 100644
index 0000000..0f4e3d0
--- /dev/null
+++ b/dev/defs/animation/animation.json
@@ -0,0 +1,48 @@
+{
+  "name": "Animation",
+  "key": {
+    "int": 27,
+    "string": "animation"
+  },
+  "properties": {
+    "artboardId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 54,
+        "string": "artboardid"
+      },
+      "description": "Identifier used to track the artboard this animation belongs to.",
+      "runtime": false
+    },
+    "name": {
+      "type": "String",
+      "initialValue": "''",
+      "key": {
+        "int": 55,
+        "string": "name"
+      },
+      "description": "Name of the animation."
+    },
+    "order": {
+      "type": "FractionalIndex",
+      "initialValue": "FractionalIndex.invalid",
+      "key": {
+        "int": 73,
+        "string": "order"
+      },
+      "description": "Order this animation shows up in the animations list.",
+      "runtime": false
+    },
+    "folderId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 282,
+        "string": "folderid"
+      },
+      "description": "Id of the folder this animation belongs to",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/animation_folder.json b/dev/defs/animation/animation_folder.json
new file mode 100644
index 0000000..e04d9d8
--- /dev/null
+++ b/dev/defs/animation/animation_folder.json
@@ -0,0 +1,9 @@
+{
+  "name": "AnimationFolder",
+  "key": {
+    "int": 143,
+    "string": "animationfolder"
+  },
+  "extends": "animation/animation.json",
+  "runtime": false
+}
\ No newline at end of file
diff --git a/dev/defs/animation/animation_state.json b/dev/defs/animation/animation_state.json
new file mode 100644
index 0000000..43d5874
--- /dev/null
+++ b/dev/defs/animation/animation_state.json
@@ -0,0 +1,21 @@
+{
+  "name": "AnimationState",
+  "key": {
+    "int": 61,
+    "string": "animationstate"
+  },
+  "extends": "animation/advanceable_state.json",
+  "properties": {
+    "animationId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 149,
+        "string": "animationid"
+      },
+      "description": "Id of the animation this layer state refers to."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/any_state.json b/dev/defs/animation/any_state.json
new file mode 100644
index 0000000..dd682b7
--- /dev/null
+++ b/dev/defs/animation/any_state.json
@@ -0,0 +1,8 @@
+{
+  "name": "AnyState",
+  "key": {
+    "int": 62,
+    "string": "anystate"
+  },
+  "extends": "animation/layer_state.json"
+}
\ No newline at end of file
diff --git a/dev/defs/animation/blend_animation.json b/dev/defs/animation/blend_animation.json
new file mode 100644
index 0000000..86ccdb5
--- /dev/null
+++ b/dev/defs/animation/blend_animation.json
@@ -0,0 +1,43 @@
+{
+  "name": "BlendAnimation",
+  "key": {
+    "int": 74,
+    "string": "blendanimation"
+  },
+  "abstract": true,
+  "properties": {
+    "blendStateId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 170,
+        "string": "blendstateid"
+      },
+      "description": "Id of the BlendState that this BlendAnimation belongs to.",
+      "runtime": false
+    },
+    "animationId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 165,
+        "string": "animationid"
+      },
+      "description": "Id of the animation this BlendAnimation references."
+    },
+    "animationOrder": {
+      "type": "FractionalIndex",
+      "initialValue": "FractionalIndex.invalid",
+      "key": {
+        "int": 169,
+        "string": "animationorder"
+      },
+      "description": "Order value for sorting animations in blend states.",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/blend_animation_1d.json b/dev/defs/animation/blend_animation_1d.json
new file mode 100644
index 0000000..651acc5
--- /dev/null
+++ b/dev/defs/animation/blend_animation_1d.json
@@ -0,0 +1,18 @@
+{
+  "name": "BlendAnimation1D",
+  "key": {
+    "int": 75,
+    "string": "blendanimation1d"
+  },
+  "extends": "animation/blend_animation.json",
+  "properties": {
+    "value": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 166,
+        "string": "value"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/blend_animation_direct.json b/dev/defs/animation/blend_animation_direct.json
new file mode 100644
index 0000000..b4c99b1
--- /dev/null
+++ b/dev/defs/animation/blend_animation_direct.json
@@ -0,0 +1,39 @@
+{
+  "name": "BlendAnimationDirect",
+  "key": {
+    "int": 77,
+    "string": "blendanimationdirect"
+  },
+  "extends": "animation/blend_animation.json",
+  "properties": {
+    "inputId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 168,
+        "string": "inputid"
+      },
+      "description": "Id of the input that drives the direct mix value for this animation."
+    },
+    "mixValue": {
+      "type": "double",
+      "initialValue": "100",
+      "key": {
+        "int": 297,
+        "string": "mixvalue"
+      },
+      "description": "Direct mix value for this animation."
+    },
+    "blendSource": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 298,
+        "string": "blendsource"
+      },
+      "description": "Source to use when establishing the mix value for the animation. 0 means look at the input, 1 look at the mixValue."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/blend_state.json b/dev/defs/animation/blend_state.json
new file mode 100644
index 0000000..635675c
--- /dev/null
+++ b/dev/defs/animation/blend_state.json
@@ -0,0 +1,9 @@
+{
+  "name": "BlendState",
+  "key": {
+    "int": 72,
+    "string": "blendstate"
+  },
+  "abstract": true,
+  "extends": "animation/layer_state.json"
+}
\ No newline at end of file
diff --git a/dev/defs/animation/blend_state_1d.json b/dev/defs/animation/blend_state_1d.json
new file mode 100644
index 0000000..f575bbc
--- /dev/null
+++ b/dev/defs/animation/blend_state_1d.json
@@ -0,0 +1,22 @@
+{
+  "name": "BlendState1D",
+  "key": {
+    "int": 76,
+    "string": "blendstate1d"
+  },
+  "extends": "animation/blend_state.json",
+  "generic": "animation/blend_animation_1d.json",
+  "properties": {
+    "inputId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 167,
+        "string": "inputid"
+      },
+      "description": "Id of the input that drives the mix value for this blend state."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/blend_state_direct.json b/dev/defs/animation/blend_state_direct.json
new file mode 100644
index 0000000..869dc90
--- /dev/null
+++ b/dev/defs/animation/blend_state_direct.json
@@ -0,0 +1,9 @@
+{
+  "name": "BlendStateDirect",
+  "key": {
+    "int": 73,
+    "string": "blendstatedirect"
+  },
+  "extends": "animation/blend_state.json",
+  "generic": "animation/blend_animation_direct.json"
+}
\ No newline at end of file
diff --git a/dev/defs/animation/blend_state_transition.json b/dev/defs/animation/blend_state_transition.json
new file mode 100644
index 0000000..45a0425
--- /dev/null
+++ b/dev/defs/animation/blend_state_transition.json
@@ -0,0 +1,21 @@
+{
+  "name": "BlendStateTransition",
+  "key": {
+    "int": 78,
+    "string": "blendstatetransition"
+  },
+  "extends": "animation/state_transition.json",
+  "properties": {
+    "exitBlendAnimationId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 171,
+        "string": "exitblendanimationid"
+      },
+      "description": "Id of the state the blend state animation used for exit time calculation."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/cubic_ease_interpolator.json b/dev/defs/animation/cubic_ease_interpolator.json
new file mode 100644
index 0000000..4ffa30b
--- /dev/null
+++ b/dev/defs/animation/cubic_ease_interpolator.json
@@ -0,0 +1,9 @@
+{
+  "name": "CubicEaseInterpolator",
+  "key": {
+    "int": 28,
+    "string": "cubicEase"
+  },
+  "exportsWithContext": true,
+  "extends": "animation/cubic_interpolator.json"
+}
\ No newline at end of file
diff --git a/dev/defs/animation/cubic_interpolator.json b/dev/defs/animation/cubic_interpolator.json
new file mode 100644
index 0000000..cd2bddf
--- /dev/null
+++ b/dev/defs/animation/cubic_interpolator.json
@@ -0,0 +1,44 @@
+{
+  "name": "CubicInterpolator",
+  "key": {
+    "int": 139,
+    "string": "cubicinterpolator"
+  },
+  "abstract": true,
+  "exportsWithContext": true,
+  "extends": "animation/keyframe_interpolator.json",
+  "properties": {
+    "x1": {
+      "type": "double",
+      "initialValue": "0.42",
+      "key": {
+        "int": 63,
+        "string": "x1"
+      }
+    },
+    "y1": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 64,
+        "string": "y1"
+      }
+    },
+    "x2": {
+      "type": "double",
+      "initialValue": "0.58",
+      "key": {
+        "int": 65,
+        "string": "x2"
+      }
+    },
+    "y2": {
+      "type": "double",
+      "initialValue": "1",
+      "key": {
+        "int": 66,
+        "string": "y2"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/cubic_interpolator_component.json b/dev/defs/animation/cubic_interpolator_component.json
new file mode 100644
index 0000000..c5147f7
--- /dev/null
+++ b/dev/defs/animation/cubic_interpolator_component.json
@@ -0,0 +1,46 @@
+{
+  "name": "CubicInterpolatorComponent",
+  "key": {
+    "int": 163,
+    "string": "cubicinterpolatorcomponent"
+  },
+  "extends": "component.json",
+  "properties": {
+    "x1": {
+      "type": "double",
+      "initialValue": "0.42",
+      "animates": true,
+      "key": {
+        "int": 337,
+        "string": "x1"
+      }
+    },
+    "y1": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 338,
+        "string": "y1"
+      }
+    },
+    "x2": {
+      "type": "double",
+      "initialValue": "0.58",
+      "animates": true,
+      "key": {
+        "int": 339,
+        "string": "x2"
+      }
+    },
+    "y2": {
+      "type": "double",
+      "initialValue": "1",
+      "animates": true,
+      "key": {
+        "int": 340,
+        "string": "y2"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/cubic_value_interpolator.json b/dev/defs/animation/cubic_value_interpolator.json
new file mode 100644
index 0000000..6372c24
--- /dev/null
+++ b/dev/defs/animation/cubic_value_interpolator.json
@@ -0,0 +1,9 @@
+{
+  "name": "CubicValueInterpolator",
+  "key": {
+    "int": 138,
+    "string": "cubicvalueinterpolator"
+  },
+  "exportsWithContext": true,
+  "extends": "animation/cubic_interpolator.json"
+}
\ No newline at end of file
diff --git a/dev/defs/animation/elastic_interpolator.json b/dev/defs/animation/elastic_interpolator.json
new file mode 100644
index 0000000..0fd7895
--- /dev/null
+++ b/dev/defs/animation/elastic_interpolator.json
@@ -0,0 +1,37 @@
+{
+  "name": "ElasticInterpolator",
+  "key": {
+    "int": 174,
+    "string": "elastic_interpolator"
+  },
+  "exportsWithContext": true,
+  "extends": "animation/keyframe_interpolator.json",
+  "properties": {
+    "easingValue": {
+      "type": "uint",
+      "initialValue": "1",
+      "key": {
+        "int": 405,
+        "string": "easing"
+      }
+    },
+    "amplitude": {
+      "type": "double",
+      "initialValue": "1",
+      "key": {
+        "int": 406,
+        "string": "amplitude"
+      },
+      "description": "The amplitude for the easing expressed as a percentage of the change."
+    },
+    "period": {
+      "type": "double",
+      "initialValue": "1",
+      "key": {
+        "int": 407,
+        "string": "period"
+      },
+      "description": "The period of the elastic expressed as a percentage of the time difference."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/entry_state.json b/dev/defs/animation/entry_state.json
new file mode 100644
index 0000000..ef3f9c4
--- /dev/null
+++ b/dev/defs/animation/entry_state.json
@@ -0,0 +1,8 @@
+{
+  "name": "EntryState",
+  "key": {
+    "int": 63,
+    "string": "entrystate"
+  },
+  "extends": "animation/layer_state.json"
+}
\ No newline at end of file
diff --git a/dev/defs/animation/exit_state.json b/dev/defs/animation/exit_state.json
new file mode 100644
index 0000000..06f16fc
--- /dev/null
+++ b/dev/defs/animation/exit_state.json
@@ -0,0 +1,8 @@
+{
+  "name": "ExitState",
+  "key": {
+    "int": 64,
+    "string": "exitstate"
+  },
+  "extends": "animation/layer_state.json"
+}
\ No newline at end of file
diff --git a/dev/defs/animation/interpolating_keyframe.json b/dev/defs/animation/interpolating_keyframe.json
new file mode 100644
index 0000000..ddc5375
--- /dev/null
+++ b/dev/defs/animation/interpolating_keyframe.json
@@ -0,0 +1,31 @@
+{
+  "name": "InterpolatingKeyFrame",
+  "key": {
+    "int": 170,
+    "string": "interpolatingkeyframe"
+  },
+  "abstract": true,
+  "extends": "animation/keyframe.json",
+  "properties": {
+    "interpolationType": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 68,
+        "string": "interpolation"
+      },
+      "description": "The type of interpolation index in KeyframeInterpolation applied to this keyframe."
+    },
+    "interpolatorId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 69,
+        "string": "interpolatorid"
+      },
+      "description": "The id of the custom interpolator used when interpolation is Cubic."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/keyed_object.json b/dev/defs/animation/keyed_object.json
new file mode 100644
index 0000000..0eac2d7
--- /dev/null
+++ b/dev/defs/animation/keyed_object.json
@@ -0,0 +1,31 @@
+{
+  "name": "KeyedObject",
+  "key": {
+    "int": 25,
+    "string": "keyedobject"
+  },
+  "properties": {
+    "objectId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 51,
+        "string": "objectid"
+      },
+      "description": "Identifier used to track the object that is keyed."
+    },
+    "animationId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 52,
+        "string": "animationid"
+      },
+      "description": "The id of the animation this keyed object is in.",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/keyed_property.json b/dev/defs/animation/keyed_property.json
new file mode 100644
index 0000000..20c678e
--- /dev/null
+++ b/dev/defs/animation/keyed_property.json
@@ -0,0 +1,28 @@
+{
+  "name": "KeyedProperty",
+  "key": {
+    "int": 26,
+    "string": "keyedproperty"
+  },
+  "properties": {
+    "keyedObjectId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 71,
+        "string": "keyedobjectid"
+      },
+      "description": "The id of the KeyedObject this KeyedProperty belongs to.",
+      "runtime": false
+    },
+    "propertyKey": {
+      "type": "uint",
+      "initialValue": "CoreContext.invalidPropertyKey",
+      "key": {
+        "int": 53,
+        "string": "propertykey"
+      },
+      "description": "The property type that is keyed."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/keyframe.json b/dev/defs/animation/keyframe.json
new file mode 100644
index 0000000..1384e8d
--- /dev/null
+++ b/dev/defs/animation/keyframe.json
@@ -0,0 +1,30 @@
+{
+  "name": "KeyFrame",
+  "key": {
+    "int": 29,
+    "string": "keyframe"
+  },
+  "abstract": true,
+  "properties": {
+    "keyedPropertyId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 72,
+        "string": "keyedpropertyid"
+      },
+      "description": "The id of the KeyedProperty this KeyFrame belongs to.",
+      "runtime": false
+    },
+    "frame": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 67,
+        "string": "frame"
+      },
+      "description": "Timecode as frame number can be converted to time by dividing by animation fps."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/keyframe_bool.json b/dev/defs/animation/keyframe_bool.json
new file mode 100644
index 0000000..bdf1d68
--- /dev/null
+++ b/dev/defs/animation/keyframe_bool.json
@@ -0,0 +1,19 @@
+{
+  "name": "KeyFrameBool",
+  "key": {
+    "int": 84,
+    "string": "keyframebool"
+  },
+  "extends": "animation/interpolating_keyframe.json",
+  "properties": {
+    "value": {
+      "type": "bool",
+      "typeRuntime": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 181,
+        "string": "value"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/keyframe_callback.json b/dev/defs/animation/keyframe_callback.json
new file mode 100644
index 0000000..61e84b5
--- /dev/null
+++ b/dev/defs/animation/keyframe_callback.json
@@ -0,0 +1,8 @@
+{
+  "name": "KeyFrameCallback",
+  "key": {
+    "int": 171,
+    "string": "keyframe_callback"
+  },
+  "extends": "animation/keyframe.json"
+}
\ No newline at end of file
diff --git a/dev/defs/animation/keyframe_color.json b/dev/defs/animation/keyframe_color.json
new file mode 100644
index 0000000..c456254
--- /dev/null
+++ b/dev/defs/animation/keyframe_color.json
@@ -0,0 +1,18 @@
+{
+  "name": "KeyFrameColor",
+  "key": {
+    "int": 37,
+    "string": "keyframecolor"
+  },
+  "extends": "animation/interpolating_keyframe.json",
+  "properties": {
+    "value": {
+      "type": "Color",
+      "initialValue": "0",
+      "key": {
+        "int": 88,
+        "string": "value"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/keyframe_double.json b/dev/defs/animation/keyframe_double.json
new file mode 100644
index 0000000..4ea3405
--- /dev/null
+++ b/dev/defs/animation/keyframe_double.json
@@ -0,0 +1,18 @@
+{
+  "name": "KeyFrameDouble",
+  "key": {
+    "int": 30,
+    "string": "keyframedouble"
+  },
+  "extends": "animation/interpolating_keyframe.json",
+  "properties": {
+    "value": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 70,
+        "string": "value"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/keyframe_id.json b/dev/defs/animation/keyframe_id.json
new file mode 100644
index 0000000..299be61
--- /dev/null
+++ b/dev/defs/animation/keyframe_id.json
@@ -0,0 +1,20 @@
+{
+  "name": "KeyFrameId",
+  "key": {
+    "int": 50,
+    "string": "keyframeid"
+  },
+  "extends": "animation/interpolating_keyframe.json",
+  "properties": {
+    "value": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 122,
+        "string": "value"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/keyframe_interpolator.json b/dev/defs/animation/keyframe_interpolator.json
new file mode 100644
index 0000000..0e10a1c
--- /dev/null
+++ b/dev/defs/animation/keyframe_interpolator.json
@@ -0,0 +1,8 @@
+{
+  "name": "KeyFrameInterpolator",
+  "key": {
+    "int": 175,
+    "string": "keyframeinterpolator"
+  },
+  "abstract": true
+}
\ No newline at end of file
diff --git a/dev/defs/animation/keyframe_string.json b/dev/defs/animation/keyframe_string.json
new file mode 100644
index 0000000..c4b301a
--- /dev/null
+++ b/dev/defs/animation/keyframe_string.json
@@ -0,0 +1,19 @@
+{
+  "name": "KeyFrameString",
+  "key": {
+    "int": 142,
+    "string": "keyframestring"
+  },
+  "extends": "animation/interpolating_keyframe.json",
+  "properties": {
+    "value": {
+      "type": "String",
+      "typeRuntime": "String",
+      "initialValue": "''",
+      "key": {
+        "int": 280,
+        "string": "value"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/keyframe_uint.json b/dev/defs/animation/keyframe_uint.json
new file mode 100644
index 0000000..2a0fe0e
--- /dev/null
+++ b/dev/defs/animation/keyframe_uint.json
@@ -0,0 +1,19 @@
+{
+  "name": "KeyFrameUint",
+  "key": {
+    "int": 450,
+    "string": "keyframeuint"
+  },
+  "extends": "animation/interpolating_keyframe.json",
+  "properties": {
+    "value": {
+      "type": "uint",
+      "typeRuntime": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 631,
+        "string": "value"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/layer_state.json b/dev/defs/animation/layer_state.json
new file mode 100644
index 0000000..b15b793
--- /dev/null
+++ b/dev/defs/animation/layer_state.json
@@ -0,0 +1,47 @@
+{
+  "name": "LayerState",
+  "key": {
+    "int": 60,
+    "string": "layerstate"
+  },
+  "abstract": true,
+  "extends": "animation/state_machine_layer_component.json",
+  "properties": {
+    "layerId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 146,
+        "string": "layerid"
+      },
+      "description": "Id of the state machine layer this state belongs to.",
+      "runtime": false
+    },
+    "x": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 147,
+        "string": "x"
+      },
+      "runtime": false
+    },
+    "y": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 148,
+        "string": "y"
+      },
+      "runtime": false
+    },
+    "flags": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 536,
+        "string": "flags"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/linear_animation.json b/dev/defs/animation/linear_animation.json
new file mode 100644
index 0000000..068f5b5
--- /dev/null
+++ b/dev/defs/animation/linear_animation.json
@@ -0,0 +1,159 @@
+{
+  "name": "LinearAnimation",
+  "key": {
+    "int": 31,
+    "string": "linearanimation"
+  },
+  "extends": "animation/animation.json",
+  "properties": {
+    "fps": {
+      "type": "uint",
+      "initialValue": "60",
+      "key": {
+        "int": 56,
+        "string": "fps"
+      },
+      "description": "Frames per second used to quantize keyframe times to discrete values that match this rate."
+    },
+    "duration": {
+      "type": "uint",
+      "initialValue": "60",
+      "key": {
+        "int": 57,
+        "string": "duration"
+      },
+      "description": "Duration expressed in number of frames."
+    },
+    "speed": {
+      "type": "double",
+      "initialValue": "1",
+      "key": {
+        "int": 58,
+        "string": "speed"
+      },
+      "description": "Playback speed multiplier."
+    },
+    "loopValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 59,
+        "string": "loop"
+      },
+      "description": "Loop value option matches Loop enumeration."
+    },
+    "workStart": {
+      "type": "uint",
+      "initialValue": "-1",
+      "key": {
+        "int": 60,
+        "string": "workstart"
+      },
+      "description": "Start of the work area in frames."
+    },
+    "workEnd": {
+      "type": "uint",
+      "initialValue": "-1",
+      "key": {
+        "int": 61,
+        "string": "workend"
+      },
+      "description": "End of the work area in frames."
+    },
+    "enableWorkArea": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 62,
+        "string": "enableworkarea"
+      },
+      "description": "Whether or not the work area is enabled."
+    },
+    "playhead": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 132,
+        "string": "playhead"
+      },
+      "description": "Playhead position in frames.",
+      "runtime": false,
+      "coop": false
+    },
+    "viewStart": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 133,
+        "string": "viewstart"
+      },
+      "description": "Viewport start in frames.",
+      "runtime": false,
+      "coop": false
+    },
+    "viewEnd": {
+      "type": "uint",
+      "initialValue": "2147483648",
+      "key": {
+        "int": 134,
+        "string": "viewend"
+      },
+      "description": "Viewport end in frames.",
+      "runtime": false,
+      "coop": false
+    },
+    "scrollOffset": {
+      "type": "double",
+      "initialValue": "0.0",
+      "key": {
+        "int": 256,
+        "string": "scrolloffset"
+      },
+      "description": "Scroll offset of the key frame view",
+      "runtime": false,
+      "coop": false
+    },
+    "expandedComponents": {
+      "type": "List<Id>",
+      "initialValue": "[]",
+      "key": {
+        "int": 266,
+        "string": "expandedcomponents"
+      },
+      "description": "List of components that are expanded on the timeline view",
+      "runtime": false,
+      "coop": false
+    },
+    "timelineMode": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 275,
+        "string": "timelinemode"
+      },
+      "description": "Edit time timeline mode",
+      "runtime": false,
+      "coop": false
+    },
+    "showSelected": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 278,
+        "string": "showselected"
+      },
+      "description": "Whether to only show selected components on the timeline hierarchy",
+      "runtime": false,
+      "coop": false
+    },
+    "quantize": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 376,
+        "string": "quantize"
+      },
+      "description": "Whether frames are quantized to desired frame rate or floating based on runtime speed."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/listener_action.json b/dev/defs/animation/listener_action.json
new file mode 100644
index 0000000..a1f0b1c
--- /dev/null
+++ b/dev/defs/animation/listener_action.json
@@ -0,0 +1,33 @@
+{
+  "name": "ListenerAction",
+  "key": {
+    "int": 125,
+    "string": "listener_action"
+  },
+  "abstract": true,
+  "properties": {
+    "listenerId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 226,
+        "string": "listenerId"
+      },
+      "description": "Identifier used to track the StateMachineListener this result belongs to.",
+      "runtime": false
+    },
+    "order": {
+      "type": "FractionalIndex",
+      "initialValue": "FractionalIndex.invalid",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 230,
+        "string": "order"
+      },
+      "description": "Order value for condition in a transition.",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/listener_align_target.json b/dev/defs/animation/listener_align_target.json
new file mode 100644
index 0000000..699b160
--- /dev/null
+++ b/dev/defs/animation/listener_align_target.json
@@ -0,0 +1,30 @@
+{
+  "name": "ListenerAlignTarget",
+  "key": {
+    "int": 126,
+    "string": "listeneraligntarget"
+  },
+  "extends": "animation/listener_action.json",
+  "properties": {
+    "targetId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 240,
+        "string": "targetid"
+      },
+      "description": "Identifier used to track the object use as a target fo this listener action."
+    },
+    "preserveOffset": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 541,
+        "string": "preserveoffset"
+      },
+      "description": "Whether to preserve offset between mouse position and target position."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/listener_bool_change.json b/dev/defs/animation/listener_bool_change.json
new file mode 100644
index 0000000..54b5ca0
--- /dev/null
+++ b/dev/defs/animation/listener_bool_change.json
@@ -0,0 +1,19 @@
+{
+  "name": "ListenerBoolChange",
+  "key": {
+    "int": 117,
+    "string": "listener_bool_change"
+  },
+  "extends": "animation/listener_input_change.json",
+  "properties": {
+    "value": {
+      "type": "uint",
+      "initialValue": "1",
+      "key": {
+        "int": 228,
+        "string": "value"
+      },
+      "description": "Value to set the input to when the listener occurs."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/listener_fire_event.json b/dev/defs/animation/listener_fire_event.json
new file mode 100644
index 0000000..d5baa7d
--- /dev/null
+++ b/dev/defs/animation/listener_fire_event.json
@@ -0,0 +1,21 @@
+{
+  "name": "ListenerFireEvent",
+  "key": {
+    "int": 168,
+    "string": "listenerfireevent"
+  },
+  "extends": "animation/listener_action.json",
+  "properties": {
+    "eventId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 389,
+        "string": "eventid"
+      },
+      "description": "Id of the Event referenced."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/listener_input_change.json b/dev/defs/animation/listener_input_change.json
new file mode 100644
index 0000000..006e50c
--- /dev/null
+++ b/dev/defs/animation/listener_input_change.json
@@ -0,0 +1,33 @@
+{
+  "name": "ListenerInputChange",
+  "key": {
+    "int": 116,
+    "string": "listener_input_change"
+  },
+  "abstract": true,
+  "extends": "animation/listener_action.json",
+  "properties": {
+    "inputId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 227,
+        "string": "inputid"
+      },
+      "description": "Id of the StateMachineInput referenced."
+    },
+    "nestedInputId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 400,
+        "string": "nestedinputid"
+      },
+      "description": "Id of the NestedInput referenced if this is listening to a nested input."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/listener_number_change.json b/dev/defs/animation/listener_number_change.json
new file mode 100644
index 0000000..f980b3a
--- /dev/null
+++ b/dev/defs/animation/listener_number_change.json
@@ -0,0 +1,19 @@
+{
+  "name": "ListenerNumberChange",
+  "key": {
+    "int": 118,
+    "string": "listener_number_change"
+  },
+  "extends": "animation/listener_input_change.json",
+  "properties": {
+    "value": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 229,
+        "string": "value"
+      },
+      "description": "Value to set the input to when the listener occurs."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/listener_trigger_change.json b/dev/defs/animation/listener_trigger_change.json
new file mode 100644
index 0000000..d3d0759
--- /dev/null
+++ b/dev/defs/animation/listener_trigger_change.json
@@ -0,0 +1,8 @@
+{
+  "name": "ListenerTriggerChange",
+  "key": {
+    "int": 115,
+    "string": "listener_trigger_change"
+  },
+  "extends": "animation/listener_input_change.json"
+}
\ No newline at end of file
diff --git a/dev/defs/animation/listener_viewmodel_change.json b/dev/defs/animation/listener_viewmodel_change.json
new file mode 100644
index 0000000..96ffb5b
--- /dev/null
+++ b/dev/defs/animation/listener_viewmodel_change.json
@@ -0,0 +1,38 @@
+{
+  "name": "ListenerViewModelChange",
+  "key": {
+    "int": 487,
+    "string": "listenerviewmodelchange"
+  },
+  "extends": "animation/listener_action.json",
+  "properties": {
+    "bindablePropertyId": {
+      "type": "Id",
+      "key": {
+        "int": 657,
+        "string": "bindablepropertyid"
+      },
+      "description": "Id of the bindable property",
+      "runtime": false
+    },
+    "fromViewModelProperty": {
+      "type": "bool",
+      "key": {
+        "int": 658,
+        "string": "fromviewmodelproperty"
+      },
+      "description": "Whether it is using another view model property as the value",
+      "runtime": false
+    },
+    "fromDataBindId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 659,
+        "string": "fromdatabindid"
+      },
+      "description": "Id of the data bind used if the value is taken from another view model property (only needed if fromViewModelProperty is true)",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/nested_bool.json b/dev/defs/animation/nested_bool.json
new file mode 100644
index 0000000..31aa257
--- /dev/null
+++ b/dev/defs/animation/nested_bool.json
@@ -0,0 +1,20 @@
+{
+  "name": "NestedBool",
+  "key": {
+    "int": 123,
+    "string": "nestedBool"
+  },
+  "extends": "animation/nested_input.json",
+  "properties": {
+    "nestedValue": {
+      "type": "bool",
+      "initialValue": "false",
+      "animates": true,
+      "pureVirtual": true,
+      "key": {
+        "int": 238,
+        "string": "value"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/nested_input.json b/dev/defs/animation/nested_input.json
new file mode 100644
index 0000000..4fd8fe1
--- /dev/null
+++ b/dev/defs/animation/nested_input.json
@@ -0,0 +1,22 @@
+{
+  "name": "NestedInput",
+  "key": {
+    "int": 121,
+    "string": "nestedinput"
+  },
+  "abstract": true,
+  "extends": "component.json",
+  "properties": {
+    "inputId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 237,
+        "string": "inputid"
+      },
+      "description": "Identifier used to track the actual backing state machine input."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/nested_linear_animation.json b/dev/defs/animation/nested_linear_animation.json
new file mode 100644
index 0000000..eb2df80
--- /dev/null
+++ b/dev/defs/animation/nested_linear_animation.json
@@ -0,0 +1,22 @@
+{
+  "name": "NestedLinearAnimation",
+  "key": {
+    "int": 97,
+    "string": "nestedlinearanimation"
+  },
+  "abstract": true,
+  "extends": "nested_animation.json",
+  "generic": "animation/linear_animation.json",
+  "properties": {
+    "mix": {
+      "type": "double",
+      "initialValue": "1",
+      "animates": true,
+      "key": {
+        "int": 200,
+        "string": "mix"
+      },
+      "description": "Value to mix the animation in."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/nested_number.json b/dev/defs/animation/nested_number.json
new file mode 100644
index 0000000..c845ea4
--- /dev/null
+++ b/dev/defs/animation/nested_number.json
@@ -0,0 +1,20 @@
+{
+  "name": "NestedNumber",
+  "key": {
+    "int": 124,
+    "string": "nestedNumber"
+  },
+  "extends": "animation/nested_input.json",
+  "properties": {
+    "nestedValue": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "pureVirtual": true,
+      "key": {
+        "int": 239,
+        "string": "nestedValue"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/nested_remap_animation.json b/dev/defs/animation/nested_remap_animation.json
new file mode 100644
index 0000000..f5e2d64
--- /dev/null
+++ b/dev/defs/animation/nested_remap_animation.json
@@ -0,0 +1,20 @@
+{
+  "name": "NestedRemapAnimation",
+  "key": {
+    "int": 98,
+    "string": "nested_remap_animation"
+  },
+  "extends": "animation/nested_linear_animation.json",
+  "properties": {
+    "time": {
+      "type": "double",
+      "initialValue": "0.0",
+      "animates": true,
+      "key": {
+        "int": 202,
+        "string": "time"
+      },
+      "description": "Time value in seconds for the nested linear animation."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/nested_simple_animation.json b/dev/defs/animation/nested_simple_animation.json
new file mode 100644
index 0000000..5d609a0
--- /dev/null
+++ b/dev/defs/animation/nested_simple_animation.json
@@ -0,0 +1,30 @@
+{
+  "name": "NestedSimpleAnimation",
+  "key": {
+    "int": 96,
+    "string": "nested_simple_animation"
+  },
+  "extends": "animation/nested_linear_animation.json",
+  "properties": {
+    "speed": {
+      "type": "double",
+      "initialValue": "1",
+      "animates": true,
+      "key": {
+        "int": 199,
+        "string": "speed"
+      },
+      "description": "Speed to play the nested animation at."
+    },
+    "isPlaying": {
+      "type": "bool",
+      "initialValue": "false",
+      "animates": true,
+      "key": {
+        "int": 201,
+        "string": "is_playing"
+      },
+      "description": "Enumerated backing value for playback state."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/nested_state_machine.json b/dev/defs/animation/nested_state_machine.json
new file mode 100644
index 0000000..1982515
--- /dev/null
+++ b/dev/defs/animation/nested_state_machine.json
@@ -0,0 +1,9 @@
+{
+  "name": "NestedStateMachine",
+  "key": {
+    "int": 95,
+    "string": "nestedStateMachine"
+  },
+  "extends": "nested_animation.json",
+  "generic": "animation/state_machine.json"
+}
\ No newline at end of file
diff --git a/dev/defs/animation/nested_trigger.json b/dev/defs/animation/nested_trigger.json
new file mode 100644
index 0000000..ab9255e
--- /dev/null
+++ b/dev/defs/animation/nested_trigger.json
@@ -0,0 +1,18 @@
+{
+  "name": "NestedTrigger",
+  "key": {
+    "int": 122,
+    "string": "nestedTrigger"
+  },
+  "extends": "animation/nested_input.json",
+  "properties": {
+    "fire": {
+      "type": "callback",
+      "animates": true,
+      "key": {
+        "int": 401,
+        "string": "fire"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/state_machine.json b/dev/defs/animation/state_machine.json
new file mode 100644
index 0000000..fa97a25
--- /dev/null
+++ b/dev/defs/animation/state_machine.json
@@ -0,0 +1,87 @@
+{
+  "name": "StateMachine",
+  "key": {
+    "int": 53,
+    "string": "statemachine"
+  },
+  "extends": "animation/animation.json",
+  "properties": {
+    "editingLayerId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 142,
+        "string": "editinglayer"
+      },
+      "description": "Id of the currently editing layer.",
+      "runtime": false,
+      "coop": false
+    },
+    "isListenersPanelOpen": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 250,
+        "string": "islistenerspanelopen"
+      },
+      "description": "Toggle determining whether the listeners panel is open",
+      "runtime": false,
+      "coop": false
+    },
+    "isInputsPanelOpen": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 251,
+        "string": "isinputspanelopen"
+      },
+      "description": "Toggle determining whether the inputs panel is open",
+      "runtime": false,
+      "coop": false
+    },
+    "isConsolePanelOpen": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 252,
+        "string": "isconsolepanelopen"
+      },
+      "description": "Toggle determining whether the console panel is open",
+      "runtime": false,
+      "coop": false
+    },
+    "listenersPanelSize": {
+      "type": "double",
+      "initialValue": "200",
+      "key": {
+        "int": 253,
+        "string": "listenerspanelsize"
+      },
+      "description": "Size of the listeners panel",
+      "runtime": false,
+      "coop": false
+    },
+    "inputsPanelSize": {
+      "type": "double",
+      "initialValue": "200",
+      "key": {
+        "int": 254,
+        "string": "inputspanelsize"
+      },
+      "description": "Size of the inputs panel",
+      "runtime": false,
+      "coop": false
+    },
+    "consolePanelSize": {
+      "type": "double",
+      "initialValue": "200",
+      "key": {
+        "int": 255,
+        "string": "consolepanelsize"
+      },
+      "description": "Size of the inputs panel",
+      "runtime": false,
+      "coop": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/state_machine_bool.json b/dev/defs/animation/state_machine_bool.json
new file mode 100644
index 0000000..3c9fbaf
--- /dev/null
+++ b/dev/defs/animation/state_machine_bool.json
@@ -0,0 +1,28 @@
+{
+  "name": "StateMachineBool",
+  "key": {
+    "int": 59,
+    "string": "statemachinebool"
+  },
+  "extends": "animation/state_machine_input.json",
+  "properties": {
+    "value": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 141,
+        "string": "value"
+      }
+    },
+    "playbackValue": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 232,
+        "string": "playbackvalue"
+      },
+      "runtime": false,
+      "coop": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/state_machine_component.json b/dev/defs/animation/state_machine_component.json
new file mode 100644
index 0000000..2266155
--- /dev/null
+++ b/dev/defs/animation/state_machine_component.json
@@ -0,0 +1,49 @@
+{
+  "name": "StateMachineComponent",
+  "key": {
+    "int": 54,
+    "string": "statemachinecomponent"
+  },
+  "abstract": true,
+  "properties": {
+    "machineId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 137,
+        "string": "machineid"
+      },
+      "description": "Id of the state machine this component belongs to.",
+      "runtime": false
+    },
+    "name": {
+      "type": "String",
+      "initialValue": "''",
+      "key": {
+        "int": 138,
+        "string": "name"
+      },
+      "description": "Non-unique identifier, used to give friendly names to state machine components (like layers or inputs)."
+    },
+    "machineOrder": {
+      "type": "FractionalIndex",
+      "initialValue": "FractionalIndex.invalid",
+      "key": {
+        "int": 139,
+        "string": "machineorder"
+      },
+      "description": "Order of this component in the state machine. Exported in order for runtime",
+      "runtime": false
+    },
+    "folderId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 311,
+        "string": "folderid"
+      },
+      "description": "Id of the folder this state machine component belongs to",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/state_machine_component_folder.json b/dev/defs/animation/state_machine_component_folder.json
new file mode 100644
index 0000000..ab05187
--- /dev/null
+++ b/dev/defs/animation/state_machine_component_folder.json
@@ -0,0 +1,9 @@
+{
+  "name": "StateMachineComponentFolder",
+  "key": {
+    "int": 151,
+    "string": "statemachinecomponentfolder"
+  },
+  "extends": "animation/state_machine_component.json",
+  "runtime": false
+}
\ No newline at end of file
diff --git a/dev/defs/animation/state_machine_component_nested_artboard.json b/dev/defs/animation/state_machine_component_nested_artboard.json
new file mode 100644
index 0000000..431b95b
--- /dev/null
+++ b/dev/defs/animation/state_machine_component_nested_artboard.json
@@ -0,0 +1,23 @@
+{
+  "name": "StateMachineComponentNestedArtboard",
+  "key": {
+    "int": 172,
+    "string": "statemachinecomponentnestedartboard"
+  },
+  "extends": "animation/state_machine_component_folder.json",
+  "runtime": false,
+  "properties": {
+    "nestedArtboardId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 404,
+        "string": "nestedartboardid"
+      },
+      "description": "Id of the Artboard represented by this component.",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/state_machine_fire_event.json b/dev/defs/animation/state_machine_fire_event.json
new file mode 100644
index 0000000..41db898
--- /dev/null
+++ b/dev/defs/animation/state_machine_fire_event.json
@@ -0,0 +1,49 @@
+{
+  "name": "StateMachineFireEvent",
+  "key": {
+    "int": 169,
+    "string": "statemachinefireevent"
+  },
+  "properties": {
+    "layerComponentId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 391,
+        "string": "layercomponentid"
+      },
+      "description": "Id of the transition or layer this belongs to.",
+      "runtime": false
+    },
+    "eventId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 392,
+        "string": "eventid"
+      },
+      "description": "Id of the Event referenced."
+    },
+    "occursValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 393,
+        "string": "occursvalue"
+      },
+      "description": "When the event fires."
+    },
+    "fireOrder": {
+      "type": "FractionalIndex",
+      "initialValue": "FractionalIndex.invalid",
+      "key": {
+        "int": 394,
+        "string": "fireorder"
+      },
+      "description": "Order value for sorting transitions in states.",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/state_machine_input.json b/dev/defs/animation/state_machine_input.json
new file mode 100644
index 0000000..93d6e60
--- /dev/null
+++ b/dev/defs/animation/state_machine_input.json
@@ -0,0 +1,21 @@
+{
+  "name": "StateMachineInput",
+  "key": {
+    "int": 55,
+    "string": "statemachineinput"
+  },
+  "abstract": true,
+  "extends": "animation/state_machine_component.json",
+  "properties": {
+    "public": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 402,
+        "string": "public"
+      },
+      "description": "Determines whether this StateMachineInput will be exposed to parent artboards.",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/state_machine_input_folder.json b/dev/defs/animation/state_machine_input_folder.json
new file mode 100644
index 0000000..86e874f
--- /dev/null
+++ b/dev/defs/animation/state_machine_input_folder.json
@@ -0,0 +1,9 @@
+{
+  "name": "StateMachineInputFolder",
+  "key": {
+    "int": 149,
+    "string": "statemachineinputfolder"
+  },
+  "extends": "animation/state_machine_component_folder.json",
+  "runtime": false
+}
\ No newline at end of file
diff --git a/dev/defs/animation/state_machine_layer.json b/dev/defs/animation/state_machine_layer.json
new file mode 100644
index 0000000..0af17fc
--- /dev/null
+++ b/dev/defs/animation/state_machine_layer.json
@@ -0,0 +1,43 @@
+{
+  "name": "StateMachineLayer",
+  "key": {
+    "int": 57,
+    "string": "statemachinelayer"
+  },
+  "extends": "animation/state_machine_component.json",
+  "properties": {
+    "stageZoom": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 143,
+        "string": "stagezoom"
+      },
+      "description": "Zoom value of the stage in the editor. Initial value is 0 to indicate it is not set.",
+      "runtime": false,
+      "coop": false
+    },
+    "stageX": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 144,
+        "string": "stagex"
+      },
+      "description": "X translation value of the stage in the editor.",
+      "runtime": false,
+      "coop": false
+    },
+    "stageY": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 145,
+        "string": "stagey"
+      },
+      "description": "Y translation value of the stage in the editor.",
+      "runtime": false,
+      "coop": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/state_machine_layer_component.json b/dev/defs/animation/state_machine_layer_component.json
new file mode 100644
index 0000000..85f1593
--- /dev/null
+++ b/dev/defs/animation/state_machine_layer_component.json
@@ -0,0 +1,8 @@
+{
+  "name": "StateMachineLayerComponent",
+  "key": {
+    "int": 66,
+    "string": "statemachinelayercomponent"
+  },
+  "abstract": true
+}
\ No newline at end of file
diff --git a/dev/defs/animation/state_machine_listener.json b/dev/defs/animation/state_machine_listener.json
new file mode 100644
index 0000000..4e99535
--- /dev/null
+++ b/dev/defs/animation/state_machine_listener.json
@@ -0,0 +1,41 @@
+{
+  "name": "StateMachineListener",
+  "key": {
+    "int": 114,
+    "string": "stateMachineListener"
+  },
+  "extends": "animation/state_machine_component.json",
+  "properties": {
+    "targetId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 224,
+        "string": "targetid"
+      },
+      "description": "Identifier used to track the object use as a target for this listener."
+    },
+    "listenerTypeValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 225,
+        "string": "listenertypevalue"
+      },
+      "description": "Listener type (hover, click, etc)."
+    },
+    "eventId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 399,
+        "string": "eventid"
+      },
+      "description": "Event id for the associated event, if listenerType is event"
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/state_machine_listener_folder.json b/dev/defs/animation/state_machine_listener_folder.json
new file mode 100644
index 0000000..9c59ae2
--- /dev/null
+++ b/dev/defs/animation/state_machine_listener_folder.json
@@ -0,0 +1,9 @@
+{
+  "name": "StateMachineListenerFolder",
+  "key": {
+    "int": 150,
+    "string": "statemachinelistenerfolder"
+  },
+  "extends": "animation/state_machine_component_folder.json",
+  "runtime": false
+}
\ No newline at end of file
diff --git a/dev/defs/animation/state_machine_nested_input.json b/dev/defs/animation/state_machine_nested_input.json
new file mode 100644
index 0000000..633b1ab
--- /dev/null
+++ b/dev/defs/animation/state_machine_nested_input.json
@@ -0,0 +1,23 @@
+{
+  "name": "StateMachineNestedInput",
+  "key": {
+    "int": 173,
+    "string": "statemachinenestedinput"
+  },
+  "extends": "animation/state_machine_component.json",
+  "runtime": false,
+  "properties": {
+    "nestedInputId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 403,
+        "string": "nestedinputid"
+      },
+      "description": "Id of the NestedInput.",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/state_machine_number.json b/dev/defs/animation/state_machine_number.json
new file mode 100644
index 0000000..8d533f2
--- /dev/null
+++ b/dev/defs/animation/state_machine_number.json
@@ -0,0 +1,28 @@
+{
+  "name": "StateMachineNumber",
+  "key": {
+    "int": 56,
+    "string": "statemachinenumber"
+  },
+  "extends": "animation/state_machine_input.json",
+  "properties": {
+    "value": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 140,
+        "string": "value"
+      }
+    },
+    "playbackValue": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 231,
+        "string": "playbackvalue"
+      },
+      "runtime": false,
+      "coop": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/state_machine_trigger.json b/dev/defs/animation/state_machine_trigger.json
new file mode 100644
index 0000000..dddc51e
--- /dev/null
+++ b/dev/defs/animation/state_machine_trigger.json
@@ -0,0 +1,8 @@
+{
+  "name": "StateMachineTrigger",
+  "key": {
+    "int": 58,
+    "string": "statemachinetrigger"
+  },
+  "extends": "animation/state_machine_input.json"
+}
\ No newline at end of file
diff --git a/dev/defs/animation/state_transition.json b/dev/defs/animation/state_transition.json
new file mode 100644
index 0000000..3ac429e
--- /dev/null
+++ b/dev/defs/animation/state_transition.json
@@ -0,0 +1,96 @@
+{
+  "name": "StateTransition",
+  "key": {
+    "int": 65,
+    "string": "statetransition"
+  },
+  "extends": "animation/state_machine_layer_component.json",
+  "properties": {
+    "stateFromId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 150,
+        "string": "statefromid"
+      },
+      "description": "Id of the state this transition originates from.",
+      "runtime": false
+    },
+    "stateToId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 151,
+        "string": "statetoid"
+      },
+      "description": "Id of the state this transition originates from."
+    },
+    "flags": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 152,
+        "string": "flags"
+      }
+    },
+    "duration": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 158,
+        "string": "duration"
+      },
+      "description": "Duration of the trasition (mix time) in milliseconds or percentage (0-100) based on flags."
+    },
+    "transitionOrder": {
+      "type": "FractionalIndex",
+      "initialValue": "FractionalIndex.invalid",
+      "key": {
+        "int": 159,
+        "string": "transitionorder"
+      },
+      "description": "Order value for sorting transitions in states.",
+      "runtime": false
+    },
+    "exitTime": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 160,
+        "string": "exittime"
+      },
+      "description": "Duration in milliseconds that must elapse before allowing the state to change. If the flags mark this property as being percentage based, the value is in 0-100% of the outgoing animation's duration"
+    },
+    "interpolationType": {
+      "type": "uint",
+      "initialValue": "1",
+      "key": {
+        "int": 349,
+        "string": "interpolationtype"
+      },
+      "description": "The type of interpolation index in Interpolation applied to this state transition ('linear' by default)."
+    },
+    "interpolatorId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 350,
+        "string": "interpolatorid"
+      },
+      "description": "The id of the custom interpolator used when interpolation is Cubic."
+    },
+    "randomWeight": {
+      "type": "uint",
+      "initialValue": "1",
+      "key": {
+        "int": 537,
+        "string": "randomweight"
+      },
+      "description": "Weight of the transition in the overall random options"
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/transition_artboard_condition.json b/dev/defs/animation/transition_artboard_condition.json
new file mode 100644
index 0000000..5a39c66
--- /dev/null
+++ b/dev/defs/animation/transition_artboard_condition.json
@@ -0,0 +1,8 @@
+{
+  "name": "TransitionArtboardCondition",
+  "key": {
+    "int": 497,
+    "string": "transitionartboardcondition"
+  },
+  "extends": "animation/transition_viewmodel_condition.json"
+}
\ No newline at end of file
diff --git a/dev/defs/animation/transition_bool_condition.json b/dev/defs/animation/transition_bool_condition.json
new file mode 100644
index 0000000..cc62cf1
--- /dev/null
+++ b/dev/defs/animation/transition_bool_condition.json
@@ -0,0 +1,8 @@
+{
+  "name": "TransitionBoolCondition",
+  "key": {
+    "int": 71,
+    "string": "transitionboolcondition"
+  },
+  "extends": "animation/transition_value_condition.json"
+}
\ No newline at end of file
diff --git a/dev/defs/animation/transition_comparator.json b/dev/defs/animation/transition_comparator.json
new file mode 100644
index 0000000..5d01f6e
--- /dev/null
+++ b/dev/defs/animation/transition_comparator.json
@@ -0,0 +1,8 @@
+{
+  "name": "TransitionComparator",
+  "key": {
+    "int": 477,
+    "string": "transitioncomparator"
+  },
+  "abstract": true
+}
\ No newline at end of file
diff --git a/dev/defs/animation/transition_condition.json b/dev/defs/animation/transition_condition.json
new file mode 100644
index 0000000..660dbfc
--- /dev/null
+++ b/dev/defs/animation/transition_condition.json
@@ -0,0 +1,33 @@
+{
+  "name": "TransitionCondition",
+  "key": {
+    "int": 476,
+    "string": "transitioncondition"
+  },
+  "abstract": true,
+  "properties": {
+    "transitionId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 153,
+        "string": "transitionid"
+      },
+      "description": "Id of the transition this condition belongs to.",
+      "runtime": false
+    },
+    "conditionOrder": {
+      "type": "FractionalIndex",
+      "initialValue": "FractionalIndex.invalid",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 154,
+        "string": "conditionorder"
+      },
+      "description": "Order value for condition in a transition.",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/transition_input_condition.json b/dev/defs/animation/transition_input_condition.json
new file mode 100644
index 0000000..6a6331f
--- /dev/null
+++ b/dev/defs/animation/transition_input_condition.json
@@ -0,0 +1,22 @@
+{
+  "name": "TransitionInputCondition",
+  "key": {
+    "int": 67,
+    "string": "transitioninputcondition"
+  },
+  "abstract": true,
+  "extends": "animation/transition_condition.json",
+  "properties": {
+    "inputId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 155,
+        "string": "inputid"
+      },
+      "description": "Id of the StateMachineInput referenced."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/transition_number_condition.json b/dev/defs/animation/transition_number_condition.json
new file mode 100644
index 0000000..b581107
--- /dev/null
+++ b/dev/defs/animation/transition_number_condition.json
@@ -0,0 +1,18 @@
+{
+  "name": "TransitionNumberCondition",
+  "key": {
+    "int": 70,
+    "string": "transitionnumbercondition"
+  },
+  "extends": "animation/transition_value_condition.json",
+  "properties": {
+    "value": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 157,
+        "string": "value"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/transition_property_artboard_comparator.json b/dev/defs/animation/transition_property_artboard_comparator.json
new file mode 100644
index 0000000..a9502eb
--- /dev/null
+++ b/dev/defs/animation/transition_property_artboard_comparator.json
@@ -0,0 +1,19 @@
+{
+  "name": "TransitionPropertyArtboardComparator",
+  "key": {
+    "int": 496,
+    "string": "transitionpropertyartboardcomparator"
+  },
+  "extends": "animation/transition_property_comparator.json",
+  "properties": {
+    "propertyType": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 677,
+        "string": "propertytype"
+      },
+      "description": "Integer representation of the artboard's property used for condition"
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/transition_property_comparator.json b/dev/defs/animation/transition_property_comparator.json
new file mode 100644
index 0000000..ee708a2
--- /dev/null
+++ b/dev/defs/animation/transition_property_comparator.json
@@ -0,0 +1,9 @@
+{
+  "name": "TransitionPropertyComparator",
+  "key": {
+    "int": 478,
+    "string": "transitionpropertycomparator"
+  },
+  "abstract": true,
+  "extends": "animation/transition_comparator.json"
+}
\ No newline at end of file
diff --git a/dev/defs/animation/transition_property_viewmodel_comparator.json b/dev/defs/animation/transition_property_viewmodel_comparator.json
new file mode 100644
index 0000000..94b11fb
--- /dev/null
+++ b/dev/defs/animation/transition_property_viewmodel_comparator.json
@@ -0,0 +1,19 @@
+{
+  "name": "TransitionPropertyViewModelComparator",
+  "key": {
+    "int": 479,
+    "string": "transitionpropertyviewmodelcomparator"
+  },
+  "extends": "animation/transition_property_comparator.json",
+  "properties": {
+    "bindablePropertyId": {
+      "type": "Id",
+      "key": {
+        "int": 646,
+        "string": "bindablepropertyid"
+      },
+      "description": "Id of the bindable property used as comparator",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/transition_trigger_condition.json b/dev/defs/animation/transition_trigger_condition.json
new file mode 100644
index 0000000..855dc65
--- /dev/null
+++ b/dev/defs/animation/transition_trigger_condition.json
@@ -0,0 +1,8 @@
+{
+  "name": "TransitionTriggerCondition",
+  "key": {
+    "int": 68,
+    "string": "transitiontriggercondition"
+  },
+  "extends": "animation/transition_input_condition.json"
+}
\ No newline at end of file
diff --git a/dev/defs/animation/transition_value_boolean_comparator.json b/dev/defs/animation/transition_value_boolean_comparator.json
new file mode 100644
index 0000000..c2ee3bf
--- /dev/null
+++ b/dev/defs/animation/transition_value_boolean_comparator.json
@@ -0,0 +1,18 @@
+{
+  "name": "TransitionValueBooleanComparator",
+  "key": {
+    "int": 481,
+    "string": "transitionvaluebooleancomparator"
+  },
+  "extends": "animation/transition_value_comparator.json",
+  "properties": {
+    "value": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 647,
+        "string": "value"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/transition_value_color_comparator.json b/dev/defs/animation/transition_value_color_comparator.json
new file mode 100644
index 0000000..ec5d41f
--- /dev/null
+++ b/dev/defs/animation/transition_value_color_comparator.json
@@ -0,0 +1,18 @@
+{
+  "name": "TransitionValueColorComparator",
+  "key": {
+    "int": 483,
+    "string": "transitionvaluecolorcomparator"
+  },
+  "extends": "animation/transition_value_comparator.json",
+  "properties": {
+    "value": {
+      "type": "Color",
+      "initialValue": "0xFF1D1D1D",
+      "key": {
+        "int": 651,
+        "string": "value"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/transition_value_comparator.json b/dev/defs/animation/transition_value_comparator.json
new file mode 100644
index 0000000..d428fad
--- /dev/null
+++ b/dev/defs/animation/transition_value_comparator.json
@@ -0,0 +1,9 @@
+{
+  "name": "TransitionValueComparator",
+  "key": {
+    "int": 480,
+    "string": "transitionvaluecomparator"
+  },
+  "abstract": true,
+  "extends": "animation/transition_comparator.json"
+}
\ No newline at end of file
diff --git a/dev/defs/animation/transition_value_condition.json b/dev/defs/animation/transition_value_condition.json
new file mode 100644
index 0000000..381326f
--- /dev/null
+++ b/dev/defs/animation/transition_value_condition.json
@@ -0,0 +1,20 @@
+{
+  "name": "TransitionValueCondition",
+  "key": {
+    "int": 69,
+    "string": "transitionvaluecondition"
+  },
+  "abstract": true,
+  "extends": "animation/transition_input_condition.json",
+  "properties": {
+    "opValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 156,
+        "string": "opvalue"
+      },
+      "description": "Integer representation of the StateMachineOp enum."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/transition_value_enum_comparator.json b/dev/defs/animation/transition_value_enum_comparator.json
new file mode 100644
index 0000000..54c364c
--- /dev/null
+++ b/dev/defs/animation/transition_value_enum_comparator.json
@@ -0,0 +1,21 @@
+{
+  "name": "TransitionValueEnumComparator",
+  "key": {
+    "int": 485,
+    "string": "transitionvalueenumcomparator"
+  },
+  "extends": "animation/transition_value_comparator.json",
+  "properties": {
+    "value": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 653,
+        "string": "value"
+      },
+      "description": "The id of the enum to compare the condition to"
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/transition_value_number_comparator.json b/dev/defs/animation/transition_value_number_comparator.json
new file mode 100644
index 0000000..7ad7656
--- /dev/null
+++ b/dev/defs/animation/transition_value_number_comparator.json
@@ -0,0 +1,18 @@
+{
+  "name": "TransitionValueNumberComparator",
+  "key": {
+    "int": 484,
+    "string": "transitionvaluenumbercomparator"
+  },
+  "extends": "animation/transition_value_comparator.json",
+  "properties": {
+    "value": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 652,
+        "string": "value"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/transition_value_string_comparator.json b/dev/defs/animation/transition_value_string_comparator.json
new file mode 100644
index 0000000..7f7cdd7
--- /dev/null
+++ b/dev/defs/animation/transition_value_string_comparator.json
@@ -0,0 +1,18 @@
+{
+  "name": "TransitionValueStringComparator",
+  "key": {
+    "int": 486,
+    "string": "transitionvaluestringcomparator"
+  },
+  "extends": "animation/transition_value_comparator.json",
+  "properties": {
+    "value": {
+      "type": "String",
+      "initialValue": "''",
+      "key": {
+        "int": 654,
+        "string": "value"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/animation/transition_viewmodel_condition.json b/dev/defs/animation/transition_viewmodel_condition.json
new file mode 100644
index 0000000..714abb1
--- /dev/null
+++ b/dev/defs/animation/transition_viewmodel_condition.json
@@ -0,0 +1,41 @@
+{
+  "name": "TransitionViewModelCondition",
+  "key": {
+    "int": 482,
+    "string": "transitionviewmodelcondition"
+  },
+  "extends": "animation/transition_condition.json",
+  "properties": {
+    "leftComparatorId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 648,
+        "string": "leftcomparatorid"
+      },
+      "description": "The id of the left comaprand to use for this condition"
+    },
+    "rightComparatorId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 649,
+        "string": "rightcomparatorid"
+      },
+      "description": "The id of the right comaprand to use for this condition"
+    },
+    "opValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 650,
+        "string": "opvalue"
+      },
+      "description": "Integer representation of the StateMachineOp enum."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/artboard.json b/dev/defs/artboard.json
new file mode 100644
index 0000000..bb52736
--- /dev/null
+++ b/dev/defs/artboard.json
@@ -0,0 +1,104 @@
+{
+  "name": "Artboard",
+  "key": {
+    "int": 1,
+    "string": "artboard"
+  },
+  "extends": "layout_component.json",
+  "properties": {
+    "originX": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 11,
+        "string": "ox"
+      },
+      "description": "Origin x in normalized coordinates (0.5 = center, 0 = left, 1 = right)."
+    },
+    "originY": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 12,
+        "string": "oy"
+      },
+      "description": "Origin y in normalized coordinates (0.5 = center, 0 = top, 1 = bottom)."
+    },
+    "defaultStateMachineId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 236,
+        "string": "defaultStateMachineId"
+      },
+      "description": "The default StateMachine attached to this artboard automatically when it is initialized."
+    },
+    "animationsScrollOffset": {
+      "type": "double",
+      "initialValue": "0.0",
+      "key": {
+        "int": 257,
+        "string": "animationsscrolloffset"
+      },
+      "description": "Scroll offset of the animation list",
+      "runtime": false,
+      "coop": false
+    },
+    "expandedComponents": {
+      "type": "List<Id>",
+      "initialValue": "[]",
+      "key": {
+        "int": 267,
+        "string": "expandedcomponents"
+      },
+      "description": "List of expanded components",
+      "runtime": false,
+      "coop": false
+    },
+    "expandedFolders": {
+      "type": "List<Id>",
+      "initialValue": "[]",
+      "key": {
+        "int": 283,
+        "string": "expandedanimationfolders"
+      },
+      "description": "List of expanded folders",
+      "runtime": false,
+      "coop": false
+    },
+    "selectedAnimations": {
+      "type": "List<Id>",
+      "initialValue": "[]",
+      "key": {
+        "int": 291,
+        "string": "selectedanimations"
+      },
+      "description": "List of selected animations",
+      "runtime": false,
+      "coop": false
+    },
+    "viewModelId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 583,
+        "string": "viewmodelid"
+      },
+      "description": "The view model attached to this artboard data context."
+    },
+    "viewModelInstanceId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 584,
+        "string": "viewmodelinstanceid"
+      },
+      "description": "A view model instance attached for editing purposes.",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/assets/asset.json b/dev/defs/assets/asset.json
new file mode 100644
index 0000000..0abfc8b
--- /dev/null
+++ b/dev/defs/assets/asset.json
@@ -0,0 +1,39 @@
+{
+  "name": "Asset",
+  "key": {
+    "int": 99,
+    "string": "asset"
+  },
+  "abstract": true,
+  "properties": {
+    "name": {
+      "type": "String",
+      "initialValue": "''",
+      "key": {
+        "int": 203,
+        "string": "name"
+      },
+      "description": "Name of the asset"
+    },
+    "parentId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 209,
+        "string": "parentid"
+      },
+      "description": "Id of the parent asset",
+      "runtime": false
+    },
+    "order": {
+      "type": "FractionalIndex",
+      "initialValue": "FractionalIndex.invalid",
+      "key": {
+        "int": 205,
+        "string": "order"
+      },
+      "description": "Order this asset shows up in the assets panel",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/assets/audio_asset.json b/dev/defs/assets/audio_asset.json
new file mode 100644
index 0000000..aed1f62
--- /dev/null
+++ b/dev/defs/assets/audio_asset.json
@@ -0,0 +1,60 @@
+{
+    "name": "AudioAsset",
+    "key": {
+        "int": 406,
+        "string": "audioasset"
+    },
+    "extends": "assets/export_audio.json",
+    "properties": {
+        "sampleRate": {
+            "type": "uint",
+            "initialValue": "0",
+            "key": {
+                "int": 473,
+                "string": "samplerate"
+            },
+            "description": "Sample rate of the source audio file.",
+            "runtime": false
+        },
+        "channels": {
+            "type": "uint",
+            "initialValue": "0",
+            "key": {
+                "int": 474,
+                "string": "channels"
+            },
+            "description": "Channel count of the source audio file.",
+            "runtime": false
+        },
+        "durationSeconds": {
+            "type": "double",
+            "initialValue": "0",
+            "key": {
+                "int": 475,
+                "string": "durationseconds"
+            },
+            "description": "Duration in seconds of the source audio file.",
+            "runtime": false
+        },
+        "formatValue": {
+            "type": "uint",
+            "initialValue": "0",
+            "key": {
+                "int": 476,
+                "string": "formatvalue"
+            },
+            "description": "Container format value of the source audio file.",
+            "runtime": false
+        },
+        "isCustom": {
+            "type": "bool",
+            "initialValue": "true",
+            "key": {
+                "int": 426,
+                "string": "iscustom"
+            },
+            "description": "True if a custom asset, i.e the user has uploaded it",
+            "runtime": false
+        }
+    }
+}
\ No newline at end of file
diff --git a/dev/defs/assets/drawable_asset.json b/dev/defs/assets/drawable_asset.json
new file mode 100644
index 0000000..97f1013
--- /dev/null
+++ b/dev/defs/assets/drawable_asset.json
@@ -0,0 +1,29 @@
+{
+  "name": "DrawableAsset",
+  "key": {
+    "int": 104,
+    "string": "drawableasset"
+  },
+  "abstract": true,
+  "extends": "assets/file_asset.json",
+  "properties": {
+    "height": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 207,
+        "string": "height"
+      },
+      "description": "Height of the original asset uploaded"
+    },
+    "width": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 208,
+        "string": "width"
+      },
+      "description": "Width of the original asset uploaded"
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/assets/export_audio.json b/dev/defs/assets/export_audio.json
new file mode 100644
index 0000000..37c747c
--- /dev/null
+++ b/dev/defs/assets/export_audio.json
@@ -0,0 +1,40 @@
+{
+    "name": "ExportAudio",
+    "key": {
+        "int": 422,
+        "string": "exportaudio"
+    },
+    "abstract": true,
+    "extends": "assets/file_asset.json",
+    "properties": {
+        "exportFormatValue": {
+            "type": "uint",
+            "initialValue": "0",
+            "key": {
+                "int": 527,
+                "string": "formatvalue"
+            },
+            "description": "Start seconds of this clip",
+            "runtime": false
+        },
+        "exportQualityValue": {
+            "type": "uint",
+            "initialValue": "0",
+            "key": {
+                "int": 528,
+                "string": "qualityvalue"
+            },
+            "description": "Start seconds of this clip",
+            "runtime": false
+        },
+        "volume": {
+            "type": "double",
+            "initialValue": "1",
+            "key": {
+                "int": 530,
+                "string": "volume"
+            },
+            "description": "Volume applied to all instances of this audio asset."
+        }
+    }
+}
\ No newline at end of file
diff --git a/dev/defs/assets/file_asset.json b/dev/defs/assets/file_asset.json
new file mode 100644
index 0000000..3173b81
--- /dev/null
+++ b/dev/defs/assets/file_asset.json
@@ -0,0 +1,60 @@
+{
+  "name": "FileAsset",
+  "key": {
+    "int": 103,
+    "string": "fileasset"
+  },
+  "abstract": true,
+  "extends": "assets/asset.json",
+  "properties": {
+    "assetId": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 204,
+        "string": "assetid"
+      },
+      "description": "Id of the asset as stored on the backend"
+    },
+    "size": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 211,
+        "string": "size"
+      },
+      "description": "Size of the asset in bytes",
+      "runtime": false
+    },
+    "exportTypeValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 358,
+        "string": "exporttypevalue"
+      },
+      "description": "How to export the asset: embedded, referenced or cdn",
+      "runtime": false
+    },
+    "cdnUuid": {
+      "type": "Bytes",
+      "encoded": true,
+      "key": {
+        "int": 359,
+        "string": "cdnuuid"
+      },
+      "description": "The cdn uuid if it exists",
+      "coop": false
+    },
+    "cdnBaseUrl": {
+      "type": "String",
+      "initialValue": "'https://public.rive.app/cdn/uuid'",
+      "key": {
+        "int": 362,
+        "string": "cdnbaseurl"
+      },
+      "description": "Set the base url of our cdn.",
+      "coop": false
+    }
+  }
+}
diff --git a/dev/defs/assets/file_asset_contents.json b/dev/defs/assets/file_asset_contents.json
new file mode 100644
index 0000000..626b2e0
--- /dev/null
+++ b/dev/defs/assets/file_asset_contents.json
@@ -0,0 +1,30 @@
+{
+  "name": "FileAssetContents",
+  "key": {
+    "int": 106,
+    "string": "file_asset_contents"
+  },
+  "properties": {
+    "assetId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 213,
+        "string": "assetid"
+      },
+      "description": "The id of the asset these bytes belong to.",
+      "runtime": false,
+      "coop": false
+    },
+    "bytes": {
+      "type": "Bytes",
+      "encoded": true,
+      "key": {
+        "int": 212,
+        "string": "bytes"
+      },
+      "description": "Byte data of the file.",
+      "coop": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/assets/folder.json b/dev/defs/assets/folder.json
new file mode 100644
index 0000000..d80714a
--- /dev/null
+++ b/dev/defs/assets/folder.json
@@ -0,0 +1,8 @@
+{
+  "name": "Folder",
+  "key": {
+    "int": 102,
+    "string": "folder"
+  },
+  "extends": "assets/asset.json"
+}
\ No newline at end of file
diff --git a/dev/defs/assets/font_asset.json b/dev/defs/assets/font_asset.json
new file mode 100644
index 0000000..01a5058
--- /dev/null
+++ b/dev/defs/assets/font_asset.json
@@ -0,0 +1,40 @@
+{
+  "name": "FontAsset",
+  "key": {
+    "int": 141,
+    "string": "fontasset"
+  },
+  "extends": "assets/file_asset.json",
+  "properties": {
+    "isCustom": {
+      "type": "bool",
+      "initialValue": "true",
+      "key": {
+        "int": 309,
+        "string": "iscustom"
+      },
+      "description": "True if the font is a custom font, i.e the user has uploaded it",
+      "runtime": false
+    },
+    "includeTypeValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 360,
+        "string": "includetypevalue"
+      },
+      "description": "Which characters of the font to include for export: all, used or custom.",
+      "runtime": false
+    },
+    "includedUnicodes": {
+      "type": "String",
+      "initialValue": "''",
+      "key": {
+        "int": 361,
+        "string": "includedunicodes"
+      },
+      "description": "Included unicodes when exporting, in U+0040-0080, format.",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/assets/image_asset.json b/dev/defs/assets/image_asset.json
new file mode 100644
index 0000000..eab997e
--- /dev/null
+++ b/dev/defs/assets/image_asset.json
@@ -0,0 +1,30 @@
+{
+  "name": "ImageAsset",
+  "key": {
+    "int": 105,
+    "string": "imageasset"
+  },
+  "extends": "assets/drawable_asset.json",
+  "properties": {
+    "format": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 241,
+        "string": "format"
+      },
+      "description": "Image format we want to use for this assets's mutations",
+      "runtime": false
+    },
+    "quality": {
+      "type": "double",
+      "initialValue": "0.75",
+      "key": {
+        "int": 242,
+        "string": "quality"
+      },
+      "description": "Quality percentage for this image mutation",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/assets/layer_image_asset.json b/dev/defs/assets/layer_image_asset.json
new file mode 100644
index 0000000..055c8cc
--- /dev/null
+++ b/dev/defs/assets/layer_image_asset.json
@@ -0,0 +1,41 @@
+{
+  "name": "LayerImageAsset",
+  "key": {
+    "int": 120,
+    "string": "layerimageasset"
+  },
+  "extends": "assets/image_asset.json",
+  "runtime": false,
+  "properties": {
+    "layer": {
+      "type": "uint",
+      "initialValue": "-1",
+      "key": {
+        "int": 233,
+        "string": "layer"
+      },
+      "description": "Layer ID as it is analysed by the backend",
+      "runtime": false
+    },
+    "x": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 234,
+        "string": "x"
+      },
+      "description": "x offset for this layer",
+      "runtime": false
+    },
+    "y": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 235,
+        "string": "y"
+      },
+      "description": "y offset for this layer",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/assets/layered_asset.json b/dev/defs/assets/layered_asset.json
new file mode 100644
index 0000000..09a1713
--- /dev/null
+++ b/dev/defs/assets/layered_asset.json
@@ -0,0 +1,9 @@
+{
+  "name": "LayeredAsset",
+  "key": {
+    "int": 119,
+    "string": "layeredasset"
+  },
+  "extends": "assets/drawable_asset.json",
+  "runtime": false
+}
\ No newline at end of file
diff --git a/dev/defs/assets/lottie_asset.json b/dev/defs/assets/lottie_asset.json
new file mode 100644
index 0000000..ade6cb1
--- /dev/null
+++ b/dev/defs/assets/lottie_asset.json
@@ -0,0 +1,9 @@
+{
+  "name": "LottieAsset",
+  "key": {
+    "int": 133,
+    "string": "lottieasset"
+  },
+  "extends": "assets/drawable_asset.json",
+  "runtime": false
+}
\ No newline at end of file
diff --git a/dev/defs/assets/svg_asset.json b/dev/defs/assets/svg_asset.json
new file mode 100644
index 0000000..47489b6
--- /dev/null
+++ b/dev/defs/assets/svg_asset.json
@@ -0,0 +1,9 @@
+{
+  "name": "SVGAsset",
+  "key": {
+    "int": 132,
+    "string": "svgasset"
+  },
+  "extends": "assets/drawable_asset.json",
+  "runtime": false
+}
\ No newline at end of file
diff --git a/dev/defs/audio_event.json b/dev/defs/audio_event.json
new file mode 100644
index 0000000..3ee2a47
--- /dev/null
+++ b/dev/defs/audio_event.json
@@ -0,0 +1,21 @@
+{
+    "name": "AudioEvent",
+    "key": {
+        "int": 407,
+        "string": "audioevent"
+    },
+    "extends": "event.json",
+    "properties": {
+        "assetId": {
+            "type": "Id",
+            "typeRuntime": "uint",
+            "initialValue": "Core.missingId",
+            "initialValueRuntime": "-1",
+            "key": {
+                "int": 408,
+                "string": "assetid"
+            },
+            "description": "Audio asset to play when event fires"
+        }
+    }
+}
\ No newline at end of file
diff --git a/dev/defs/backboard.json b/dev/defs/backboard.json
new file mode 100644
index 0000000..a57597d
--- /dev/null
+++ b/dev/defs/backboard.json
@@ -0,0 +1,317 @@
+{
+  "name": "Backboard",
+  "key": {
+    "int": 23,
+    "string": "backboard"
+  },
+  "properties": {
+    "activeArtboardId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 43,
+        "string": "activeArtboardId"
+      },
+      "description": "Identifier used to track the last active artboard.",
+      "runtime": false,
+      "coop": false
+    },
+    "mainArtboardId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 44,
+        "string": "mainArtboardId"
+      },
+      "description": "Identifier used to track the main artboard (this is the default one that shows up in runtimes unless specified).",
+      "runtime": false
+    },
+    "colorValue": {
+      "type": "Color",
+      "initialValue": "0xFF1D1D1D",
+      "key": {
+        "int": 45,
+        "string": "colorValue"
+      },
+      "description": "The background color.",
+      "runtime": false
+    },
+    "animateColorValue": {
+      "type": "Color",
+      "initialValue": "0xFF1D1D1D",
+      "key": {
+        "int": 354,
+        "string": "animatecolorvalue"
+      },
+      "description": "The background color while in Animate mode",
+      "runtime": false,
+      "coop": false
+    },
+    "editorMode": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 135,
+        "string": "editormode"
+      },
+      "description": "Edit time mode.",
+      "runtime": false,
+      "coop": false
+    },
+    "treeMode": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 247,
+        "string": "treemode"
+      },
+      "description": "Edit time hierarchy/assets/events mode.",
+      "runtime": false,
+      "coop": false
+    },
+    "animationPanelSize": {
+      "type": "double",
+      "initialValue": "300.0",
+      "key": {
+        "int": 258,
+        "string": "animationpanelsize"
+      },
+      "description": "Size of the animation panel",
+      "runtime": false,
+      "coop": false
+    },
+    "hierarchyPanelSize": {
+      "type": "double",
+      "initialValue": "300.0",
+      "key": {
+        "int": 259,
+        "string": "hierarchypanelsize"
+      },
+      "description": "Size of the Hierarchy panel",
+      "runtime": false,
+      "coop": false
+    },
+    "stageX": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 260,
+        "string": "stagex"
+      },
+      "description": "X translation value of the stage in the editor.",
+      "runtime": false,
+      "coop": false
+    },
+    "stageY": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 261,
+        "string": "stagey"
+      },
+      "description": "Y translation value of the stage in the editor.",
+      "runtime": false,
+      "coop": false
+    },
+    "stageZoom": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 262,
+        "string": "stagezoom"
+      },
+      "description": "Zoom value of the stage in the editor. Initial value is 0 to indicate it is not set.",
+      "runtime": false,
+      "coop": false
+    },
+    "rulersEnabled": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 263,
+        "string": "rulersenabled"
+      },
+      "description": "If rulers are enabled for this file",
+      "runtime": false,
+      "coop": false
+    },
+    "snappingEnabled": {
+      "type": "bool",
+      "initialValue": "true",
+      "key": {
+        "int": 264,
+        "string": "snappingenabled"
+      },
+      "description": "If snapping are enabled for this file",
+      "runtime": false,
+      "coop": false
+    },
+    "bonesEnabled": {
+      "type": "bool",
+      "initialValue": "true",
+      "key": {
+        "int": 265,
+        "string": "bonesenabled"
+      },
+      "description": "If bones are enabled for this file",
+      "runtime": false,
+      "coop": false
+    },
+    "targetsEnabled": {
+      "type": "bool",
+      "initialValue": "true",
+      "key": {
+        "int": 379,
+        "string": "targetsenabled"
+      },
+      "description": "If targets and empty groups are enabled for this file",
+      "runtime": false,
+      "coop": false
+    },
+    "motionPathsEnabled": {
+      "type": "bool",
+      "initialValue": "true",
+      "key": {
+        "int": 351,
+        "string": "motionpathsenabled"
+      },
+      "description": "If motion paths are enabled for this file",
+      "runtime": false,
+      "coop": false
+    },
+    "showFinalPlaybackEnabled": {
+      "type": "bool",
+      "initialValue": "true",
+      "key": {
+        "int": 355,
+        "string": "showfinalplaybackenabled"
+      },
+      "description": "If final playback (hiding gizmos during playback) are enabled for this file",
+      "runtime": false,
+      "coop": false
+    },
+    "showModifierRange": {
+      "type": "bool",
+      "initialValue": "true",
+      "key": {
+        "int": 374,
+        "string": "showmodifierrange"
+      },
+      "description": "Whether text modifier ranges show on the stage",
+      "runtime": false,
+      "coop": false
+    },
+    "showModifierRangeValues": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 375,
+        "string": "showmodifierrangevalues"
+      },
+      "description": "Whether text modifier range values show on the stage",
+      "runtime": false,
+      "coop": false
+    },
+    "toolbarOutputAction": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 290,
+        "string": "toolbaroutputaction"
+      },
+      "description": "Primary output action in the toolbar (share, publish, download)",
+      "runtime": false,
+      "coop": false
+    },
+    "assetsPanelState": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 315,
+        "string": "assetspanelstate"
+      },
+      "runtime": false,
+      "coop": false
+    },
+    "defaultFontFamily": {
+      "type": "String",
+      "initialValue": "'Inter'",
+      "key": {
+        "int": 343,
+        "string": "defaultfontfamily"
+      },
+      "runtime": false,
+      "coop": false
+    },
+    "isDefaultFontFamilyCustom": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 352,
+        "string": "isdefaultfontfamilycustom"
+      },
+      "runtime": false,
+      "coop": false
+    },
+    "defaultFontStyle": {
+      "type": "String",
+      "initialValue": "'Regular'",
+      "key": {
+        "int": 344,
+        "string": "defaultfontstyle"
+      },
+      "runtime": false,
+      "coop": false
+    },
+    "defaultFontSize": {
+      "type": "double",
+      "initialValue": "20.0",
+      "key": {
+        "int": 345,
+        "string": "defaultfontsize"
+      },
+      "runtime": false,
+      "coop": false
+    },
+    "defaultTextAlign": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 346,
+        "string": "defaulttextalign"
+      },
+      "runtime": false,
+      "coop": false
+    },
+    "defaultTextOverflow": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 347,
+        "string": "defaulttextoverflow"
+      },
+      "runtime": false,
+      "coop": false
+    },
+    "defaultTextHighlightTextRuns": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 348,
+        "string": "defaulttexthighlighttextruns"
+      },
+      "runtime": false,
+      "coop": false
+    },
+    "defaultCollapseTags": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 387,
+        "string": "defaultcollapsetags"
+      },
+      "runtime": false,
+      "coop": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/bones/bone.json b/dev/defs/bones/bone.json
new file mode 100644
index 0000000..8c95a14
--- /dev/null
+++ b/dev/defs/bones/bone.json
@@ -0,0 +1,19 @@
+{
+  "name": "Bone",
+  "key": {
+    "int": 40,
+    "string": "bone"
+  },
+  "extends": "bones/skeletal_component.json",
+  "properties": {
+    "length": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 89,
+        "string": "length"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/bones/cubic_weight.json b/dev/defs/bones/cubic_weight.json
new file mode 100644
index 0000000..86b854d
--- /dev/null
+++ b/dev/defs/bones/cubic_weight.json
@@ -0,0 +1,42 @@
+{
+  "name": "CubicWeight",
+  "key": {
+    "int": 46,
+    "string": "cubicweight"
+  },
+  "extends": "bones/weight.json",
+  "properties": {
+    "inValues": {
+      "type": "uint",
+      "initialValue": "255",
+      "key": {
+        "int": 110,
+        "string": "invalues"
+      }
+    },
+    "inIndices": {
+      "type": "uint",
+      "initialValue": "1",
+      "key": {
+        "int": 111,
+        "string": "inindices"
+      }
+    },
+    "outValues": {
+      "type": "uint",
+      "initialValue": "255",
+      "key": {
+        "int": 112,
+        "string": "outvalues"
+      }
+    },
+    "outIndices": {
+      "type": "uint",
+      "initialValue": "1",
+      "key": {
+        "int": 113,
+        "string": "outindices"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/bones/root_bone.json b/dev/defs/bones/root_bone.json
new file mode 100644
index 0000000..fefc6b3
--- /dev/null
+++ b/dev/defs/bones/root_bone.json
@@ -0,0 +1,32 @@
+{
+  "name": "RootBone",
+  "key": {
+    "int": 41,
+    "string": "rootbone"
+  },
+  "extends": "bones/bone.json",
+  "properties": {
+    "x": {
+      "type": "double",
+      "initialValue": "0",
+      "overrideGet": true,
+      "animates": true,
+      "group": "position",
+      "key": {
+        "int": 90,
+        "string": "x"
+      }
+    },
+    "y": {
+      "type": "double",
+      "initialValue": "0",
+      "overrideGet": true,
+      "animates": true,
+      "group": "position",
+      "key": {
+        "int": 91,
+        "string": "y"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/bones/skeletal_component.json b/dev/defs/bones/skeletal_component.json
new file mode 100644
index 0000000..1836f54
--- /dev/null
+++ b/dev/defs/bones/skeletal_component.json
@@ -0,0 +1,9 @@
+{
+  "name": "SkeletalComponent",
+  "key": {
+    "int": 39,
+    "string": "skeletalcomponent"
+  },
+  "abstract": true,
+  "extends": "transform_component.json"
+}
\ No newline at end of file
diff --git a/dev/defs/bones/skin.json b/dev/defs/bones/skin.json
new file mode 100644
index 0000000..1b5f65f
--- /dev/null
+++ b/dev/defs/bones/skin.json
@@ -0,0 +1,64 @@
+{
+  "name": "Skin",
+  "key": {
+    "int": 43,
+    "string": "skin"
+  },
+  "extends": "container_component.json",
+  "properties": {
+    "xx": {
+      "type": "double",
+      "initialValue": "1",
+      "key": {
+        "int": 104,
+        "string": "xx"
+      },
+      "description": "x component of x unit vector in the bind transform"
+    },
+    "yx": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 105,
+        "string": "yx"
+      },
+      "description": "y component of x unit vector in the bind transform"
+    },
+    "xy": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 106,
+        "string": "xy"
+      },
+      "description": "x component of y unit vector in the bind transform"
+    },
+    "yy": {
+      "type": "double",
+      "initialValue": "1",
+      "key": {
+        "int": 107,
+        "string": "yy"
+      },
+      "description": "y component of y unit vector in the bind transform"
+    },
+    "tx": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 108,
+        "string": "tx"
+      },
+      "description": "x position component of the bind transform"
+    },
+    "ty": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 109,
+        "string": "ty"
+      },
+      "description": "y position component of the bind transform"
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/bones/tendon.json b/dev/defs/bones/tendon.json
new file mode 100644
index 0000000..f92567a
--- /dev/null
+++ b/dev/defs/bones/tendon.json
@@ -0,0 +1,75 @@
+{
+  "name": "Tendon",
+  "key": {
+    "int": 44,
+    "string": "tendon"
+  },
+  "extends": "component.json",
+  "properties": {
+    "boneId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 95,
+        "string": "boneid"
+      },
+      "description": "Identifier used to track the bone this tendon connects to."
+    },
+    "xx": {
+      "type": "double",
+      "initialValue": "1",
+      "key": {
+        "int": 96,
+        "string": "xx"
+      },
+      "description": "x component of x unit vector in the bind transform"
+    },
+    "yx": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 97,
+        "string": "yx"
+      },
+      "description": "y component of x unit vector in the bind transform"
+    },
+    "xy": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 98,
+        "string": "xy"
+      },
+      "description": "x component of y unit vector in the bind transform"
+    },
+    "yy": {
+      "type": "double",
+      "initialValue": "1",
+      "key": {
+        "int": 99,
+        "string": "yy"
+      },
+      "description": "y component of y unit vector in the bind transform"
+    },
+    "tx": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 100,
+        "string": "tx"
+      },
+      "description": "x position component of the bind transform"
+    },
+    "ty": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 101,
+        "string": "ty"
+      },
+      "description": "y position component of the bind transform"
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/bones/weight.json b/dev/defs/bones/weight.json
new file mode 100644
index 0000000..889d283
--- /dev/null
+++ b/dev/defs/bones/weight.json
@@ -0,0 +1,26 @@
+{
+  "name": "Weight",
+  "key": {
+    "int": 45,
+    "string": "weight"
+  },
+  "extends": "component.json",
+  "properties": {
+    "values": {
+      "type": "uint",
+      "initialValue": "255",
+      "key": {
+        "int": 102,
+        "string": "values"
+      }
+    },
+    "indices": {
+      "type": "uint",
+      "initialValue": "1",
+      "key": {
+        "int": 103,
+        "string": "indices"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/clipboard_header.json b/dev/defs/clipboard_header.json
new file mode 100644
index 0000000..22ad63b
--- /dev/null
+++ b/dev/defs/clipboard_header.json
@@ -0,0 +1,40 @@
+{
+  "name": "ClipboardHeader",
+  "key": {
+    "int": 146,
+    "string": "clipboardheader"
+  },
+  "runtime": false,
+  "properties": {
+    "fileId": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 293,
+        "string": "fileid"
+      },
+      "description": "File clipboard contents has come from.",
+      "runtime": false
+    },
+    "projectId": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 294,
+        "string": "projectid"
+      },
+      "description": "Project clipboard contents has come from.",
+      "runtime": false
+    },
+    "clipboardTypeValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 295,
+        "string": "clipboardtypevalue"
+      },
+      "description": "Type of clipboard used to read these bytes",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/component.json b/dev/defs/component.json
new file mode 100644
index 0000000..75fd1e8
--- /dev/null
+++ b/dev/defs/component.json
@@ -0,0 +1,70 @@
+{
+  "name": "Component",
+  "key": {
+    "int": 10,
+    "string": "component"
+  },
+  "abstract": true,
+  "properties": {
+    "dependentIds": {
+      "type": "List<Id>",
+      "initialValue": "[]",
+      "key": {
+        "int": 3,
+        "string": "dependentIds"
+      },
+      "description": "List of integer ids for objects registered in the same context that depend on this object.",
+      "runtime": false
+    },
+    "name": {
+      "type": "String",
+      "initialValue": "''",
+      "key": {
+        "int": 4,
+        "string": "name"
+      },
+      "description": "Non-unique identifier, used to give friendly names to elements in the hierarchy. Runtimes provide an API for finding components by this [name]."
+    },
+    "parentId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 5,
+        "string": "parentId"
+      },
+      "description": "Identifier used to track parent ContainerComponent."
+    },
+    "tagIds": {
+      "type": "List<Id>",
+      "initialValue": "[]",
+      "key": {
+        "int": 382,
+        "string": "tagids"
+      },
+      "description": "Assigned tag ids.",
+      "runtime": false
+    },
+    "childOrder": {
+      "type": "FractionalIndex",
+      "initialValue": "FractionalIndex.invalid",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 6,
+        "string": "childOrder"
+      },
+      "description": "Order value for sorting child elements in ContainerComponent parent.",
+      "runtime": false
+    },
+    "flags": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 130,
+        "string": "flags"
+      },
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/constraints/constraint.json b/dev/defs/constraints/constraint.json
new file mode 100644
index 0000000..baf6931
--- /dev/null
+++ b/dev/defs/constraints/constraint.json
@@ -0,0 +1,21 @@
+{
+  "name": "Constraint",
+  "key": {
+    "int": 79,
+    "string": "constraint"
+  },
+  "abstract": true,
+  "extends": "component.json",
+  "properties": {
+    "strength": {
+      "type": "double",
+      "initialValue": "1.0",
+      "animates": true,
+      "key": {
+        "int": 172,
+        "string": "strength"
+      },
+      "description": "Strength of the constraint. 0 means off. 1 means fully constraining."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/constraints/distance_constraint.json b/dev/defs/constraints/distance_constraint.json
new file mode 100644
index 0000000..6ba3093
--- /dev/null
+++ b/dev/defs/constraints/distance_constraint.json
@@ -0,0 +1,29 @@
+{
+  "name": "DistanceConstraint",
+  "key": {
+    "int": 82,
+    "string": "distanceconstraint"
+  },
+  "extends": "constraints/targeted_constraint.json",
+  "properties": {
+    "distance": {
+      "type": "double",
+      "initialValue": "100.0",
+      "animates": true,
+      "key": {
+        "int": 177,
+        "string": "distance"
+      },
+      "description": "The unit distance the constraint will move the constrained object relative to the target."
+    },
+    "modeValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 178,
+        "string": "modevalue"
+      },
+      "description": "Backing value for the mode enum."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/constraints/follow_path_constraint.json b/dev/defs/constraints/follow_path_constraint.json
new file mode 100644
index 0000000..5b44ed7
--- /dev/null
+++ b/dev/defs/constraints/follow_path_constraint.json
@@ -0,0 +1,39 @@
+{
+  "name": "FollowPathConstraint",
+  "key": {
+    "int": 165,
+    "string": "followpathconstraint"
+  },
+  "extends": "constraints/transform_space_constraint.json",
+  "properties": {
+    "distance": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 363,
+        "string": "distance"
+      },
+      "description": "Distance along the path to follow."
+    },
+    "orient": {
+      "type": "bool",
+      "initialValue": "true",
+      "animates": true,
+      "key": {
+        "int": 364,
+        "string": "orient"
+      },
+      "description": "True when the orientation from the path is copied to the constrained transform."
+    },
+    "offset": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 365,
+        "string": "offset"
+      },
+      "description": "True when the local translation is used to offset the transformed one."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/constraints/ik_constraint.json b/dev/defs/constraints/ik_constraint.json
new file mode 100644
index 0000000..7bc8557
--- /dev/null
+++ b/dev/defs/constraints/ik_constraint.json
@@ -0,0 +1,29 @@
+{
+  "name": "IKConstraint",
+  "key": {
+    "int": 81,
+    "string": "ikconstraint"
+  },
+  "extends": "constraints/targeted_constraint.json",
+  "properties": {
+    "invertDirection": {
+      "type": "bool",
+      "initialValue": "false",
+      "animates": true,
+      "key": {
+        "int": 174,
+        "string": "invert_direction"
+      },
+      "description": "True when the direction taken towards the target should be inverted from the default."
+    },
+    "parentBoneCount": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 175,
+        "string": "parentbonecount"
+      },
+      "description": "The number of bones above this one that are influenced by this IK constraint."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/constraints/rotation_constraint.json b/dev/defs/constraints/rotation_constraint.json
new file mode 100644
index 0000000..db981c8
--- /dev/null
+++ b/dev/defs/constraints/rotation_constraint.json
@@ -0,0 +1,8 @@
+{
+  "name": "RotationConstraint",
+  "key": {
+    "int": 89,
+    "string": "rotationconstraint"
+  },
+  "extends": "constraints/transform_component_constraint.json"
+}
\ No newline at end of file
diff --git a/dev/defs/constraints/scale_constraint.json b/dev/defs/constraints/scale_constraint.json
new file mode 100644
index 0000000..a188ed7
--- /dev/null
+++ b/dev/defs/constraints/scale_constraint.json
@@ -0,0 +1,8 @@
+{
+  "name": "ScaleConstraint",
+  "key": {
+    "int": 88,
+    "string": "scaleconstraint"
+  },
+  "extends": "constraints/transform_component_constraint_y.json"
+}
\ No newline at end of file
diff --git a/dev/defs/constraints/targeted_constraint.json b/dev/defs/constraints/targeted_constraint.json
new file mode 100644
index 0000000..29b616d
--- /dev/null
+++ b/dev/defs/constraints/targeted_constraint.json
@@ -0,0 +1,22 @@
+{
+  "name": "TargetedConstraint",
+  "key": {
+    "int": 80,
+    "string": "targetedconstraint"
+  },
+  "abstract": true,
+  "extends": "constraints/constraint.json",
+  "properties": {
+    "targetId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 173,
+        "string": "targetid"
+      },
+      "description": "Identifier used to track the TransformComponent used as the target for the constraint."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/constraints/transform_component_constraint.json b/dev/defs/constraints/transform_component_constraint.json
new file mode 100644
index 0000000..e2f2f36
--- /dev/null
+++ b/dev/defs/constraints/transform_component_constraint.json
@@ -0,0 +1,83 @@
+{
+  "name": "TransformComponentConstraint",
+  "key": {
+    "int": 85,
+    "string": "transformcomponentconstraint"
+  },
+  "abstract": true,
+  "extends": "constraints/transform_space_constraint.json",
+  "properties": {
+    "minMaxSpaceValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 195,
+        "string": "minmaxspacevalue"
+      },
+      "description": "The min/max transform space."
+    },
+    "copyFactor": {
+      "type": "double",
+      "initialValue": "1",
+      "key": {
+        "int": 182,
+        "string": "copyfactor"
+      },
+      "description": "Copy factor."
+    },
+    "minValue": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 183,
+        "string": "minvalue"
+      },
+      "description": "Minimum value."
+    },
+    "maxValue": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 184,
+        "string": "maxvalue"
+      },
+      "description": "Maximum value."
+    },
+    "offset": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 188,
+        "string": "offset"
+      },
+      "description": "True when the original component (rotation/scale/translation) is used to offset the copied one."
+    },
+    "doesCopy": {
+      "type": "bool",
+      "initialValue": "true",
+      "key": {
+        "int": 189,
+        "string": "doescopy"
+      },
+      "description": "Whether the component is copied."
+    },
+    "min": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 190,
+        "string": "min"
+      },
+      "description": "Whether min is used."
+    },
+    "max": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 191,
+        "string": "max"
+      },
+      "description": "Whether max is used."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/constraints/transform_component_constraint_y.json b/dev/defs/constraints/transform_component_constraint_y.json
new file mode 100644
index 0000000..d26d40b
--- /dev/null
+++ b/dev/defs/constraints/transform_component_constraint_y.json
@@ -0,0 +1,65 @@
+{
+  "name": "TransformComponentConstraintY",
+  "key": {
+    "int": 86,
+    "string": "transformcomponentconstrainty"
+  },
+  "abstract": true,
+  "extends": "constraints/transform_component_constraint.json",
+  "properties": {
+    "copyFactorY": {
+      "type": "double",
+      "initialValue": "1",
+      "key": {
+        "int": 185,
+        "string": "copyfactory"
+      },
+      "description": "Copy factor."
+    },
+    "minValueY": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 186,
+        "string": "minvaluey"
+      },
+      "description": "Minimum value."
+    },
+    "maxValueY": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 187,
+        "string": "maxvaluey"
+      },
+      "description": "Maximum value."
+    },
+    "doesCopyY": {
+      "type": "bool",
+      "initialValue": "true",
+      "key": {
+        "int": 192,
+        "string": "doescopyy"
+      },
+      "description": "Whether the Y component is copied."
+    },
+    "minY": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 193,
+        "string": "miny"
+      },
+      "description": "Whether min Y is used."
+    },
+    "maxY": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 194,
+        "string": "maxx"
+      },
+      "description": "Whether max Y is used."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/constraints/transform_constraint.json b/dev/defs/constraints/transform_constraint.json
new file mode 100644
index 0000000..4a552c2
--- /dev/null
+++ b/dev/defs/constraints/transform_constraint.json
@@ -0,0 +1,30 @@
+{
+  "name": "TransformConstraint",
+  "key": {
+    "int": 83,
+    "string": "transformconstraint"
+  },
+  "extends": "constraints/transform_space_constraint.json",
+  "properties": {
+    "originX": {
+      "type": "double",
+      "initialValue": "0.0",
+      "animates": true,
+      "key": {
+        "int": 372,
+        "string": "originx"
+      },
+      "description": "Origin x in normalized coordinates (0.5 = center, 0 = left, 1 = right)."
+    },
+    "originY": {
+      "type": "double",
+      "initialValue": "0.0",
+      "animates": true,
+      "key": {
+        "int": 373,
+        "string": "originy"
+      },
+      "description": "Origin y in normalized coordinates (0.5 = center, 0 = top, 1 = bottom)."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/constraints/transform_space_constraint.json b/dev/defs/constraints/transform_space_constraint.json
new file mode 100644
index 0000000..9f2f9ee
--- /dev/null
+++ b/dev/defs/constraints/transform_space_constraint.json
@@ -0,0 +1,29 @@
+{
+  "name": "TransformSpaceConstraint",
+  "key": {
+    "int": 90,
+    "string": "transformspaceconstraint"
+  },
+  "abstract": true,
+  "extends": "constraints/targeted_constraint.json",
+  "properties": {
+    "sourceSpaceValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 179,
+        "string": "sourcespacevalue"
+      },
+      "description": "The source transform space."
+    },
+    "destSpaceValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 180,
+        "string": "destspacevalue"
+      },
+      "description": "The destination transform space."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/constraints/translation_constraint.json b/dev/defs/constraints/translation_constraint.json
new file mode 100644
index 0000000..ba9346e
--- /dev/null
+++ b/dev/defs/constraints/translation_constraint.json
@@ -0,0 +1,8 @@
+{
+  "name": "TranslationConstraint",
+  "key": {
+    "int": 87,
+    "string": "translationconstraint"
+  },
+  "extends": "constraints/transform_component_constraint_y.json"
+}
\ No newline at end of file
diff --git a/dev/defs/container_component.json b/dev/defs/container_component.json
new file mode 100644
index 0000000..1991ca6
--- /dev/null
+++ b/dev/defs/container_component.json
@@ -0,0 +1,9 @@
+{
+  "name": "ContainerComponent",
+  "key": {
+    "int": 11,
+    "string": "containercomponent"
+  },
+  "abstract": true,
+  "extends": "component.json"
+}
\ No newline at end of file
diff --git a/dev/defs/custom_property.json b/dev/defs/custom_property.json
new file mode 100644
index 0000000..98c3684
--- /dev/null
+++ b/dev/defs/custom_property.json
@@ -0,0 +1,9 @@
+{
+  "name": "CustomProperty",
+  "key": {
+    "int": 167,
+    "string": "customproperty"
+  },
+  "abstract": true,
+  "extends": "component.json"
+}
\ No newline at end of file
diff --git a/dev/defs/custom_property_boolean.json b/dev/defs/custom_property_boolean.json
new file mode 100644
index 0000000..d114ca6
--- /dev/null
+++ b/dev/defs/custom_property_boolean.json
@@ -0,0 +1,19 @@
+{
+  "name": "CustomPropertyBoolean",
+  "key": {
+    "int": 129,
+    "string": "custompropertyboolean"
+  },
+  "extends": "custom_property.json",
+  "properties": {
+    "propertyValue": {
+      "type": "bool",
+      "initialValue": "false",
+      "animates": true,
+      "key": {
+        "int": 245,
+        "string": "value"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/custom_property_number.json b/dev/defs/custom_property_number.json
new file mode 100644
index 0000000..3271b79
--- /dev/null
+++ b/dev/defs/custom_property_number.json
@@ -0,0 +1,19 @@
+{
+  "name": "CustomPropertyNumber",
+  "key": {
+    "int": 127,
+    "string": "custompropertynumber"
+  },
+  "extends": "custom_property.json",
+  "properties": {
+    "propertyValue": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 243,
+        "string": "value"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/custom_property_string.json b/dev/defs/custom_property_string.json
new file mode 100644
index 0000000..6f60ac9
--- /dev/null
+++ b/dev/defs/custom_property_string.json
@@ -0,0 +1,19 @@
+{
+  "name": "CustomPropertyString",
+  "key": {
+    "int": 130,
+    "string": "custompropertystring"
+  },
+  "extends": "custom_property.json",
+  "properties": {
+    "propertyValue": {
+      "type": "String",
+      "initialValue": "''",
+      "animates": true,
+      "key": {
+        "int": 246,
+        "string": "value"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/data_bind/bindable_property.json b/dev/defs/data_bind/bindable_property.json
new file mode 100644
index 0000000..dbc57e5
--- /dev/null
+++ b/dev/defs/data_bind/bindable_property.json
@@ -0,0 +1,20 @@
+{
+  "name": "BindableProperty",
+  "key": {
+    "int": 9,
+    "string": "bindableproperty"
+  },
+  "abstract": true,
+  "properties": {
+    "dataBindId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 633,
+        "string": "databindid"
+      },
+      "description": "Id of the data bind binded to this property",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/data_bind/bindable_property_boolean.json b/dev/defs/data_bind/bindable_property_boolean.json
new file mode 100644
index 0000000..78c24f2
--- /dev/null
+++ b/dev/defs/data_bind/bindable_property_boolean.json
@@ -0,0 +1,19 @@
+{
+  "name": "BindablePropertyBoolean",
+  "key": {
+    "int": 472,
+    "string": "bindablepropertyboolean"
+  },
+  "extends": "data_bind/bindable_property.json",
+  "properties": {
+    "propertyValue": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 634,
+        "string": "propertyvalue"
+      },
+      "description": "A property of type bool that can be used for data binding."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/data_bind/bindable_property_color.json b/dev/defs/data_bind/bindable_property_color.json
new file mode 100644
index 0000000..1dd5134
--- /dev/null
+++ b/dev/defs/data_bind/bindable_property_color.json
@@ -0,0 +1,19 @@
+{
+  "name": "BindablePropertyColor",
+  "key": {
+    "int": 475,
+    "string": "bindablepropertycolor"
+  },
+  "extends": "data_bind/bindable_property.json",
+  "properties": {
+    "propertyValue": {
+      "type": "Color",
+      "initialValue": "0xFF1D1D1D",
+      "key": {
+        "int": 638,
+        "string": "propertyvalue"
+      },
+      "description": "A property of type int that can be used for data binding."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/data_bind/bindable_property_enum.json b/dev/defs/data_bind/bindable_property_enum.json
new file mode 100644
index 0000000..b3faf03
--- /dev/null
+++ b/dev/defs/data_bind/bindable_property_enum.json
@@ -0,0 +1,21 @@
+{
+  "name": "BindablePropertyEnum",
+  "key": {
+    "int": 474,
+    "string": "bindablepropertyenum"
+  },
+  "extends": "data_bind/bindable_property.json",
+  "properties": {
+    "propertyValue": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 637,
+        "string": "propertyvalue"
+      },
+      "description": "The id of the enum that can be used for data binding."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/data_bind/bindable_property_number.json b/dev/defs/data_bind/bindable_property_number.json
new file mode 100644
index 0000000..a2ab667
--- /dev/null
+++ b/dev/defs/data_bind/bindable_property_number.json
@@ -0,0 +1,19 @@
+{
+  "name": "BindablePropertyNumber",
+  "key": {
+    "int": 473,
+    "string": "bindablepropertynumber"
+  },
+  "extends": "data_bind/bindable_property.json",
+  "properties": {
+    "propertyValue": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 636,
+        "string": "propertyvalue"
+      },
+      "description": "A property of type double that can be used for data binding."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/data_bind/bindable_property_string.json b/dev/defs/data_bind/bindable_property_string.json
new file mode 100644
index 0000000..aac63df
--- /dev/null
+++ b/dev/defs/data_bind/bindable_property_string.json
@@ -0,0 +1,19 @@
+{
+  "name": "BindablePropertyString",
+  "key": {
+    "int": 471,
+    "string": "bindablepropertystring"
+  },
+  "extends": "data_bind/bindable_property.json",
+  "properties": {
+    "propertyValue": {
+      "type": "String",
+      "initialValue": "''",
+      "key": {
+        "int": 635,
+        "string": "propertyvalue"
+      },
+      "description": "A property of type String that can be used for data binding."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/data_bind/converters/data_converter.json b/dev/defs/data_bind/converters/data_converter.json
new file mode 100644
index 0000000..b5a0c49
--- /dev/null
+++ b/dev/defs/data_bind/converters/data_converter.json
@@ -0,0 +1,30 @@
+{
+  "name": "DataConverter",
+  "key": {
+    "int": 488,
+    "string": "dataconverter"
+  },
+  "abstract": true,
+  "properties": {
+    "order": {
+      "type": "FractionalIndex",
+      "initialValue": "FractionalIndex.invalid",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 661,
+        "string": "order"
+      },
+      "description": "Order value for sorting data converters",
+      "runtime": false
+    },
+    "name": {
+      "type": "String",
+      "initialValue": "''",
+      "key": {
+        "int": 662,
+        "string": "name"
+      },
+      "description": "Non-unique identifier, used to give friendly names to data converters."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/data_bind/converters/data_converter_group.json b/dev/defs/data_bind/converters/data_converter_group.json
new file mode 100644
index 0000000..acc05f8
--- /dev/null
+++ b/dev/defs/data_bind/converters/data_converter_group.json
@@ -0,0 +1,8 @@
+{
+  "name": "DataConverterGroup",
+  "key": {
+    "int": 499,
+    "string": "dataconvertergroup"
+  },
+  "extends": "data_bind/converters/data_converter.json"
+}
\ No newline at end of file
diff --git a/dev/defs/data_bind/converters/data_converter_group_item.json b/dev/defs/data_bind/converters/data_converter_group_item.json
new file mode 100644
index 0000000..60e95ec
--- /dev/null
+++ b/dev/defs/data_bind/converters/data_converter_group_item.json
@@ -0,0 +1,39 @@
+{
+  "name": "DataConverterGroupItem",
+  "key": {
+    "int": 498,
+    "string": "dataconvertergroupitem"
+  },
+  "properties": {
+    "order": {
+      "type": "FractionalIndex",
+      "initialValue": "FractionalIndex.invalid",
+      "key": {
+        "int": 678,
+        "string": "order"
+      },
+      "runtime": false
+    },
+    "converterId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 679,
+        "string": "converterid"
+      },
+      "description": "Id of the converter"
+    },
+    "groupId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 680,
+        "string": "groupid"
+      },
+      "description": "Id of the group this item belongs to",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/data_bind/converters/data_converter_operation.json b/dev/defs/data_bind/converters/data_converter_operation.json
new file mode 100644
index 0000000..1a92179
--- /dev/null
+++ b/dev/defs/data_bind/converters/data_converter_operation.json
@@ -0,0 +1,28 @@
+{
+  "name": "DataConverterOperation",
+  "key": {
+    "int": 500,
+    "string": "dataconverteroperation"
+  },
+  "extends": "data_bind/converters/data_converter.json",
+  "properties": {
+    "value": {
+      "type": "double",
+      "initialValue": "1",
+      "key": {
+        "int": 681,
+        "string": "value"
+      },
+      "description": "Number to multiply the input to"
+    },
+    "operationType": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 682,
+        "string": "operationtype"
+      },
+      "description": "The operation tu use for the input and value"
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/data_bind/converters/data_converter_rounder.json b/dev/defs/data_bind/converters/data_converter_rounder.json
new file mode 100644
index 0000000..30dfbfd
--- /dev/null
+++ b/dev/defs/data_bind/converters/data_converter_rounder.json
@@ -0,0 +1,20 @@
+{
+  "name": "DataConverterRounder",
+  "key": {
+    "int": 489,
+    "string": "dataconverterrounder"
+  },
+  "extends": "data_bind/converters/data_converter.json",
+  "properties": {
+    "decimals": {
+      "type": "uint",
+      "typeRuntime": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 669,
+        "string": "decimals"
+      },
+      "description": "Number of decimals to round to"
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/data_bind/converters/data_converter_to_string.json b/dev/defs/data_bind/converters/data_converter_to_string.json
new file mode 100644
index 0000000..5584ce4
--- /dev/null
+++ b/dev/defs/data_bind/converters/data_converter_to_string.json
@@ -0,0 +1,8 @@
+{
+  "name": "DataConverterToString",
+  "key": {
+    "int": 490,
+    "string": "dataconvertertostring"
+  },
+  "extends": "data_bind/converters/data_converter.json"
+}
\ No newline at end of file
diff --git a/dev/defs/data_bind/data_bind.json b/dev/defs/data_bind/data_bind.json
new file mode 100644
index 0000000..3ec525b
--- /dev/null
+++ b/dev/defs/data_bind/data_bind.json
@@ -0,0 +1,47 @@
+{
+  "name": "DataBind",
+  "key": {
+    "int": 446,
+    "string": "databind"
+  },
+  "properties": {
+    "targetId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 585,
+        "string": "targetid"
+      },
+      "description": "Identifier used to track the object that is targetted.",
+      "runtime": false
+    },
+    "propertyKey": {
+      "type": "uint",
+      "initialValue": "CoreContext.invalidPropertyKey",
+      "key": {
+        "int": 586,
+        "string": "propertykey"
+      },
+      "description": "The property that is targeted."
+    },
+    "flags": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 587,
+        "string": "flags"
+      }
+    },
+    "converterId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 660,
+        "string": "converterid"
+      },
+      "description": "Identifier used to link to a data converter."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/data_bind/data_bind_context.json b/dev/defs/data_bind/data_bind_context.json
new file mode 100644
index 0000000..d9e04e6
--- /dev/null
+++ b/dev/defs/data_bind/data_bind_context.json
@@ -0,0 +1,21 @@
+{
+  "name": "DataBindContext",
+  "key": {
+    "int": 447,
+    "string": "databindcontext"
+  },
+  "extends": "data_bind/data_bind.json",
+  "properties": {
+    "sourcePathIds": {
+      "type": "List<Id>",
+      "typeRuntime": "Bytes",
+      "encoded": true,
+      "initialValue": "[]",
+      "key": {
+        "int": 588,
+        "string": "sourcepathids"
+      },
+      "description": "Path to the selected property."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/draw_rules.json b/dev/defs/draw_rules.json
new file mode 100644
index 0000000..a607dfc
--- /dev/null
+++ b/dev/defs/draw_rules.json
@@ -0,0 +1,22 @@
+{
+  "name": "DrawRules",
+  "key": {
+    "int": 49,
+    "string": "drawrules"
+  },
+  "extends": "container_component.json",
+  "properties": {
+    "drawTargetId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "animates": true,
+      "key": {
+        "int": 121,
+        "string": "drawtargetid"
+      },
+      "description": "Id of the DrawTarget that is currently active for this set of rules."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/draw_target.json b/dev/defs/draw_target.json
new file mode 100644
index 0000000..9cb5df1
--- /dev/null
+++ b/dev/defs/draw_target.json
@@ -0,0 +1,31 @@
+{
+  "name": "DrawTarget",
+  "key": {
+    "int": 48,
+    "string": "drawtarget"
+  },
+  "extends": "component.json",
+  "properties": {
+    "drawableId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 119,
+        "string": "drawableid"
+      },
+      "description": "Id of the drawable this target references."
+    },
+    "placementValue": {
+      "type": "uint",
+      "typeRuntime": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 120,
+        "string": "placementvalue"
+      },
+      "description": "Backing enum value for the Placement."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/drawable.json b/dev/defs/drawable.json
new file mode 100644
index 0000000..6ee1d46
--- /dev/null
+++ b/dev/defs/drawable.json
@@ -0,0 +1,28 @@
+{
+  "name": "Drawable",
+  "key": {
+    "int": 13,
+    "string": "drawable"
+  },
+  "abstract": true,
+  "extends": "node.json",
+  "properties": {
+    "blendModeValue": {
+      "type": "uint",
+      "initialValue": "3",
+      "key": {
+        "int": 23,
+        "string": "blendModeValue"
+      }
+    },
+    "drawableFlags": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 129,
+        "string": "drawableflags"
+      },
+      "coop": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/event.json b/dev/defs/event.json
new file mode 100644
index 0000000..99db764
--- /dev/null
+++ b/dev/defs/event.json
@@ -0,0 +1,36 @@
+{
+  "name": "Event",
+  "key": {
+    "int": 128,
+    "string": "event"
+  },
+  "extends": "container_component.json",
+  "properties": {
+    "x": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 396,
+        "string": "x"
+      },
+      "runtime": false
+    },
+    "y": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 397,
+        "string": "y"
+      },
+      "runtime": false
+    },
+    "trigger": {
+      "type": "callback",
+      "animates": true,
+      "key": {
+        "int": 395,
+        "string": "trigger"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/guide.json b/dev/defs/guide.json
new file mode 100644
index 0000000..87f6d18
--- /dev/null
+++ b/dev/defs/guide.json
@@ -0,0 +1,32 @@
+{
+  "name": "Guide",
+  "key": {
+    "int": 140,
+    "string": "guide"
+  },
+  "extends": "component.json",
+  "runtime": false,
+  "properties": {
+    "axisPosition": {
+      "type": "double",
+      "initialValue": "0",
+      "overrideGet": true,
+      "group": "axisposition",
+      "key": {
+        "int": 276,
+        "string": "axisposition"
+      },
+      "runtime": false
+    },
+    "axisValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 277,
+        "string": "axisvalue"
+      },
+      "description": "Axis the guide position relates to. 0 for x, 1 for y.",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/joystick.json b/dev/defs/joystick.json
new file mode 100644
index 0000000..f356b79
--- /dev/null
+++ b/dev/defs/joystick.json
@@ -0,0 +1,132 @@
+{
+  "name": "Joystick",
+  "key": {
+    "int": 148,
+    "string": "joystick"
+  },
+  "extends": "component.json",
+  "properties": {
+    "x": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "position",
+      "key": {
+        "int": 299,
+        "string": "x"
+      }
+    },
+    "y": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "position",
+      "key": {
+        "int": 300,
+        "string": "y"
+      }
+    },
+    "posX": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 303,
+        "string": "posx"
+      }
+    },
+    "posY": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 304,
+        "string": "posy"
+      }
+    },
+    "originX": {
+      "type": "double",
+      "initialValue": "0.5",
+      "key": {
+        "int": 307,
+        "string": "originx"
+      },
+      "description": "Origin x in normalized coordinates (0.5 = center, 0 = left, 1 = right)."
+    },
+    "originY": {
+      "type": "double",
+      "initialValue": "0.5",
+      "key": {
+        "int": 308,
+        "string": "originy"
+      },
+      "description": "Origin y in normalized coordinates (0.5 = center, 0 = top, 1 = bottom)."
+    },
+    "width": {
+      "type": "double",
+      "initialValue": "100",
+      "key": {
+        "int": 305,
+        "string": "width"
+      }
+    },
+    "height": {
+      "type": "double",
+      "initialValue": "100",
+      "key": {
+        "int": 306,
+        "string": "height"
+      }
+    },
+    "xId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 301,
+        "string": "x_id"
+      },
+      "description": "Identifier used to track the animation used for the x axis of the joystick."
+    },
+    "yId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 302,
+        "string": "y_id"
+      },
+      "description": "Identifier used to track the animation used for the y axis of the joystick."
+    },
+    "joystickFlags": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 312,
+        "string": "joystickflags"
+      }
+    },
+    "joystickOrder": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 314,
+        "string": "joystickorder"
+      },
+      "runtime": false,
+      "coop": false,
+      "journal": false
+    },
+    "handleSourceId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 313,
+        "string": "handlesourceid"
+      },
+      "description": "Identifier used to track the custom handle source of the joystick."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/layout/axis.json b/dev/defs/layout/axis.json
new file mode 100644
index 0000000..0703844
--- /dev/null
+++ b/dev/defs/layout/axis.json
@@ -0,0 +1,29 @@
+{
+  "name": "Axis",
+  "key": {
+    "int": 492,
+    "string": "axis"
+  },
+  "abstract": true,
+  "extends": "component.json",
+  "properties": {
+    "offset": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 675,
+        "string": "offset"
+      }
+    },
+    "normalized": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 676,
+        "string": "normalized"
+      },
+      "description": "true means offset indicates a percentage"
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/layout/axis_x.json b/dev/defs/layout/axis_x.json
new file mode 100644
index 0000000..47774c7
--- /dev/null
+++ b/dev/defs/layout/axis_x.json
@@ -0,0 +1,8 @@
+{
+  "name": "AxisX",
+  "key": {
+    "int": 495,
+    "string": "axisx"
+  },
+  "extends": "layout/axis.json"
+}
\ No newline at end of file
diff --git a/dev/defs/layout/axis_y.json b/dev/defs/layout/axis_y.json
new file mode 100644
index 0000000..b3bd16e
--- /dev/null
+++ b/dev/defs/layout/axis_y.json
@@ -0,0 +1,8 @@
+{
+  "name": "AxisY",
+  "key": {
+    "int": 494,
+    "string": "axisy"
+  },
+  "extends": "layout/axis.json"
+}
\ No newline at end of file
diff --git a/dev/defs/layout/layout_component_style.json b/dev/defs/layout/layout_component_style.json
new file mode 100644
index 0000000..d96f7af
--- /dev/null
+++ b/dev/defs/layout/layout_component_style.json
@@ -0,0 +1,779 @@
+{
+  "name": "LayoutComponentStyle",
+  "key": {
+    "int": 420,
+    "string": "layoutcomponentstyle"
+  },
+  "extends": "component.json",
+  "properties": {
+    "gapHorizontal": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutgap",
+      "key": {
+        "int": 498,
+        "string": "gaphorizontal"
+      },
+      "description": "Horizontal gap between children in layout component"
+    },
+    "gapVertical": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutgap",
+      "key": {
+        "int": 499,
+        "string": "gapvertical"
+      },
+      "description": "Vertical gap between children in layout component"
+    },
+    "maxWidth": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutmax",
+      "key": {
+        "int": 500,
+        "string": "maxwidth"
+      },
+      "description": "Max width of the item."
+    },
+    "maxHeight": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutmax",
+      "key": {
+        "int": 501,
+        "string": "maxheight"
+      },
+      "description": "Max height of the item."
+    },
+    "minWidth": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutmin",
+      "key": {
+        "int": 502,
+        "string": "minwidth"
+      },
+      "description": "Min width of the item."
+    },
+    "minHeight": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutmin",
+      "key": {
+        "int": 503,
+        "string": "minheight"
+      },
+      "description": "Min height of the item."
+    },
+    "borderLeft": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutborder",
+      "key": {
+        "int": 504,
+        "string": "borderleft"
+      },
+      "description": "Left border value."
+    },
+    "borderRight": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutborder",
+      "key": {
+        "int": 505,
+        "string": "borderright"
+      },
+      "description": "Right border value."
+    },
+    "borderTop": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutborder",
+      "key": {
+        "int": 506,
+        "string": "bordertop"
+      },
+      "description": "Top border value."
+    },
+    "borderBottom": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutborder",
+      "key": {
+        "int": 507,
+        "string": "borderbottom"
+      },
+      "description": "Bottom border value."
+    },
+    "marginLeft": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutmargin",
+      "key": {
+        "int": 508,
+        "string": "marginleft"
+      },
+      "description": "Left margin value."
+    },
+    "marginRight": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutmargin",
+      "key": {
+        "int": 509,
+        "string": "marginright"
+      },
+      "description": "Right margin value."
+    },
+    "marginTop": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutmargin",
+      "key": {
+        "int": 510,
+        "string": "margintop"
+      },
+      "description": "Top margin value."
+    },
+    "marginBottom": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutmargin",
+      "key": {
+        "int": 511,
+        "string": "marginbottom"
+      },
+      "description": "Bottom margin value."
+    },
+    "paddingLeft": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutpadding",
+      "key": {
+        "int": 512,
+        "string": "paddingleft"
+      },
+      "description": "Left padding value."
+    },
+    "paddingRight": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutpadding",
+      "key": {
+        "int": 513,
+        "string": "paddingright"
+      },
+      "description": "Right padding value."
+    },
+    "paddingTop": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutpadding",
+      "key": {
+        "int": 514,
+        "string": "paddingtop"
+      },
+      "description": "Top padding value."
+    },
+    "paddingBottom": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutpadding",
+      "key": {
+        "int": 515,
+        "string": "paddingbottom"
+      },
+      "description": "Bottom padding value."
+    },
+    "positionLeft": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutposition",
+      "key": {
+        "int": 516,
+        "string": "positionleft"
+      },
+      "description": "Left position value."
+    },
+    "positionRight": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutposition",
+      "key": {
+        "int": 517,
+        "string": "positionright"
+      },
+      "description": "Right position value."
+    },
+    "positionTop": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutposition",
+      "key": {
+        "int": 518,
+        "string": "positiontop"
+      },
+      "description": "Top position value."
+    },
+    "positionBottom": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "layoutposition",
+      "key": {
+        "int": 519,
+        "string": "positionbottom"
+      },
+      "description": "Bottom position value."
+    },
+    "flex": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 520,
+        "string": "flex"
+      },
+      "description": "Flex value."
+    },
+    "flexGrow": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 521,
+        "string": "flexgrow"
+      },
+      "description": "Flex grow value."
+    },
+    "flexShrink": {
+      "type": "double",
+      "initialValue": "1",
+      "animates": true,
+      "key": {
+        "int": 522,
+        "string": "flexshrink"
+      },
+      "description": "Flex shrink value."
+    },
+    "flexBasis": {
+      "type": "double",
+      "initialValue": "1",
+      "animates": true,
+      "key": {
+        "int": 523,
+        "string": "flexbasis"
+      },
+      "description": "Flex basis value."
+    },
+    "aspectRatio": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 524,
+        "string": "aspectratio"
+      },
+      "description": "Aspect ratio value."
+    },
+    "showUnitToggle": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 542,
+        "string": "showunittoggle"
+      },
+      "description": "Show layout unit toggle in inspector position widget. Editor only.",
+      "runtime": false
+    },
+    "edgeConstraints": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 545,
+        "string": "edgeconstraints"
+      },
+      "runtime": false
+    },
+    "layoutWidthScaleType": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 655,
+        "string": "layoutwidthscaletype"
+      }
+    },
+    "layoutHeightScaleType": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 656,
+        "string": "layoutheightscaletype"
+      }
+    },
+    "layoutAlignmentType": {
+      "type": "uint",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 632,
+        "string": "layoutalignmenttype"
+      }
+    },
+    "animationStyleType": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 589,
+        "string": "animationstyletype"
+      },
+      "description": "The type of animation none|custom|inherit applied to this layout."
+    },
+    "interpolationType": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 590,
+        "string": "interpolationtype"
+      },
+      "description": "The type of interpolation index in KeyframeInterpolation applied to this layout."
+    },
+    "interpolatorId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 591,
+        "string": "interpolatorid"
+      },
+      "description": "The id of the custom interpolator used when interpolation is Cubic."
+    },
+    "interpolationTime": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 592,
+        "string": "interpolationtime"
+      },
+      "description": "The time over which the interpolator applies."
+    },
+    "displayValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 596,
+        "string": "displayvalue"
+      },
+      "description": ""
+    },
+    "positionTypeValue": {
+      "type": "uint",
+      "initialValue": "1",
+      "key": {
+        "int": 597,
+        "string": "positiontypevalue"
+      },
+      "description": ""
+    },
+    "flexDirectionValue": {
+      "type": "uint",
+      "initialValue": "2",
+      "animates": true,
+      "key": {
+        "int": 598,
+        "string": "flexdirectionvalue"
+      },
+      "description": "Flex dir"
+    },
+    "directionValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 599,
+        "string": "directionvalue"
+      },
+      "description": ""
+    },
+    "alignContentValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 600,
+        "string": "aligncontentvalue"
+      },
+      "description": ""
+    },
+    "alignItemsValue": {
+      "type": "uint",
+      "initialValue": "1",
+      "animates": true,
+      "key": {
+        "int": 601,
+        "string": "alignitemsvalue"
+      },
+      "description": ""
+    },
+    "alignSelfValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 602,
+        "string": "alignselfvalue"
+      },
+      "description": ""
+    },
+    "justifyContentValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 603,
+        "string": "justifycontentvalue"
+      },
+      "description": ""
+    },
+    "flexWrapValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 604,
+        "string": "flexwrapvalue"
+      },
+      "description": ""
+    },
+    "overflowValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 605,
+        "string": "overflowvalue"
+      },
+      "description": ""
+    },
+    "intrinsicallySizedValue": {
+      "type": "bool",
+      "initialValue": "false",
+      "animates": true,
+      "key": {
+        "int": 606,
+        "string": "intrinsicallysizedvalue"
+      },
+      "description": ""
+    },
+    "widthUnitsValue": {
+      "type": "uint",
+      "initialValue": "1",
+      "key": {
+        "int": 607,
+        "string": "widthunitsvalue"
+      },
+      "description": ""
+    },
+    "heightUnitsValue": {
+      "type": "uint",
+      "initialValue": "1",
+      "key": {
+        "int": 608,
+        "string": "heightunitsvalue"
+      },
+      "description": ""
+    },
+    "borderLeftUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutborder",
+      "key": {
+        "int": 609,
+        "string": "borderleftunitsvalue"
+      },
+      "description": ""
+    },
+    "borderRightUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutborder",
+      "key": {
+        "int": 610,
+        "string": "borderrightunitsvalue"
+      },
+      "description": ""
+    },
+    "borderTopUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutborder",
+      "key": {
+        "int": 611,
+        "string": "bordertopunitsvalue"
+      },
+      "description": ""
+    },
+    "borderBottomUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutborder",
+      "key": {
+        "int": 612,
+        "string": "borderbottomunitsvalue"
+      },
+      "description": ""
+    },
+    "marginLeftUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutmargin",
+      "key": {
+        "int": 613,
+        "string": "marginleftunitsvalue"
+      },
+      "description": ""
+    },
+    "marginRightUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutmargin",
+      "key": {
+        "int": 614,
+        "string": "marginrightunitsvalue"
+      },
+      "description": ""
+    },
+    "marginTopUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutmargin",
+      "key": {
+        "int": 615,
+        "string": "margintopunitsvalue"
+      },
+      "description": ""
+    },
+    "marginBottomUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutmargin",
+      "key": {
+        "int": 616,
+        "string": "marginbottomunitsvalue"
+      },
+      "description": ""
+    },
+    "paddingLeftUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutpadding",
+      "key": {
+        "int": 617,
+        "string": "paddingleftunitsvalue"
+      },
+      "description": ""
+    },
+    "paddingRightUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutpadding",
+      "key": {
+        "int": 618,
+        "string": "paddingrightunitsvalue"
+      },
+      "description": ""
+    },
+    "paddingTopUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutpadding",
+      "key": {
+        "int": 619,
+        "string": "paddingtopunitsvalue"
+      },
+      "description": ""
+    },
+    "paddingBottomUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutpadding",
+      "key": {
+        "int": 620,
+        "string": "paddingbottomunitsvalue"
+      },
+      "description": ""
+    },
+    "positionLeftUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutposition",
+      "key": {
+        "int": 621,
+        "string": "positionleftunitsvalue"
+      },
+      "description": ""
+    },
+    "positionRightUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutposition",
+      "key": {
+        "int": 622,
+        "string": "positionrightunitsvalue"
+      },
+      "description": ""
+    },
+    "positionTopUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutposition",
+      "key": {
+        "int": 623,
+        "string": "positiontopunitsvalue"
+      },
+      "description": ""
+    },
+    "positionBottomUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutposition",
+      "key": {
+        "int": 624,
+        "string": "positionbottomunitsvalue"
+      },
+      "description": ""
+    },
+    "gapHorizontalUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutgap",
+      "key": {
+        "int": 625,
+        "string": "gaphorizontalunitsvalue"
+      },
+      "description": ""
+    },
+    "gapVerticalUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutgap",
+      "key": {
+        "int": 626,
+        "string": "gapverticalunitsvalue"
+      },
+      "description": ""
+    },
+    "minWidthUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutmin",
+      "key": {
+        "int": 627,
+        "string": "minwidthunitsvalue"
+      },
+      "description": ""
+    },
+    "minHeightUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutmin",
+      "key": {
+        "int": 628,
+        "string": "minheightunitsvalue"
+      },
+      "description": ""
+    },
+    "maxWidthUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutmax",
+      "key": {
+        "int": 629,
+        "string": "maxwidthunitsvalue"
+      },
+      "description": ""
+    },
+    "maxHeightUnitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "group": "layoutmax",
+      "key": {
+        "int": 630,
+        "string": "maxheightunitsvalue"
+      },
+      "description": ""
+    },
+    "linkCornerRadius": {
+      "type": "bool",
+      "initialValue": "true",
+      "key": {
+        "int": 639,
+        "string": "linkcornerradius"
+      },
+      "description": "Whether the TL corner radius defines all the radiuses"
+    },
+    "cornerRadiusTL": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 640,
+        "string": "cornerradiustl"
+      },
+      "description": "Top left radius of the corners of this layout"
+    },
+    "cornerRadiusTR": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 641,
+        "string": "cornerradiustr"
+      },
+      "description": "Top right radius of the corners of this layout"
+    },
+    "cornerRadiusBL": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 642,
+        "string": "cornerradiusbl"
+      },
+      "description": "Bottom left radius of the corners of this layout"
+    },
+    "cornerRadiusBR": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 643,
+        "string": "cornerradiusbr"
+      },
+      "description": "Bottom right radius of the corners of this layout"
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/layout/n_slicer.json b/dev/defs/layout/n_slicer.json
new file mode 100644
index 0000000..c244b37
--- /dev/null
+++ b/dev/defs/layout/n_slicer.json
@@ -0,0 +1,8 @@
+{
+  "name": "NSlicer",
+  "key": {
+    "int": 493,
+    "string": "nslicer"
+  },
+  "extends": "container_component.json"
+}
\ No newline at end of file
diff --git a/dev/defs/layout/n_slicer_tile_mode.json b/dev/defs/layout/n_slicer_tile_mode.json
new file mode 100644
index 0000000..fae9388
--- /dev/null
+++ b/dev/defs/layout/n_slicer_tile_mode.json
@@ -0,0 +1,48 @@
+{
+  "name": "NSlicerTileMode",
+  "key": {
+    "int": 491,
+    "string": "nslicertilemode"
+  },
+  "extends": "component.json",
+  "properties": {
+    "patchX": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 670,
+        "string": "patchx"
+      },
+      "description": "the x index of the patch to style",
+      "runtime": false
+    },
+    "patchY": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 671,
+        "string": "patchy"
+      },
+      "description": "the y index of the patch to style",
+      "runtime": false
+    },
+    "patchIndex": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 672,
+        "string": "patchindex"
+      },
+      "description": "the index of the patch to style, non-negative"
+    },
+    "style": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 673,
+        "string": "style"
+      },
+      "description": "represents stretch, repeat, hidden, etc."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/layout_component.json b/dev/defs/layout_component.json
new file mode 100644
index 0000000..8aca893
--- /dev/null
+++ b/dev/defs/layout_component.json
@@ -0,0 +1,50 @@
+{
+  "name": "LayoutComponent",
+  "key": {
+    "int": 409,
+    "string": "layoutcomponent"
+  },
+  "extends": "drawable.json",
+  "properties": {
+    "clip": {
+      "type": "bool",
+      "initialValue": "true",
+      "key": {
+        "int": 196,
+        "string": "clip"
+      },
+      "description": "True when the layout component bounds clip its contents."
+    },
+    "width": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 7,
+        "string": "w"
+      },
+      "description": "Initial width of the item."
+    },
+    "height": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 8,
+        "string": "h"
+      },
+      "description": "Initial height of the item."
+    },
+    "styleId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 494,
+        "string": "styleid"
+      },
+      "description": "LayoutStyle that defines the styling for this LayoutComponent"
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/nested_animation.json b/dev/defs/nested_animation.json
new file mode 100644
index 0000000..db6cbfb
--- /dev/null
+++ b/dev/defs/nested_animation.json
@@ -0,0 +1,22 @@
+{
+  "name": "NestedAnimation",
+  "key": {
+    "int": 93,
+    "string": "nestedanimation"
+  },
+  "abstract": true,
+  "extends": "container_component.json",
+  "properties": {
+    "animationId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 198,
+        "string": "animationid"
+      },
+      "description": "Identifier used to track the animation in the nested artboard."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/nested_artboard.json b/dev/defs/nested_artboard.json
new file mode 100644
index 0000000..8a1ea48
--- /dev/null
+++ b/dev/defs/nested_artboard.json
@@ -0,0 +1,32 @@
+{
+  "name": "NestedArtboard",
+  "key": {
+    "int": 92,
+    "string": "nestedartboard"
+  },
+  "extends": "drawable.json",
+  "properties": {
+    "artboardId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 197,
+        "string": "artboardid"
+      },
+      "description": "Identifier used to track the Artboard nested."
+    },
+    "dataBindPathIds": {
+      "type": "List<Id>",
+      "typeRuntime": "Bytes",
+      "encoded": true,
+      "initialValue": "[]",
+      "key": {
+        "int": 582,
+        "string": "databindpathids"
+      },
+      "description": "Path to the selected property."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/nested_artboard_layout.json b/dev/defs/nested_artboard_layout.json
new file mode 100644
index 0000000..4730f94
--- /dev/null
+++ b/dev/defs/nested_artboard_layout.json
@@ -0,0 +1,66 @@
+{
+  "name": "NestedArtboardLayout",
+  "key": {
+    "int": 452,
+    "string": "nestedartboardlayout"
+  },
+  "extends": "nested_artboard.json",
+  "properties": {
+    "instanceWidth": {
+      "type": "double",
+      "initialValue": "-1",
+      "animates": true,
+      "key": {
+        "int": 663,
+        "string": "instancewidth"
+      },
+      "description": "Width value in points or percent of this nested artboard instance."
+    },
+    "instanceHeight": {
+      "type": "double",
+      "initialValue": "-1",
+      "animates": true,
+      "key": {
+        "int": 664,
+        "string": "instanceheight"
+      },
+      "description": "Height value in points or percent of this nested artboard instance"
+    },
+    "instanceWidthUnitsValue": {
+      "type": "uint",
+      "initialValue": "1",
+      "key": {
+        "int": 665,
+        "string": "instancewidthunitsvalue"
+      },
+      "description": "Whether to display width in points or percent"
+    },
+    "instanceHeightUnitsValue": {
+      "type": "uint",
+      "initialValue": "1",
+      "key": {
+        "int": 666,
+        "string": "instanceheightunitsvalue"
+      },
+      "description": "Whether to display height in points or percent"
+    },
+    "instanceWidthScaleType": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 667,
+        "string": "instancewidthscaletype"
+      },
+      "description": "Width scale type fixed | fill"
+    },
+    "instanceHeightScaleType": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 668,
+        "string": "instanceheightscaletype"
+      },
+      "description": "Height scale type fixed | fill"
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/nested_artboard_leaf.json b/dev/defs/nested_artboard_leaf.json
new file mode 100644
index 0000000..cddf158
--- /dev/null
+++ b/dev/defs/nested_artboard_leaf.json
@@ -0,0 +1,34 @@
+{
+    "name": "NestedArtboardLeaf",
+    "key": {
+        "int": 451,
+        "string": "nested_artboard_leaf"
+    },
+    "extends": "nested_artboard.json",
+    "properties": {
+        "fit": {
+            "type": "uint",
+            "key": {
+                "int": 538,
+                "string": "fit"
+            },
+            "description": "Fit type for the nested artboard's runtime artboard."
+        },
+        "alignmentX": {
+            "type": "double",
+            "key": {
+                "int": 644,
+                "string": "alignmentx"
+            },
+            "description": "Alignment value on X."
+        },
+        "alignmentY": {
+            "type": "double",
+            "key": {
+                "int": 645,
+                "string": "alignmenty"
+            },
+            "description": "Alignment value on Y."
+        }
+    }
+}
\ No newline at end of file
diff --git a/dev/defs/node.json b/dev/defs/node.json
new file mode 100644
index 0000000..36a1ea3
--- /dev/null
+++ b/dev/defs/node.json
@@ -0,0 +1,56 @@
+{
+  "name": "Node",
+  "key": {
+    "int": 2,
+    "string": "node"
+  },
+  "extends": "transform_component.json",
+  "properties": {
+    "x": {
+      "type": "double",
+      "initialValue": "0",
+      "overrideGet": true,
+      "animates": true,
+      "group": "position",
+      "key": {
+        "int": 13,
+        "string": "x",
+        "alternates": [
+          {
+            "int": 9,
+            "string": "xArtboard"
+          }
+        ]
+      },
+      "bindable": true
+    },
+    "y": {
+      "type": "double",
+      "initialValue": "0",
+      "overrideGet": true,
+      "animates": true,
+      "group": "position",
+      "key": {
+        "int": 14,
+        "string": "y",
+        "alternates": [
+          {
+            "int": 10,
+            "string": "yArtboard"
+          }
+        ]
+      },
+      "bindable": true
+    },
+    "styleValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 176,
+        "string": "style"
+      },
+      "description": "Display style of the node in the editor. 0 for group and 1 for target.",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/open_url_event.json b/dev/defs/open_url_event.json
new file mode 100644
index 0000000..77b57f9
--- /dev/null
+++ b/dev/defs/open_url_event.json
@@ -0,0 +1,28 @@
+{
+  "name": "OpenUrlEvent",
+  "key": {
+    "int": 131,
+    "string": "openurlevent"
+  },
+  "extends": "event.json",
+  "properties": {
+    "url": {
+      "type": "String",
+      "initialValue": "''",
+      "key": {
+        "int": 248,
+        "string": "url"
+      },
+      "description": "URL to open."
+    },
+    "targetValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 249,
+        "string": "targetvalue"
+      },
+      "description": "Backing value for the target enum."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/clipping_shape.json b/dev/defs/shapes/clipping_shape.json
new file mode 100644
index 0000000..5f95370
--- /dev/null
+++ b/dev/defs/shapes/clipping_shape.json
@@ -0,0 +1,38 @@
+{
+  "name": "ClippingShape",
+  "key": {
+    "int": 42,
+    "string": "clippingshape"
+  },
+  "extends": "component.json",
+  "properties": {
+    "sourceId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 92,
+        "string": "sourceid"
+      },
+      "description": "Identifier used to track the node to use as a clipping source."
+    },
+    "fillRule": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 93,
+        "string": "fillrule"
+      },
+      "description": "Backing enum value for the clipping fill rule (nonZero or evenOdd)."
+    },
+    "isVisible": {
+      "type": "bool",
+      "initialValue": "true",
+      "key": {
+        "int": 94,
+        "string": "isvisible"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/contour_mesh_vertex.json b/dev/defs/shapes/contour_mesh_vertex.json
new file mode 100644
index 0000000..089b2ac
--- /dev/null
+++ b/dev/defs/shapes/contour_mesh_vertex.json
@@ -0,0 +1,8 @@
+{
+  "name": "ContourMeshVertex",
+  "key": {
+    "int": 111,
+    "string": "contourmeshvertex"
+  },
+  "extends": "shapes/mesh_vertex.json"
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/cubic_asymmetric_vertex.json b/dev/defs/shapes/cubic_asymmetric_vertex.json
new file mode 100644
index 0000000..d902961
--- /dev/null
+++ b/dev/defs/shapes/cubic_asymmetric_vertex.json
@@ -0,0 +1,40 @@
+{
+  "name": "CubicAsymmetricVertex",
+  "key": {
+    "int": 34,
+    "string": "cubicasymmetricvertex"
+  },
+  "extends": "shapes/cubic_vertex.json",
+  "properties": {
+    "rotation": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 79,
+        "string": "rotation"
+      },
+      "description": "The control points' angle."
+    },
+    "inDistance": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 80,
+        "string": "indistance"
+      },
+      "description": "The in point's distance from the translation of the point."
+    },
+    "outDistance": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 81,
+        "string": "outdistance"
+      },
+      "description": "The out point's distance from the translation of the point."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/cubic_detached_vertex.json b/dev/defs/shapes/cubic_detached_vertex.json
new file mode 100644
index 0000000..d6ffd9c
--- /dev/null
+++ b/dev/defs/shapes/cubic_detached_vertex.json
@@ -0,0 +1,54 @@
+{
+  "name": "CubicDetachedVertex",
+  "key": {
+    "int": 6,
+    "string": "cubicvertex"
+  },
+  "extends": "shapes/cubic_vertex.json",
+  "properties": {
+    "inRotation": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "bezier_in",
+      "key": {
+        "int": 84,
+        "string": "inrotation"
+      },
+      "description": "The in point's angle."
+    },
+    "inDistance": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "bezier_in",
+      "key": {
+        "int": 85,
+        "string": "indistance"
+      },
+      "description": "The in point's distance from the translation of the point."
+    },
+    "outRotation": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "bezier_out",
+      "key": {
+        "int": 86,
+        "string": "outrotation"
+      },
+      "description": "The out point's angle."
+    },
+    "outDistance": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "bezier_out",
+      "key": {
+        "int": 87,
+        "string": "outdistance"
+      },
+      "description": "The out point's distance from the translation of the point."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/cubic_mirrored_vertex.json b/dev/defs/shapes/cubic_mirrored_vertex.json
new file mode 100644
index 0000000..67dc6f3
--- /dev/null
+++ b/dev/defs/shapes/cubic_mirrored_vertex.json
@@ -0,0 +1,30 @@
+{
+  "name": "CubicMirroredVertex",
+  "key": {
+    "int": 35,
+    "string": "cubicmirroredvertex"
+  },
+  "extends": "shapes/cubic_vertex.json",
+  "properties": {
+    "rotation": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 82,
+        "string": "rotation"
+      },
+      "description": "The control points' angle."
+    },
+    "distance": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 83,
+        "string": "distance"
+      },
+      "description": "The control points' distance from the translation of the point."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/cubic_vertex.json b/dev/defs/shapes/cubic_vertex.json
new file mode 100644
index 0000000..170db2d
--- /dev/null
+++ b/dev/defs/shapes/cubic_vertex.json
@@ -0,0 +1,10 @@
+{
+  "name": "CubicVertex",
+  "key": {
+    "int": 36,
+    "string": "cubicvertex"
+  },
+  "abstract": true,
+  "extends": "shapes/path_vertex.json",
+  "generic": "bones/cubic_weight.json"
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/ellipse.json b/dev/defs/shapes/ellipse.json
new file mode 100644
index 0000000..cee0990
--- /dev/null
+++ b/dev/defs/shapes/ellipse.json
@@ -0,0 +1,8 @@
+{
+  "name": "Ellipse",
+  "key": {
+    "int": 4,
+    "string": "ellipse"
+  },
+  "extends": "shapes/parametric_path.json"
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/forced_edge.json b/dev/defs/shapes/forced_edge.json
new file mode 100644
index 0000000..d4162ac
--- /dev/null
+++ b/dev/defs/shapes/forced_edge.json
@@ -0,0 +1,33 @@
+{
+  "name": "ForcedEdge",
+  "key": {
+    "int": 112,
+    "string": "forcededge"
+  },
+  "extends": "component.json",
+  "runtime": false,
+  "properties": {
+    "fromId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 219,
+        "string": "fromid"
+      },
+      "description": "Identifier used to track MeshVertex the force edge extends from."
+    },
+    "toId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 220,
+        "string": "toid"
+      },
+      "description": "Identifier used to track MeshVertex the force edge extends to."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/image.json b/dev/defs/shapes/image.json
new file mode 100644
index 0000000..fb45809
--- /dev/null
+++ b/dev/defs/shapes/image.json
@@ -0,0 +1,41 @@
+{
+  "name": "Image",
+  "key": {
+    "int": 100,
+    "string": "image"
+  },
+  "extends": "drawable.json",
+  "properties": {
+    "assetId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 206,
+        "string": "assetid"
+      },
+      "description": "Image drawable for an image asset"
+    },
+    "originX": {
+      "type": "double",
+      "initialValue": "0.5",
+      "animates": true,
+      "key": {
+        "int": 380,
+        "string": "originx"
+      },
+      "description": "Origin x in normalized coordinates (0.5 = center, 0 = left, 1 = right)."
+    },
+    "originY": {
+      "type": "double",
+      "initialValue": "0.5",
+      "animates": true,
+      "key": {
+        "int": 381,
+        "string": "originy"
+      },
+      "description": "Origin y in normalized coordinates (0.5 = center, 0 = top, 1 = bottom)."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/mesh.json b/dev/defs/shapes/mesh.json
new file mode 100644
index 0000000..857676b
--- /dev/null
+++ b/dev/defs/shapes/mesh.json
@@ -0,0 +1,40 @@
+{
+  "name": "Mesh",
+  "key": {
+    "int": 109,
+    "string": "mesh"
+  },
+  "extends": "container_component.json",
+  "properties": {
+    "isClosed": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 217,
+        "string": "isclosed"
+      },
+      "description": "Whether the contour is closed.",
+      "runtime": false
+    },
+    "editingModeValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 214,
+        "string": "editing_mode_value"
+      },
+      "description": "We use this to track the edit-time state of the Mesh for the VertexEditor.",
+      "runtime": false,
+      "coop": false
+    },
+    "triangleIndexBytes": {
+      "type": "Bytes",
+      "encoded": true,
+      "key": {
+        "int": 223,
+        "string": "triangleindexbytes"
+      },
+      "description": "Byte data for the triangle indices."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/mesh_vertex.json b/dev/defs/shapes/mesh_vertex.json
new file mode 100644
index 0000000..06a7c0f
--- /dev/null
+++ b/dev/defs/shapes/mesh_vertex.json
@@ -0,0 +1,30 @@
+{
+  "name": "MeshVertex",
+  "key": {
+    "int": 108,
+    "string": "meshvertex"
+  },
+  "extends": "shapes/vertex.json",
+  "properties": {
+    "u": {
+      "type": "double",
+      "initialValue": "0",
+      "group": "texture",
+      "key": {
+        "int": 215,
+        "string": "u"
+      },
+      "description": "U value for the texture coordinate of the vertex."
+    },
+    "v": {
+      "type": "double",
+      "initialValue": "0",
+      "group": "texture",
+      "key": {
+        "int": 216,
+        "string": "v"
+      },
+      "description": "V value for the texture coordinate of the vertex."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/paint/fill.json b/dev/defs/shapes/paint/fill.json
new file mode 100644
index 0000000..eabf085
--- /dev/null
+++ b/dev/defs/shapes/paint/fill.json
@@ -0,0 +1,18 @@
+{
+  "name": "Fill",
+  "key": {
+    "int": 20,
+    "string": "fill"
+  },
+  "extends": "shapes/paint/shape_paint.json",
+  "properties": {
+    "fillRule": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 40,
+        "string": "fillrule"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/paint/gradient_stop.json b/dev/defs/shapes/paint/gradient_stop.json
new file mode 100644
index 0000000..4555199
--- /dev/null
+++ b/dev/defs/shapes/paint/gradient_stop.json
@@ -0,0 +1,28 @@
+{
+  "name": "GradientStop",
+  "key": {
+    "int": 19,
+    "string": "gradientstop"
+  },
+  "extends": "component.json",
+  "properties": {
+    "colorValue": {
+      "type": "Color",
+      "initialValue": "0xFFFFFFFF",
+      "animates": true,
+      "key": {
+        "int": 38,
+        "string": "color"
+      }
+    },
+    "position": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 39,
+        "string": "position"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/paint/linear_gradient.json b/dev/defs/shapes/paint/linear_gradient.json
new file mode 100644
index 0000000..5d7e249
--- /dev/null
+++ b/dev/defs/shapes/paint/linear_gradient.json
@@ -0,0 +1,59 @@
+{
+  "name": "LinearGradient",
+  "key": {
+    "int": 22,
+    "string": "lineargradient"
+  },
+  "extends": "container_component.json",
+  "properties": {
+    "startX": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "start",
+      "key": {
+        "int": 42,
+        "string": "startX"
+      }
+    },
+    "startY": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "start",
+      "key": {
+        "int": 33,
+        "string": "startY"
+      }
+    },
+    "endX": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "end",
+      "key": {
+        "int": 34,
+        "string": "endX"
+      }
+    },
+    "endY": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "end",
+      "key": {
+        "int": 35,
+        "string": "endY"
+      }
+    },
+    "opacity": {
+      "type": "double",
+      "initialValue": "1",
+      "animates": true,
+      "key": {
+        "int": 46,
+        "string": "opacity"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/paint/radial_gradient.json b/dev/defs/shapes/paint/radial_gradient.json
new file mode 100644
index 0000000..29b5f46
--- /dev/null
+++ b/dev/defs/shapes/paint/radial_gradient.json
@@ -0,0 +1,8 @@
+{
+  "name": "RadialGradient",
+  "key": {
+    "int": 17,
+    "string": "radialgradient"
+  },
+  "extends": "shapes/paint/linear_gradient.json"
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/paint/shape_paint.json b/dev/defs/shapes/paint/shape_paint.json
new file mode 100644
index 0000000..979ffc2
--- /dev/null
+++ b/dev/defs/shapes/paint/shape_paint.json
@@ -0,0 +1,20 @@
+{
+  "name": "ShapePaint",
+  "key": {
+    "int": 21,
+    "string": "shapePaint"
+  },
+  "abstract": true,
+  "extends": "container_component.json",
+  "properties": {
+    "isVisible": {
+      "type": "bool",
+      "initialValue": "true",
+      "key": {
+        "int": 41,
+        "string": "isVisible"
+      },
+      "virtual": true
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/paint/solid_color.json b/dev/defs/shapes/paint/solid_color.json
new file mode 100644
index 0000000..7182495
--- /dev/null
+++ b/dev/defs/shapes/paint/solid_color.json
@@ -0,0 +1,19 @@
+{
+  "name": "SolidColor",
+  "key": {
+    "int": 18,
+    "string": "solidcolor"
+  },
+  "extends": "component.json",
+  "properties": {
+    "colorValue": {
+      "type": "Color",
+      "initialValue": "0xFF747474",
+      "animates": true,
+      "key": {
+        "int": 37,
+        "string": "color"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/paint/stroke.json b/dev/defs/shapes/paint/stroke.json
new file mode 100644
index 0000000..10f2002
--- /dev/null
+++ b/dev/defs/shapes/paint/stroke.json
@@ -0,0 +1,43 @@
+{
+  "name": "Stroke",
+  "key": {
+    "int": 24,
+    "string": "stroke"
+  },
+  "extends": "shapes/paint/shape_paint.json",
+  "properties": {
+    "thickness": {
+      "type": "double",
+      "initialValue": "1",
+      "animates": true,
+      "key": {
+        "int": 47,
+        "string": "thickness"
+      }
+    },
+    "cap": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 48,
+        "string": "cap"
+      }
+    },
+    "join": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 49,
+        "string": "join"
+      }
+    },
+    "transformAffectsStroke": {
+      "type": "bool",
+      "initialValue": "true",
+      "key": {
+        "int": 50,
+        "string": "transformaffectsstroke"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/paint/trim_path.json b/dev/defs/shapes/paint/trim_path.json
new file mode 100644
index 0000000..8dfd591
--- /dev/null
+++ b/dev/defs/shapes/paint/trim_path.json
@@ -0,0 +1,45 @@
+{
+  "name": "TrimPath",
+  "key": {
+    "int": 47,
+    "string": "trimpath"
+  },
+  "extends": "component.json",
+  "properties": {
+    "start": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 114,
+        "string": "trimStart"
+      }
+    },
+    "end": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 115,
+        "string": "trimEnd"
+      }
+    },
+    "offset": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 116,
+        "string": "trimOffset"
+      }
+    },
+    "modeValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 117,
+        "string": "modevalue"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/parametric_path.json b/dev/defs/shapes/parametric_path.json
new file mode 100644
index 0000000..470e46f
--- /dev/null
+++ b/dev/defs/shapes/parametric_path.json
@@ -0,0 +1,51 @@
+{
+  "name": "ParametricPath",
+  "key": {
+    "int": 15,
+    "string": "parametricpath"
+  },
+  "abstract": true,
+  "extends": "shapes/path.json",
+  "properties": {
+    "width": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 20,
+        "string": "width"
+      },
+      "description": "Width of the parametric path."
+    },
+    "height": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 21,
+        "string": "height"
+      },
+      "description": "Height of the parametric path."
+    },
+    "originX": {
+      "type": "double",
+      "initialValue": "0.5",
+      "animates": true,
+      "key": {
+        "int": 123,
+        "string": "originx"
+      },
+      "description": "Origin x in normalized coordinates (0.5 = center, 0 = left, 1 = right)."
+    },
+    "originY": {
+      "type": "double",
+      "initialValue": "0.5",
+      "animates": true,
+      "key": {
+        "int": 124,
+        "string": "originy"
+      },
+      "description": "Origin y in normalized coordinates (0.5 = center, 0 = top, 1 = bottom)."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/path.json b/dev/defs/shapes/path.json
new file mode 100644
index 0000000..6ad1dee
--- /dev/null
+++ b/dev/defs/shapes/path.json
@@ -0,0 +1,20 @@
+{
+  "name": "Path",
+  "key": {
+    "int": 12,
+    "string": "path"
+  },
+  "abstract": true,
+  "extends": "node.json",
+  "properties": {
+    "pathFlags": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 128,
+        "string": "pathflags"
+      },
+      "coop": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/path_vertex.json b/dev/defs/shapes/path_vertex.json
new file mode 100644
index 0000000..5021bf8
--- /dev/null
+++ b/dev/defs/shapes/path_vertex.json
@@ -0,0 +1,10 @@
+{
+  "name": "PathVertex",
+  "key": {
+    "int": 14,
+    "string": "pathvertex"
+  },
+  "abstract": true,
+  "extends": "shapes/vertex.json",
+  "genericPassThrough": "bones/weight.json"
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/points_path.json b/dev/defs/shapes/points_path.json
new file mode 100644
index 0000000..d8a2912
--- /dev/null
+++ b/dev/defs/shapes/points_path.json
@@ -0,0 +1,30 @@
+{
+  "name": "PointsPath",
+  "key": {
+    "int": 16,
+    "string": "pointspath"
+  },
+  "extends": "shapes/path.json",
+  "properties": {
+    "isClosed": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 32,
+        "string": "isclosed"
+      },
+      "description": "If the path should close back on its first vertex."
+    },
+    "editingModeValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 74,
+        "string": "editingmode"
+      },
+      "description": "We use this to track the edit-time state of the PointsPath for the VertexEditor.",
+      "runtime": false,
+      "coop": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/polygon.json b/dev/defs/shapes/polygon.json
new file mode 100644
index 0000000..864dbad
--- /dev/null
+++ b/dev/defs/shapes/polygon.json
@@ -0,0 +1,28 @@
+{
+  "name": "Polygon",
+  "key": {
+    "int": 51,
+    "string": "polygon"
+  },
+  "extends": "shapes/parametric_path.json",
+  "properties": {
+    "points": {
+      "type": "uint",
+      "initialValue": "5",
+      "key": {
+        "int": 125,
+        "string": "points"
+      },
+      "description": "The number of points for the polygon."
+    },
+    "cornerRadius": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 126,
+        "string": "cornerradius"
+      },
+      "description": "The corner radius."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/rectangle.json b/dev/defs/shapes/rectangle.json
new file mode 100644
index 0000000..59fae90
--- /dev/null
+++ b/dev/defs/shapes/rectangle.json
@@ -0,0 +1,59 @@
+{
+  "name": "Rectangle",
+  "key": {
+    "int": 7,
+    "string": "rectangle"
+  },
+  "extends": "shapes/parametric_path.json",
+  "properties": {
+    "linkCornerRadius": {
+      "type": "bool",
+      "initialValue": "true",
+      "key": {
+        "int": 164,
+        "string": "linkcornerradius"
+      },
+      "description": "Whether the TL corner radius defines all the radiuses"
+    },
+    "cornerRadiusTL": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 31,
+        "string": "cornerRadiusTL"
+      },
+      "description": "Top left radius of the corners of this rectangle"
+    },
+    "cornerRadiusTR": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 161,
+        "string": "cornerRadiusTR"
+      },
+      "description": "Top right radius of the corners of this rectangle"
+    },
+    "cornerRadiusBL": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 162,
+        "string": "cornerRadiusBL"
+      },
+      "description": "Bottom left radius of the corners of this rectangle"
+    },
+    "cornerRadiusBR": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 163,
+        "string": "cornerRadiusBR"
+      },
+      "description": "Bottom right radius of the corners of this rectangle"
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/shape.json b/dev/defs/shapes/shape.json
new file mode 100644
index 0000000..0992623
--- /dev/null
+++ b/dev/defs/shapes/shape.json
@@ -0,0 +1,8 @@
+{
+  "name": "Shape",
+  "key": {
+    "int": 3,
+    "string": "shape"
+  },
+  "extends": "drawable.json"
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/star.json b/dev/defs/shapes/star.json
new file mode 100644
index 0000000..c80122a
--- /dev/null
+++ b/dev/defs/shapes/star.json
@@ -0,0 +1,20 @@
+{
+  "name": "Star",
+  "key": {
+    "int": 52,
+    "string": "star"
+  },
+  "extends": "shapes/polygon.json",
+  "properties": {
+    "innerRadius": {
+      "type": "double",
+      "initialValue": "0.5",
+      "animates": true,
+      "key": {
+        "int": 127,
+        "string": "inner_radius"
+      },
+      "description": "Percentage of width/height to project inner points of the star."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/straight_vertex.json b/dev/defs/shapes/straight_vertex.json
new file mode 100644
index 0000000..186e46a
--- /dev/null
+++ b/dev/defs/shapes/straight_vertex.json
@@ -0,0 +1,21 @@
+{
+  "name": "StraightVertex",
+  "key": {
+    "int": 5,
+    "string": "straightvertex"
+  },
+  "extends": "shapes/path_vertex.json",
+  "generic": "bones/weight.json",
+  "properties": {
+    "radius": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 26,
+        "string": "radius"
+      },
+      "description": "Radius of the vertex"
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/triangle.json b/dev/defs/shapes/triangle.json
new file mode 100644
index 0000000..fc9c44b
--- /dev/null
+++ b/dev/defs/shapes/triangle.json
@@ -0,0 +1,8 @@
+{
+  "name": "Triangle",
+  "key": {
+    "int": 8,
+    "string": "triangle"
+  },
+  "extends": "shapes/parametric_path.json"
+}
\ No newline at end of file
diff --git a/dev/defs/shapes/vertex.json b/dev/defs/shapes/vertex.json
new file mode 100644
index 0000000..524de3b
--- /dev/null
+++ b/dev/defs/shapes/vertex.json
@@ -0,0 +1,33 @@
+{
+  "name": "Vertex",
+  "key": {
+    "int": 107,
+    "string": "vertex"
+  },
+  "abstract": true,
+  "extends": "container_component.json",
+  "properties": {
+    "x": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "position",
+      "key": {
+        "int": 24,
+        "string": "x"
+      },
+      "description": "X value for the translation of the vertex."
+    },
+    "y": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "position",
+      "key": {
+        "int": 25,
+        "string": "y"
+      },
+      "description": "Y value for the translation of the vertex."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/solo.json b/dev/defs/solo.json
new file mode 100644
index 0000000..8c888b8
--- /dev/null
+++ b/dev/defs/solo.json
@@ -0,0 +1,22 @@
+{
+  "name": "Solo",
+  "key": {
+    "int": 147,
+    "string": "solo"
+  },
+  "extends": "node.json",
+  "properties": {
+    "activeComponentId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "0",
+      "animates": true,
+      "key": {
+        "int": 296,
+        "string": "activeComponentId"
+      },
+      "description": "Identifier of the active child in the solo set."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/tag.json b/dev/defs/tag.json
new file mode 100644
index 0000000..58c6ebf
--- /dev/null
+++ b/dev/defs/tag.json
@@ -0,0 +1,57 @@
+{
+  "name": "Tag",
+  "key": {
+    "int": 166,
+    "string": "tag"
+  },
+  "runtime": false,
+  "properties": {
+    "name": {
+      "type": "String",
+      "initialValue": "''",
+      "key": {
+        "int": 383,
+        "string": "name"
+      },
+      "runtime": false
+    },
+    "colorValue": {
+      "type": "Color",
+      "initialValue": "0xFF57A5E0",
+      "key": {
+        "int": 384,
+        "string": "colorvalue"
+      },
+      "runtime": false
+    },
+    "flags": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 385,
+        "string": "flags"
+      },
+      "runtime": false
+    },
+    "order": {
+      "type": "FractionalIndex",
+      "initialValue": "FractionalIndex.invalid",
+      "key": {
+        "int": 388,
+        "string": "order"
+      },
+      "description": "Order this tag shows up in the tags list.",
+      "runtime": false
+    },
+    "dependentIds": {
+      "type": "List<Id>",
+      "initialValue": "[]",
+      "key": {
+        "int": 386,
+        "string": "dependentids"
+      },
+      "description": "Object ids assigned to this tag.",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/text/text.json b/dev/defs/text/text.json
new file mode 100644
index 0000000..7316395
--- /dev/null
+++ b/dev/defs/text/text.json
@@ -0,0 +1,104 @@
+{
+  "name": "Text",
+  "key": {
+    "int": 134,
+    "string": "text"
+  },
+  "extends": "drawable.json",
+  "properties": {
+    "alignValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 281,
+        "string": "alignvalue"
+      }
+    },
+    "sizingValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 284,
+        "string": "sizingvalue"
+      }
+    },
+    "overflowValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 287,
+        "string": "overflowvalue"
+      },
+      "description": "One of visible, hidden, clipped, ellipsis."
+    },
+    "width": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 285,
+        "string": "width"
+      },
+      "description": "Width of the text object."
+    },
+    "height": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 286,
+        "string": "height"
+      },
+      "description": "Height of the text object."
+    },
+    "highlightTextRuns": {
+      "type": "bool",
+      "initialValue": "true",
+      "key": {
+        "int": 310,
+        "string": "highlighttextruns"
+      },
+      "description": "Whether to draw colored underlines to identify runs.",
+      "runtime": false,
+      "coop": false
+    },
+    "originX": {
+      "type": "double",
+      "initialValue": "0.0",
+      "animates": true,
+      "key": {
+        "int": 366,
+        "string": "originx"
+      },
+      "description": "Origin x in normalized coordinates (0.5 = center, 0 = left, 1 = right)."
+    },
+    "originY": {
+      "type": "double",
+      "initialValue": "0.0",
+      "animates": true,
+      "key": {
+        "int": 367,
+        "string": "originy"
+      },
+      "description": "Origin y in normalized coordinates (0.5 = center, 0 = top, 1 = bottom)."
+    },
+    "paragraphSpacing": {
+      "type": "double",
+      "initialValue": "0.0",
+      "animates": true,
+      "key": {
+        "int": 371,
+        "string": "paragraphspacing"
+      }
+    },
+    "originValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 377,
+        "string": "originValue"
+      },
+      "description": "Logical starting location of origin."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/text/text_modifier.json b/dev/defs/text/text_modifier.json
new file mode 100644
index 0000000..ffd4f6d
--- /dev/null
+++ b/dev/defs/text/text_modifier.json
@@ -0,0 +1,9 @@
+{
+  "name": "TextModifier",
+  "key": {
+    "int": 160,
+    "string": "textmodifier"
+  },
+  "abstract": true,
+  "extends": "component.json"
+}
\ No newline at end of file
diff --git a/dev/defs/text/text_modifier_group.json b/dev/defs/text/text_modifier_group.json
new file mode 100644
index 0000000..496c10f
--- /dev/null
+++ b/dev/defs/text/text_modifier_group.json
@@ -0,0 +1,96 @@
+{
+  "name": "TextModifierGroup",
+  "key": {
+    "int": 159,
+    "string": "textmodifiergroup"
+  },
+  "extends": "container_component.json",
+  "properties": {
+    "modifierFlags": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 335,
+        "string": "modifierflags"
+      }
+    },
+    "originX": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "origin",
+      "key": {
+        "int": 328,
+        "string": "x"
+      }
+    },
+    "originY": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "origin",
+      "key": {
+        "int": 329,
+        "string": "y"
+      }
+    },
+    "opacity": {
+      "type": "double",
+      "initialValue": "1",
+      "animates": true,
+      "key": {
+        "int": 324,
+        "string": "opacity"
+      }
+    },
+    "x": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "position",
+      "key": {
+        "int": 322,
+        "string": "x"
+      }
+    },
+    "y": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "group": "position",
+      "key": {
+        "int": 323,
+        "string": "y"
+      }
+    },
+    "rotation": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 332,
+        "string": "rotation"
+      }
+    },
+    "scaleX": {
+      "type": "double",
+      "initialValue": "1",
+      "animates": true,
+      "group": "scale",
+      "key": {
+        "int": 330,
+        "string": "x"
+      }
+    },
+    "scaleY": {
+      "type": "double",
+      "initialValue": "1",
+      "animates": true,
+      "group": "scale",
+      "key": {
+        "int": 331,
+        "string": "y"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/text/text_modifier_range.json b/dev/defs/text/text_modifier_range.json
new file mode 100644
index 0000000..d88db5e
--- /dev/null
+++ b/dev/defs/text/text_modifier_range.json
@@ -0,0 +1,107 @@
+{
+  "name": "TextModifierRange",
+  "key": {
+    "int": 158,
+    "string": "textmodifierrange"
+  },
+  "extends": "container_component.json",
+  "properties": {
+    "modifyFrom": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 327,
+        "string": "modifyfrom"
+      }
+    },
+    "modifyTo": {
+      "type": "double",
+      "initialValue": "1",
+      "animates": true,
+      "key": {
+        "int": 336,
+        "string": "modifyto"
+      }
+    },
+    "strength": {
+      "type": "double",
+      "initialValue": "1",
+      "animates": true,
+      "key": {
+        "int": 334,
+        "string": "strength"
+      }
+    },
+    "unitsValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 316,
+        "string": "unitsvalue"
+      }
+    },
+    "typeValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 325,
+        "string": "typevalue"
+      }
+    },
+    "modeValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 326,
+        "string": "modevalue"
+      }
+    },
+    "clamp": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 333,
+        "string": "clamp"
+      }
+    },
+    "falloffFrom": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 317,
+        "string": "fallofffrom"
+      }
+    },
+    "falloffTo": {
+      "type": "double",
+      "initialValue": "1",
+      "animates": true,
+      "key": {
+        "int": 318,
+        "string": "falloffto"
+      }
+    },
+    "offset": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 319,
+        "string": "offset"
+      }
+    },
+    "runId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 378,
+        "string": "runid"
+      },
+      "description": "Identifier used to which run should be targeted."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/text/text_shape_modifier.json b/dev/defs/text/text_shape_modifier.json
new file mode 100644
index 0000000..0b02033
--- /dev/null
+++ b/dev/defs/text/text_shape_modifier.json
@@ -0,0 +1,9 @@
+{
+  "name": "TextShapeModifier",
+  "key": {
+    "int": 161,
+    "string": "textshapemodifier"
+  },
+  "abstract": true,
+  "extends": "text/text_modifier.json"
+}
\ No newline at end of file
diff --git a/dev/defs/text/text_style.json b/dev/defs/text/text_style.json
new file mode 100644
index 0000000..c20fba6
--- /dev/null
+++ b/dev/defs/text/text_style.json
@@ -0,0 +1,74 @@
+{
+  "name": "TextStyle",
+  "key": {
+    "int": 137,
+    "string": "textstyle"
+  },
+  "extends": "container_component.json",
+  "properties": {
+    "fontSize": {
+      "type": "double",
+      "initialValue": "12",
+      "animates": true,
+      "key": {
+        "int": 274,
+        "string": "fontsize"
+      }
+    },
+    "lineHeight": {
+      "type": "double",
+      "initialValue": "-1.0",
+      "animates": true,
+      "key": {
+        "int": 370,
+        "string": "lineheight"
+      }
+    },
+    "letterSpacing": {
+      "type": "double",
+      "initialValue": "0.0",
+      "animates": true,
+      "key": {
+        "int": 390,
+        "string": "letterspacing"
+      }
+    },
+    "fontAssetId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 279,
+        "string": "fontassetid"
+      }
+    },
+    "familyName": {
+      "type": "String",
+      "initialValue": "''",
+      "key": {
+        "int": 341,
+        "string": "familyname"
+      },
+      "runtime": false
+    },
+    "styleName": {
+      "type": "String",
+      "initialValue": "''",
+      "key": {
+        "int": 342,
+        "string": "stylename"
+      },
+      "runtime": false
+    },
+    "isCustomFont": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 353,
+        "string": "iscustomfont"
+      },
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/text/text_style_axis.json b/dev/defs/text/text_style_axis.json
new file mode 100644
index 0000000..23524e9
--- /dev/null
+++ b/dev/defs/text/text_style_axis.json
@@ -0,0 +1,27 @@
+{
+  "name": "TextStyleAxis",
+  "key": {
+    "int": 144,
+    "string": "textStyleAxis"
+  },
+  "extends": "component.json",
+  "properties": {
+    "tag": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 289,
+        "string": "tag"
+      }
+    },
+    "axisValue": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 288,
+        "string": "axisValue"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/text/text_style_feature.json b/dev/defs/text/text_style_feature.json
new file mode 100644
index 0000000..9c64015
--- /dev/null
+++ b/dev/defs/text/text_style_feature.json
@@ -0,0 +1,27 @@
+{
+  "name": "TextStyleFeature",
+  "key": {
+    "int": 164,
+    "string": "textstylefeature"
+  },
+  "extends": "component.json",
+  "properties": {
+    "tag": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 356,
+        "string": "tag"
+      }
+    },
+    "featureValue": {
+      "type": "uint",
+      "initialValue": "1",
+      "animates": true,
+      "key": {
+        "int": 357,
+        "string": "featurevalue"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/text/text_value_run.json b/dev/defs/text/text_value_run.json
new file mode 100644
index 0000000..ecb8931
--- /dev/null
+++ b/dev/defs/text/text_value_run.json
@@ -0,0 +1,42 @@
+{
+  "name": "TextValueRun",
+  "key": {
+    "int": 135,
+    "string": "textvaluerun"
+  },
+  "extends": "component.json",
+  "properties": {
+    "styleId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "animates": true,
+      "key": {
+        "int": 272,
+        "string": "styleid"
+      },
+      "description": "The id of the style to be applied to this run."
+    },
+    "text": {
+      "type": "String",
+      "initialValue": "''",
+      "animates": true,
+      "key": {
+        "int": 268,
+        "string": "text_value"
+      },
+      "description": "The text string value."
+    },
+    "fieldHeight": {
+      "type": "double",
+      "initialValue": "98.0",
+      "key": {
+        "int": 398,
+        "string": "fieldheight"
+      },
+      "runtime": false,
+      "coop": false
+    }
+  }
+}
diff --git a/dev/defs/text/text_variation_modifier.json b/dev/defs/text/text_variation_modifier.json
new file mode 100644
index 0000000..674b625
--- /dev/null
+++ b/dev/defs/text/text_variation_modifier.json
@@ -0,0 +1,27 @@
+{
+  "name": "TextVariationModifier",
+  "key": {
+    "int": 162,
+    "string": "textvariationmodifier"
+  },
+  "extends": "text/text_shape_modifier.json",
+  "properties": {
+    "axisTag": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 320,
+        "string": "axistag"
+      }
+    },
+    "axisValue": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 321,
+        "string": "axisvalue"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/transform_component.json b/dev/defs/transform_component.json
new file mode 100644
index 0000000..084705f
--- /dev/null
+++ b/dev/defs/transform_component.json
@@ -0,0 +1,40 @@
+{
+  "name": "TransformComponent",
+  "key": {
+    "int": 38,
+    "string": "transformcomponent"
+  },
+  "abstract": true,
+  "extends": "world_transform_component.json",
+  "properties": {
+    "rotation": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 15,
+        "string": "r"
+      }
+    },
+    "scaleX": {
+      "type": "double",
+      "initialValue": "1",
+      "animates": true,
+      "group": "scale",
+      "key": {
+        "int": 16,
+        "string": "sx"
+      }
+    },
+    "scaleY": {
+      "type": "double",
+      "initialValue": "1",
+      "animates": true,
+      "group": "scale",
+      "key": {
+        "int": 17,
+        "string": "sy"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/data_enum.json b/dev/defs/viewmodel/data_enum.json
new file mode 100644
index 0000000..c65b252
--- /dev/null
+++ b/dev/defs/viewmodel/data_enum.json
@@ -0,0 +1,40 @@
+{
+  "name": "DataEnum",
+  "key": {
+    "int": 438,
+    "string": "dataenum"
+  },
+  "properties": {
+    "order": {
+      "type": "FractionalIndex",
+      "initialValue": "FractionalIndex.invalid",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 570,
+        "string": "order"
+      },
+      "description": "Order value for sorting data enums.",
+      "runtime": false
+    },
+    "splitKeyValues": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 571,
+        "string": "splitkeyvalues"
+      },
+      "description": "Whether the user can edit keys and values separately.",
+      "runtime": false
+    },
+    "name": {
+      "type": "String",
+      "initialValue": "''",
+      "key": {
+        "int": 572,
+        "string": "name"
+      },
+      "description": "Non-unique identifier, used to give friendly names to enums.",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/data_enum_value.json b/dev/defs/viewmodel/data_enum_value.json
new file mode 100644
index 0000000..d597785
--- /dev/null
+++ b/dev/defs/viewmodel/data_enum_value.json
@@ -0,0 +1,48 @@
+{
+  "name": "DataEnumValue",
+  "key": {
+    "int": 445,
+    "string": "dataenumvalue"
+  },
+  "properties": {
+    "key": {
+      "type": "String",
+      "initialValue": "''",
+      "key": {
+        "int": 578,
+        "string": "key"
+      },
+      "description": "The key of this key value enum pair."
+    },
+    "value": {
+      "type": "String",
+      "initialValue": "''",
+      "key": {
+        "int": 579,
+        "string": "value"
+      },
+      "description": "The value of this key value enum pair."
+    },
+    "enumId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 580,
+        "string": "enumid"
+      },
+      "description": "The id of the enum property this value belongs to.",
+      "runtime": false
+    },
+    "order": {
+      "type": "FractionalIndex",
+      "initialValue": "FractionalIndex.invalid",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 581,
+        "string": "order"
+      },
+      "description": "Order value for sorting enum values.",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/viewmodel.json b/dev/defs/viewmodel/viewmodel.json
new file mode 100644
index 0000000..bff9a16
--- /dev/null
+++ b/dev/defs/viewmodel/viewmodel.json
@@ -0,0 +1,32 @@
+{
+  "name": "ViewModel",
+  "key": {
+    "int": 435,
+    "string": "viewmodel"
+  },
+  "extends": "viewmodel/viewmodel_component.json",
+  "properties": {
+    "viewModelOrder": {
+      "type": "FractionalIndex",
+      "initialValue": "FractionalIndex.invalid",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 563,
+        "string": "viewmodelorder"
+      },
+      "description": "Order value for sorting View Models.",
+      "runtime": false
+    },
+    "defaultInstanceId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 564,
+        "string": "defaultinstanceid"
+      },
+      "description": "The default instance attached to the view model."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/viewmodel_component.json b/dev/defs/viewmodel/viewmodel_component.json
new file mode 100644
index 0000000..fd68e1e
--- /dev/null
+++ b/dev/defs/viewmodel/viewmodel_component.json
@@ -0,0 +1,18 @@
+{
+  "name": "ViewModelComponent",
+  "key": {
+    "int": 429,
+    "string": "viewmodelcomponent"
+  },
+  "properties": {
+    "name": {
+      "type": "String",
+      "initialValue": "''",
+      "key": {
+        "int": 557,
+        "string": "name"
+      },
+      "description": "Non-unique identifier, used to give friendly names to any view model component."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/viewmodel_instance.json b/dev/defs/viewmodel/viewmodel_instance.json
new file mode 100644
index 0000000..1ac4447
--- /dev/null
+++ b/dev/defs/viewmodel/viewmodel_instance.json
@@ -0,0 +1,50 @@
+{
+  "name": "ViewModelInstance",
+  "key": {
+    "int": 437,
+    "string": "viewmodelinstance"
+  },
+  "extends": "component.json",
+  "properties": {
+    "viewModelId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 566,
+        "string": "viewmodelid"
+      }
+    },
+    "x": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 567,
+        "string": "x"
+      },
+      "description": "The x coordinate of the stage representation.",
+      "runtime": false
+    },
+    "y": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 568,
+        "string": "y"
+      },
+      "description": "The y coordinate of the stage representation.",
+      "runtime": false
+    },
+    "onStage": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 569,
+        "string": "onstage"
+      },
+      "description": "Whether it is visible on stage or not.",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/viewmodel_instance_boolean.json b/dev/defs/viewmodel/viewmodel_instance_boolean.json
new file mode 100644
index 0000000..a3ea1d5
--- /dev/null
+++ b/dev/defs/viewmodel/viewmodel_instance_boolean.json
@@ -0,0 +1,29 @@
+{
+  "name": "ViewModelInstanceBoolean",
+  "key": {
+    "int": 449,
+    "string": "viewmodelinstanceboolean"
+  },
+  "extends": "viewmodel/viewmodel_instance_value.json",
+  "properties": {
+    "propertyValue": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 593,
+        "string": "propertyvalue"
+      },
+      "description": "The boolean value."
+    },
+    "playbackValue": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 594,
+        "string": "playbackvalue"
+      },
+      "runtime": false,
+      "coop": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/viewmodel_instance_color.json b/dev/defs/viewmodel/viewmodel_instance_color.json
new file mode 100644
index 0000000..23ae5df
--- /dev/null
+++ b/dev/defs/viewmodel/viewmodel_instance_color.json
@@ -0,0 +1,29 @@
+{
+  "name": "ViewModelInstanceColor",
+  "key": {
+    "int": 426,
+    "string": "viewmodelinstancecolor"
+  },
+  "extends": "viewmodel/viewmodel_instance_value.json",
+  "properties": {
+    "propertyValue": {
+      "type": "Color",
+      "initialValue": "0xFF1D1D1D",
+      "key": {
+        "int": 555,
+        "string": "propertyvalue"
+      },
+      "description": "The color value"
+    },
+    "playbackValue": {
+      "type": "Color",
+      "initialValue": "0xFF1D1D1D",
+      "key": {
+        "int": 556,
+        "string": "playbackvalue"
+      },
+      "runtime": false,
+      "coop": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/viewmodel_instance_enum.json b/dev/defs/viewmodel/viewmodel_instance_enum.json
new file mode 100644
index 0000000..7dd93df
--- /dev/null
+++ b/dev/defs/viewmodel/viewmodel_instance_enum.json
@@ -0,0 +1,21 @@
+{
+  "name": "ViewModelInstanceEnum",
+  "key": {
+    "int": 432,
+    "string": "viewmodelinstanceenum"
+  },
+  "extends": "viewmodel/viewmodel_instance_value.json",
+  "properties": {
+    "propertyValue": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 560,
+        "string": "propertyvalue"
+      },
+      "description": "The id of the enum value."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/viewmodel_instance_list.json b/dev/defs/viewmodel/viewmodel_instance_list.json
new file mode 100644
index 0000000..d38e669
--- /dev/null
+++ b/dev/defs/viewmodel/viewmodel_instance_list.json
@@ -0,0 +1,8 @@
+{
+  "name": "ViewModelInstanceList",
+  "key": {
+    "int": 441,
+    "string": "viewmodelinstancelist"
+  },
+  "extends": "viewmodel/viewmodel_instance_value.json"
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/viewmodel_instance_list_item.json b/dev/defs/viewmodel/viewmodel_instance_list_item.json
new file mode 100644
index 0000000..30938fb
--- /dev/null
+++ b/dev/defs/viewmodel/viewmodel_instance_list_item.json
@@ -0,0 +1,71 @@
+{
+  "name": "ViewModelInstanceListItem",
+  "key": {
+    "int": 427,
+    "string": "viewmodelinstancelistitem"
+  },
+  "properties": {
+    "useLinkedArtboard": {
+      "type": "bool",
+      "initialValue": "true",
+      "key": {
+        "int": 547,
+        "string": "uselinkedartboard"
+      },
+      "description": "Whether the artboard linked to the view model should be used."
+    },
+    "instanceListId": {
+      "type": "Id",
+      "initialValue": "Core.missingId",
+      "key": {
+        "int": 548,
+        "string": "instancelistid"
+      },
+      "description": "The id of the list it belongs to.",
+      "runtime": false
+    },
+    "viewModelId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 549,
+        "string": "viewmodelid"
+      },
+      "description": "The view model id."
+    },
+    "viewModelInstanceId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 550,
+        "string": "viewmodelinstanceid"
+      },
+      "description": "The view model instance id."
+    },
+    "artboardId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 551,
+        "string": "artboardid"
+      },
+      "description": "The artboard id to link the viewmodel to if implicit is set to false."
+    },
+    "order": {
+      "type": "FractionalIndex",
+      "initialValue": "FractionalIndex.invalid",
+      "key": {
+        "int": 552,
+        "string": "order"
+      },
+      "description": "The order position in the list.",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/viewmodel_instance_number.json b/dev/defs/viewmodel/viewmodel_instance_number.json
new file mode 100644
index 0000000..e0637f9
--- /dev/null
+++ b/dev/defs/viewmodel/viewmodel_instance_number.json
@@ -0,0 +1,29 @@
+{
+  "name": "ViewModelInstanceNumber",
+  "key": {
+    "int": 442,
+    "string": "viewmodelinstancenumber"
+  },
+  "extends": "viewmodel/viewmodel_instance_value.json",
+  "properties": {
+    "propertyValue": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 575,
+        "string": "propertyvalue"
+      },
+      "description": "The number value."
+    },
+    "playbackValue": {
+      "type": "double",
+      "initialValue": "0",
+      "key": {
+        "int": 576,
+        "string": "playbackvalue"
+      },
+      "runtime": false,
+      "coop": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/viewmodel_instance_string.json b/dev/defs/viewmodel/viewmodel_instance_string.json
new file mode 100644
index 0000000..bd12abb
--- /dev/null
+++ b/dev/defs/viewmodel/viewmodel_instance_string.json
@@ -0,0 +1,29 @@
+{
+  "name": "ViewModelInstanceString",
+  "key": {
+    "int": 433,
+    "string": "viewmodelinstancestring"
+  },
+  "extends": "viewmodel/viewmodel_instance_value.json",
+  "properties": {
+    "propertyValue": {
+      "type": "String",
+      "initialValue": "''",
+      "key": {
+        "int": 561,
+        "string": "propertyvalue"
+      },
+      "description": "The string value."
+    },
+    "playbackValue": {
+      "type": "String",
+      "initialValue": "''",
+      "key": {
+        "int": 562,
+        "string": "playbackvalue"
+      },
+      "runtime": false,
+      "coop": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/viewmodel_instance_value.json b/dev/defs/viewmodel/viewmodel_instance_value.json
new file mode 100644
index 0000000..df37f83
--- /dev/null
+++ b/dev/defs/viewmodel/viewmodel_instance_value.json
@@ -0,0 +1,33 @@
+{
+  "name": "ViewModelInstanceValue",
+  "key": {
+    "int": 428,
+    "string": "viewmodelinstancevalue"
+  },
+  "abstract": true,
+  "properties": {
+    "viewModelInstanceId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 553,
+        "string": "viewmodelinstanceid"
+      },
+      "description": "Identifier of the view model instance this value is for, at runtime this is expected in context (after) a ViewModelInstance object.",
+      "runtime": false
+    },
+    "viewModelPropertyId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 554,
+        "string": "viewmodelpropertyid"
+      },
+      "description": "Identifier of the property this value will provide data for. At runtime these ids are normalized relative to the ViewModel itself."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/viewmodel_instance_viewmodel.json b/dev/defs/viewmodel/viewmodel_instance_viewmodel.json
new file mode 100644
index 0000000..e156b14
--- /dev/null
+++ b/dev/defs/viewmodel/viewmodel_instance_viewmodel.json
@@ -0,0 +1,21 @@
+{
+  "name": "ViewModelInstanceViewModel",
+  "key": {
+    "int": 444,
+    "string": "viewmodelinstanceviewmodel"
+  },
+  "extends": "viewmodel/viewmodel_instance_value.json",
+  "properties": {
+    "propertyValue": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 577,
+        "string": "propertyvalue"
+      },
+      "description": "The id of the viewmodel instance."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/viewmodel_property.json b/dev/defs/viewmodel/viewmodel_property.json
new file mode 100644
index 0000000..5ee2482
--- /dev/null
+++ b/dev/defs/viewmodel/viewmodel_property.json
@@ -0,0 +1,33 @@
+{
+  "name": "ViewModelProperty",
+  "key": {
+    "int": 430,
+    "string": "viewmodelproperty"
+  },
+  "extends": "viewmodel/viewmodel_component.json",
+  "properties": {
+    "viewModelId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 558,
+        "string": "viewmodelid"
+      },
+      "description": "Identifier used to track parent View Model.",
+      "runtime": false
+    },
+    "propertyOrder": {
+      "type": "FractionalIndex",
+      "initialValue": "FractionalIndex.invalid",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 559,
+        "string": "propertyorder"
+      },
+      "description": "Order value for sorting child elements in View Model.",
+      "runtime": false
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/viewmodel_property_boolean.json b/dev/defs/viewmodel/viewmodel_property_boolean.json
new file mode 100644
index 0000000..a770346
--- /dev/null
+++ b/dev/defs/viewmodel/viewmodel_property_boolean.json
@@ -0,0 +1,8 @@
+{
+  "name": "ViewModelPropertyBoolean",
+  "key": {
+    "int": 448,
+    "string": "viewmodelpropertyboolean"
+  },
+  "extends": "viewmodel/viewmodel_property.json"
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/viewmodel_property_color.json b/dev/defs/viewmodel/viewmodel_property_color.json
new file mode 100644
index 0000000..c64d0ff
--- /dev/null
+++ b/dev/defs/viewmodel/viewmodel_property_color.json
@@ -0,0 +1,8 @@
+{
+  "name": "ViewModelPropertyColor",
+  "key": {
+    "int": 440,
+    "string": "viewmodelpropertycolor"
+  },
+  "extends": "viewmodel/viewmodel_property.json"
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/viewmodel_property_enum.json b/dev/defs/viewmodel/viewmodel_property_enum.json
new file mode 100644
index 0000000..2706486
--- /dev/null
+++ b/dev/defs/viewmodel/viewmodel_property_enum.json
@@ -0,0 +1,31 @@
+{
+  "name": "ViewModelPropertyEnum",
+  "key": {
+    "int": 439,
+    "string": "viewmodelpropertyenum"
+  },
+  "extends": "viewmodel/viewmodel_property.json",
+  "properties": {
+    "splitKeyValues": {
+      "type": "bool",
+      "initialValue": "false",
+      "key": {
+        "int": 573,
+        "string": "splitkeyvalues"
+      },
+      "description": "Whether the user can edit keys and values separately.",
+      "runtime": false
+    },
+    "enumId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 574,
+        "string": "enumid"
+      },
+      "description": "The id of the enum."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/viewmodel_property_list.json b/dev/defs/viewmodel/viewmodel_property_list.json
new file mode 100644
index 0000000..1fa6b0e
--- /dev/null
+++ b/dev/defs/viewmodel/viewmodel_property_list.json
@@ -0,0 +1,8 @@
+{
+  "name": "ViewModelPropertyList",
+  "key": {
+    "int": 434,
+    "string": "viewmodelpropertylist"
+  },
+  "extends": "viewmodel/viewmodel_property.json"
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/viewmodel_property_number.json b/dev/defs/viewmodel/viewmodel_property_number.json
new file mode 100644
index 0000000..ef9cced
--- /dev/null
+++ b/dev/defs/viewmodel/viewmodel_property_number.json
@@ -0,0 +1,8 @@
+{
+  "name": "ViewModelPropertyNumber",
+  "key": {
+    "int": 431,
+    "string": "viewmodelpropertynumber"
+  },
+  "extends": "viewmodel/viewmodel_property.json"
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/viewmodel_property_string.json b/dev/defs/viewmodel/viewmodel_property_string.json
new file mode 100644
index 0000000..ca95b29
--- /dev/null
+++ b/dev/defs/viewmodel/viewmodel_property_string.json
@@ -0,0 +1,8 @@
+{
+  "name": "ViewModelPropertyString",
+  "key": {
+    "int": 443,
+    "string": "viewmodelpropertystring"
+  },
+  "extends": "viewmodel/viewmodel_property.json"
+}
\ No newline at end of file
diff --git a/dev/defs/viewmodel/viewmodel_property_viewmodel.json b/dev/defs/viewmodel/viewmodel_property_viewmodel.json
new file mode 100644
index 0000000..10d615a
--- /dev/null
+++ b/dev/defs/viewmodel/viewmodel_property_viewmodel.json
@@ -0,0 +1,21 @@
+{
+  "name": "ViewModelPropertyViewModel",
+  "key": {
+    "int": 436,
+    "string": "viewmodelpropertyviewmodel"
+  },
+  "extends": "viewmodel/viewmodel_property.json",
+  "properties": {
+    "viewModelReferenceId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "0",
+      "key": {
+        "int": 565,
+        "string": "viewmodelreferenceid"
+      },
+      "description": "Identifier used to track the viewmodel this property points to."
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/defs/world_transform_component.json b/dev/defs/world_transform_component.json
new file mode 100644
index 0000000..249706b
--- /dev/null
+++ b/dev/defs/world_transform_component.json
@@ -0,0 +1,20 @@
+{
+  "name": "WorldTransformComponent",
+  "key": {
+    "int": 91,
+    "string": "worldtransformcomponent"
+  },
+  "abstract": true,
+  "extends": "container_component.json",
+  "properties": {
+    "opacity": {
+      "type": "double",
+      "initialValue": "1",
+      "animates": true,
+      "key": {
+        "int": 18,
+        "string": "opacity"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/dev/generate_core.sh b/dev/generate_core.sh
new file mode 100755
index 0000000..006def7
--- /dev/null
+++ b/dev/generate_core.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -e
+
+if [[ ! -f "./bin/core_generator" || "$1" == "build" ]]; then
+    mkdir -p ./bin
+    dart compile exe ./core_generator/lib/main.dart -o ./bin/core_generator
+fi
+./bin/core_generator
\ No newline at end of file
diff --git a/dev/setup_premake.sh b/dev/setup_premake.sh
new file mode 100644
index 0000000..220c650
--- /dev/null
+++ b/dev/setup_premake.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+
+unameOut="$(uname -s)"
+case "${unameOut}" in
+Linux*) MACHINE=linux ;;
+Darwin*) MACHINE=mac ;;
+CYGWIN*) MACHINE=cygwin ;;
+MINGW*) MACHINE=mingw ;;
+*) MACHINE="UNKNOWN:${unameOut}" ;;
+esac
+
+# check if use has already installed premake5
+if ! command -v premake5 &>/dev/null; then
+    # no premake found in path
+    if [[ ! -f "bin/premake5" ]]; then
+        mkdir -p bin
+        pushd bin
+        echo Downloading Premake5
+        if [ "$MACHINE" = 'mac' ]; then
+            PREMAKE_URL=https://github.com/premake/premake-core/releases/download/v5.0.0-beta2/premake-5.0.0-beta2-macosx.tar.gz
+        else
+            PREMAKE_URL=https://github.com/premake/premake-core/releases/download/v5.0.0-beta2/premake-5.0.0-beta2-linux.tar.gz
+        fi
+        curl $PREMAKE_URL -L -o premake.tar.gz
+        # Export premake5 into bin
+        tar -xvf premake.tar.gz 2>/dev/null
+        # Delete downloaded archive
+        rm premake.tar.gz
+        popd
+    fi
+    export PREMAKE=$PWD/bin/premake5
+else
+    export PREMAKE=premake5
+fi
diff --git a/dev/test.bat b/dev/test.bat
new file mode 100644
index 0000000..cbd3ca3
--- /dev/null
+++ b/dev/test.bat
@@ -0,0 +1,21 @@
+@echo off
+call ..\dependencies\windows\config_directories.bat
+
+if not exist "%DEPENDENCIES%\bin\premake5.exe" (
+    pushd "%DEPENDENCIES_SCRIPTS%"
+    call .\get_premake5.bat || goto :error
+    popd
+)
+
+set "PREMAKE=%DEPENDENCIES%\bin\premake5.exe"
+pushd test
+%PREMAKE% --scripts=..\..\build --no-download-progress --with_rive_tools --with_rive_text --with_rive_audio=external vs2022
+
+MSBuild.exe /?  2> NUL
+if not %ERRORLEVEL%==9009 (
+    set "MSBuild=MSBuild.exe"
+) else (
+    set "MSBuild=%ProgramFiles%\Microsoft Visual Studio\2022\Community\Msbuild\Current\Bin\MSBuild.exe"
+)
+call "%MSBuild%" out\debug\rive.sln
+out\debug\tests.exe
\ No newline at end of file
diff --git a/dev/test.sh b/dev/test.sh
new file mode 100755
index 0000000..213e09a
--- /dev/null
+++ b/dev/test.sh
@@ -0,0 +1,106 @@
+#!/bin/bash
+set -e
+
+unameOut="$(uname -s)"
+case "${unameOut}" in
+Linux*) machine=linux ;;
+Darwin*) machine=macosx ;;
+MINGW*) machine=windows ;;
+*) machine="unhandled:${unameOut}" ;;
+esac
+
+CONFIG=debug
+
+for var in "$@"; do
+  if [[ $var = "release" ]]; then
+    CONFIG=release
+  elif [ "$var" = "memory" ]; then
+    echo Will perform memory checks...
+    UTILITY='leaks --atExit --'
+    shift
+  elif [ "$var" = "lldb" ]; then
+    echo Starting debugger...
+    UTILITY='lldb'
+    shift
+  fi
+done
+
+if [[ ! -f "dependencies/bin/premake5" ]]; then
+  mkdir -p dependencies/bin
+  pushd dependencies
+  if [[ $machine = "macosx" ]]; then
+    # v5.0.0-beta2 doesn't support apple silicon properly, update the branch
+    # once a stable one is avaialble that supports it
+    git clone --depth 1 --branch master https://github.com/premake/premake-core.git
+    pushd premake-core
+    if [[ $LOCAL_ARCH == "arm64" ]]; then
+      PREMAKE_MAKE_ARCH=ARM
+    else
+      PREMAKE_MAKE_ARCH=x86
+    fi
+    make -f Bootstrap.mak osx PLATFORM=$PREMAKE_MAKE_ARCH
+    cp bin/release/* ../bin
+    popd
+  elif [[ $machine = "windows" ]]; then
+    pushd bin
+    curl https://github.com/premake/premake-core/releases/download/v5.0.0-beta2/premake-5.0.0-beta2-windows.zip -L -o premake_windows.zip
+    unzip premake_windows.zip
+    rm premake_windows.zip
+    popd
+  fi
+  popd
+fi
+
+export PREMAKE=$PWD/dependencies/bin/premake5
+
+pushd test
+
+for var in "$@"; do
+  if [[ $var = "clean" ]]; then
+    echo 'Cleaning...'
+    rm -fR out
+  fi
+done
+
+mkdir -p out
+
+if [[ $machine = "macosx" ]]; then
+  TARGET=gmake2
+elif [[ $machine = "windows" ]]; then
+  TARGET=vs2022
+fi
+
+pushd ../../
+RUNTIME=$PWD
+popd
+
+export PREMAKE_PATH="$RUNTIME/dependencies/export-compile-commands":"$RUNTIME/build":"$PREMAKE_PATH"
+PREMAKE_COMMANDS="--with_rive_text --with_rive_audio=external --with_rive_layout --config=$CONFIG"
+
+out_dir() {
+  echo "out/$CONFIG"
+}
+if [[ $machine = "macosx" ]]; then
+  OUT_DIR="$(out_dir)"
+  $PREMAKE $TARGET $PREMAKE_COMMANDS --out=$OUT_DIR
+  pushd $OUT_DIR
+  make -j$(($(sysctl -n hw.physicalcpu) + 1))
+  popd
+  $UTILITY $OUT_DIR/tests
+elif [[ $machine = "windows" ]]; then
+  if [[ -f "$PROGRAMFILES/Microsoft Visual Studio/2022/Enterprise/Msbuild/Current/Bin/MSBuild.exe" ]]; then
+    export MSBUILD="$PROGRAMFILES/Microsoft Visual Studio/2022/Enterprise/Msbuild/Current/Bin/MSBuild.exe"
+  elif [[ -f "$PROGRAMFILES/Microsoft Visual Studio/2022/Community/Msbuild/Current/Bin/MSBuild.exe" ]]; then
+    export MSBUILD="$PROGRAMFILES/Microsoft Visual Studio/2022/Community/Msbuild/Current/Bin/MSBuild.exe"
+  fi
+  OUT_DIR="$(out_dir)"
+  echo $PREMAKE $TARGET $PREMAKE_COMMANDS --out=$OUT_DIR
+  ls -l
+  $PREMAKE $TARGET $PREMAKE_COMMANDS --out=$OUT_DIR
+  pushd $OUT_DIR
+  "$MSBUILD" rive.sln -m:$NUMBER_OF_PROCESSORS
+  popd
+  $OUT_DIR/tests.exe
+fi
+
+popd
diff --git a/dev/test/include/catch.hpp b/dev/test/include/catch.hpp
new file mode 100644
index 0000000..348ab9d
--- /dev/null
+++ b/dev/test/include/catch.hpp
@@ -0,0 +1,18443 @@
+// clang-format off
+/*
+ *  Catch v2.13.7
+ *  Generated: 2021-07-28 20:29:27.753164
+ *  ----------------------------------------------------------
+ *  This file has been merged from multiple headers. Please don't edit it
+ * directly Copyright (c) 2021 Two Blue Cubes Ltd. All rights reserved.
+ *
+ *  Distributed under the Boost Software License, Version 1.0. (See accompanying
+ *  file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+ */
+#ifndef TWOBLUECUBES_SINGLE_INCLUDE_CATCH_HPP_INCLUDED
+#define TWOBLUECUBES_SINGLE_INCLUDE_CATCH_HPP_INCLUDED
+// start catch.hpp
+
+#define CATCH_VERSION_MAJOR 2
+#define CATCH_VERSION_MINOR 13
+#define CATCH_VERSION_PATCH 7
+
+#ifdef __clang__
+#pragma clang system_header
+#elif defined __GNUC__
+#pragma GCC system_header
+#endif
+
+// start catch_suppress_warnings.h
+
+#ifdef __clang__
+#ifdef __ICC // icpc defines the __clang__ macro
+#pragma warning(push)
+#pragma warning(disable : 161 1682)
+#else // __ICC
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wpadded"
+#pragma clang diagnostic ignored "-Wswitch-enum"
+#pragma clang diagnostic ignored "-Wcovered-switch-default"
+#endif
+#elif defined __GNUC__
+// Because REQUIREs trigger GCC's -Wparentheses, and because still
+// supported version of g++ have only buggy support for _Pragmas,
+// Wparentheses have to be suppressed globally.
+#pragma GCC diagnostic ignored "-Wparentheses" // See #674 for details
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-variable"
+#pragma GCC diagnostic ignored "-Wpadded"
+#endif
+// end catch_suppress_warnings.h
+#if defined(CATCH_CONFIG_MAIN) || defined(CATCH_CONFIG_RUNNER)
+#define CATCH_IMPL
+#define CATCH_CONFIG_ALL_PARTS
+#endif
+
+// In the impl file, we want to have access to all parts of the headers
+// Can also be used to sanely support PCHs
+#if defined(CATCH_CONFIG_ALL_PARTS)
+#define CATCH_CONFIG_EXTERNAL_INTERFACES
+#if defined(CATCH_CONFIG_DISABLE_MATCHERS)
+#undef CATCH_CONFIG_DISABLE_MATCHERS
+#endif
+#if !defined(CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER)
+#define CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER
+#endif
+#endif
+
+#if !defined(CATCH_CONFIG_IMPL_ONLY)
+// start catch_platform.h
+
+// See e.g.:
+// https://opensource.apple.com/source/CarbonHeaders/CarbonHeaders-18.1/TargetConditionals.h.auto.html
+#ifdef __APPLE__
+#include <TargetConditionals.h>
+#if (defined(TARGET_OS_OSX) && TARGET_OS_OSX == 1) || (defined(TARGET_OS_MAC) && TARGET_OS_MAC == 1)
+#define CATCH_PLATFORM_MAC
+#elif (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE == 1)
+#define CATCH_PLATFORM_IPHONE
+#endif
+
+#elif defined(linux) || defined(__linux) || defined(__linux__)
+#define CATCH_PLATFORM_LINUX
+
+#elif defined(WIN32) || defined(__WIN32__) || defined(_WIN32) || defined(_MSC_VER) ||              \
+    defined(__MINGW32__)
+#define CATCH_PLATFORM_WINDOWS
+#endif
+
+// end catch_platform.h
+
+#ifdef CATCH_IMPL
+#ifndef CLARA_CONFIG_MAIN
+#define CLARA_CONFIG_MAIN_NOT_DEFINED
+#define CLARA_CONFIG_MAIN
+#endif
+#endif
+
+// start catch_user_interfaces.h
+
+namespace Catch {
+    unsigned int rngSeed();
+}
+
+// end catch_user_interfaces.h
+// start catch_tag_alias_autoregistrar.h
+
+// start catch_common.h
+
+// start catch_compiler_capabilities.h
+
+// Detect a number of compiler features - by compiler
+// The following features are defined:
+//
+// CATCH_CONFIG_COUNTER : is the __COUNTER__ macro supported?
+// CATCH_CONFIG_WINDOWS_SEH : is Windows SEH supported?
+// CATCH_CONFIG_POSIX_SIGNALS : are POSIX signals supported?
+// CATCH_CONFIG_DISABLE_EXCEPTIONS : Are exceptions enabled?
+// ****************
+// Note to maintainers: if new toggles are added please document them
+// in configuration.md, too
+// ****************
+
+// In general each macro has a _NO_<feature name> form
+// (e.g. CATCH_CONFIG_NO_POSIX_SIGNALS) which disables the feature.
+// Many features, at point of detection, define an _INTERNAL_ macro, so they
+// can be combined, en-mass, with the _NO_ forms later.
+
+#ifdef __cplusplus
+
+#if (__cplusplus >= 201402L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 201402L)
+#define CATCH_CPP14_OR_GREATER
+#endif
+
+#if (__cplusplus >= 201703L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)
+#define CATCH_CPP17_OR_GREATER
+#endif
+
+#endif
+
+// Only GCC compiler should be used in this block, so other compilers trying to
+// mask themselves as GCC should be ignored.
+#if defined(__GNUC__) && !defined(__clang__) && !defined(__ICC) && !defined(__CUDACC__) &&         \
+    !defined(__LCC__)
+#define CATCH_INTERNAL_START_WARNINGS_SUPPRESSION _Pragma("GCC diagnostic push")
+#define CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION _Pragma("GCC diagnostic pop")
+
+#define CATCH_INTERNAL_IGNORE_BUT_WARN(...) (void)__builtin_constant_p(__VA_ARGS__)
+
+#endif
+
+#if defined(__clang__)
+
+#define CATCH_INTERNAL_START_WARNINGS_SUPPRESSION _Pragma("clang diagnostic push")
+#define CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION _Pragma("clang diagnostic pop")
+
+// As of this writing, IBM XL's implementation of __builtin_constant_p has a bug
+// which results in calls to destructors being emitted for each temporary,
+// without a matching initialization. In practice, this can result in something
+// like `std::string::~string` being called on an uninitialized value.
+//
+// For example, this code will likely segfault under IBM XL:
+// ```
+// REQUIRE(std::string("12") + "34" == "1234")
+// ```
+//
+// Therefore, `CATCH_INTERNAL_IGNORE_BUT_WARN` is not implemented.
+#if !defined(__ibmxl__) && !defined(__CUDACC__)
+#define CATCH_INTERNAL_IGNORE_BUT_WARN(...)                                                        \
+    (void)__builtin_constant_p(__VA_ARGS__) /* NOLINT(cppcoreguidelines-pro-type-vararg,           \
+                                               hicpp-vararg) */
+#endif
+
+#define CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS                                                   \
+    _Pragma("clang diagnostic ignored \"-Wexit-time-destructors\"")                                \
+        _Pragma("clang diagnostic ignored \"-Wglobal-constructors\"")
+
+#define CATCH_INTERNAL_SUPPRESS_PARENTHESES_WARNINGS                                               \
+    _Pragma("clang diagnostic ignored \"-Wparentheses\"")
+
+#define CATCH_INTERNAL_SUPPRESS_UNUSED_WARNINGS                                                    \
+    _Pragma("clang diagnostic ignored \"-Wunused-variable\"")
+
+#define CATCH_INTERNAL_SUPPRESS_ZERO_VARIADIC_WARNINGS                                             \
+    _Pragma("clang diagnostic ignored "                                                            \
+            "\"-Wgnu-zero-variadic-macro-arguments\"")
+
+#define CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS                                           \
+    _Pragma("clang diagnostic ignored \"-Wunused-template\"")
+
+#endif // __clang__
+
+////////////////////////////////////////////////////////////////////////////////
+// Assume that non-Windows platforms support posix signals by default
+#if !defined(CATCH_PLATFORM_WINDOWS)
+#define CATCH_INTERNAL_CONFIG_POSIX_SIGNALS
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+// We know some environments not to support full POSIX signals
+#if defined(__CYGWIN__) || defined(__QNX__) || defined(__EMSCRIPTEN__) || defined(__DJGPP__)
+#define CATCH_INTERNAL_CONFIG_NO_POSIX_SIGNALS
+#endif
+
+#ifdef __OS400__
+#define CATCH_INTERNAL_CONFIG_NO_POSIX_SIGNALS
+#define CATCH_CONFIG_COLOUR_NONE
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+// Android somehow still does not support std::to_string
+#if defined(__ANDROID__)
+#define CATCH_INTERNAL_CONFIG_NO_CPP11_TO_STRING
+#define CATCH_INTERNAL_CONFIG_ANDROID_LOGWRITE
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+// Not all Windows environments support SEH properly
+#if defined(__MINGW32__)
+#define CATCH_INTERNAL_CONFIG_NO_WINDOWS_SEH
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+// PS4
+#if defined(__ORBIS__)
+#define CATCH_INTERNAL_CONFIG_NO_NEW_CAPTURE
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+// Cygwin
+#ifdef __CYGWIN__
+
+// Required for some versions of Cygwin to declare gettimeofday
+// see:
+// http://stackoverflow.com/questions/36901803/gettimeofday-not-declared-in-this-scope-cygwin
+#define _BSD_SOURCE
+// some versions of cygwin (most) do not support std::to_string. Use the libstd
+// check.
+// https://gcc.gnu.org/onlinedocs/gcc-4.8.2/libstdc++/api/a01053_source.html
+// line 2812-2813
+#if !((__cplusplus >= 201103L) && defined(_GLIBCXX_USE_C99) &&                                     \
+      !defined(_GLIBCXX_HAVE_BROKEN_VSWPRINTF))
+
+#define CATCH_INTERNAL_CONFIG_NO_CPP11_TO_STRING
+
+#endif
+#endif // __CYGWIN__
+
+////////////////////////////////////////////////////////////////////////////////
+// Visual C++
+#if defined(_MSC_VER)
+
+#define CATCH_INTERNAL_START_WARNINGS_SUPPRESSION __pragma(warning(push))
+#define CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION __pragma(warning(pop))
+
+// Universal Windows platform does not support SEH
+// Or console colours (or console at all...)
+#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP)
+#define CATCH_CONFIG_COLOUR_NONE
+#else
+#define CATCH_INTERNAL_CONFIG_WINDOWS_SEH
+#endif
+
+// MSVC traditional preprocessor needs some workaround for __VA_ARGS__
+// _MSVC_TRADITIONAL == 0 means new conformant preprocessor
+// _MSVC_TRADITIONAL == 1 means old traditional non-conformant preprocessor
+#if !defined(__clang__) // Handle Clang masquerading for msvc
+
+// Rive disabled warning based on discussion
+// https://2dimensions.slack.com/archives/CLLCU09T6/p1699024342752529
+#pragma warning( disable : 5267 )
+
+#if !defined(_MSVC_TRADITIONAL) || (defined(_MSVC_TRADITIONAL) && _MSVC_TRADITIONAL)
+#define CATCH_INTERNAL_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#endif // MSVC_TRADITIONAL
+#endif // __clang__
+
+#endif // _MSC_VER
+
+#if defined(_REENTRANT) || defined(_MSC_VER)
+// Enable async processing, as -pthread is specified or no additional linking is
+// required
+#define CATCH_INTERNAL_CONFIG_USE_ASYNC
+#endif // _MSC_VER
+
+////////////////////////////////////////////////////////////////////////////////
+// Check if we are compiled with -fno-exceptions or equivalent
+#if defined(__EXCEPTIONS) || defined(__cpp_exceptions) || defined(_CPPUNWIND)
+#define CATCH_INTERNAL_CONFIG_EXCEPTIONS_ENABLED
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+// DJGPP
+#ifdef __DJGPP__
+#define CATCH_INTERNAL_CONFIG_NO_WCHAR
+#endif // __DJGPP__
+
+////////////////////////////////////////////////////////////////////////////////
+// Embarcadero C++Build
+#if defined(__BORLANDC__)
+#define CATCH_INTERNAL_CONFIG_POLYFILL_ISNAN
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Use of __COUNTER__ is suppressed during code analysis in
+// CLion/AppCode 2017.2.x and former, because __COUNTER__ is not properly
+// handled by it.
+// Otherwise all supported compilers support COUNTER macro,
+// but user still might want to turn it off
+#if (!defined(__JETBRAINS_IDE__) || __JETBRAINS_IDE__ >= 20170300L)
+#define CATCH_INTERNAL_CONFIG_COUNTER
+#endif
+
+////////////////////////////////////////////////////////////////////////////////
+
+// RTX is a special version of Windows that is real time.
+// This means that it is detected as Windows, but does not provide
+// the same set of capabilities as real Windows does.
+#if defined(UNDER_RTSS) || defined(RTX64_BUILD)
+#define CATCH_INTERNAL_CONFIG_NO_WINDOWS_SEH
+#define CATCH_INTERNAL_CONFIG_NO_ASYNC
+#define CATCH_CONFIG_COLOUR_NONE
+#endif
+
+#if !defined(_GLIBCXX_USE_C99_MATH_TR1)
+#define CATCH_INTERNAL_CONFIG_GLOBAL_NEXTAFTER
+#endif
+
+// Various stdlib support checks that require __has_include
+#if defined(__has_include)
+// Check if string_view is available and usable
+#if __has_include(<string_view>) && defined(CATCH_CPP17_OR_GREATER)
+#define CATCH_INTERNAL_CONFIG_CPP17_STRING_VIEW
+#endif
+
+// Check if optional is available and usable
+#if __has_include(<optional>) && defined(CATCH_CPP17_OR_GREATER)
+#define CATCH_INTERNAL_CONFIG_CPP17_OPTIONAL
+#endif // __has_include(<optional>) && defined(CATCH_CPP17_OR_GREATER)
+
+// Check if byte is available and usable
+#if __has_include(<cstddef>) && defined(CATCH_CPP17_OR_GREATER)
+#include <cstddef>
+#if defined(__cpp_lib_byte) && (__cpp_lib_byte > 0)
+#define CATCH_INTERNAL_CONFIG_CPP17_BYTE
+#endif
+#endif // __has_include(<cstddef>) && defined(CATCH_CPP17_OR_GREATER)
+
+// Check if variant is available and usable
+#if __has_include(<variant>) && defined(CATCH_CPP17_OR_GREATER)
+#if defined(__clang__) && (__clang_major__ < 8)
+// work around clang bug with libstdc++
+// https://bugs.llvm.org/show_bug.cgi?id=31852 fix should be in clang 8,
+// workaround in libstdc++ 8.2
+#include <ciso646>
+#if defined(__GLIBCXX__) && defined(_GLIBCXX_RELEASE) && (_GLIBCXX_RELEASE < 9)
+#define CATCH_CONFIG_NO_CPP17_VARIANT
+#else
+#define CATCH_INTERNAL_CONFIG_CPP17_VARIANT
+#endif // defined(__GLIBCXX__) && defined(_GLIBCXX_RELEASE) && (_GLIBCXX_RELEASE
+       // < 9)
+#else
+#define CATCH_INTERNAL_CONFIG_CPP17_VARIANT
+#endif // defined(__clang__) && (__clang_major__ < 8)
+#endif // __has_include(<variant>) && defined(CATCH_CPP17_OR_GREATER)
+#endif // defined(__has_include)
+
+#if defined(CATCH_INTERNAL_CONFIG_COUNTER) && !defined(CATCH_CONFIG_NO_COUNTER) &&                 \
+    !defined(CATCH_CONFIG_COUNTER)
+#define CATCH_CONFIG_COUNTER
+#endif
+#if defined(CATCH_INTERNAL_CONFIG_WINDOWS_SEH) && !defined(CATCH_CONFIG_NO_WINDOWS_SEH) &&         \
+    !defined(CATCH_CONFIG_WINDOWS_SEH) && !defined(CATCH_INTERNAL_CONFIG_NO_WINDOWS_SEH)
+#define CATCH_CONFIG_WINDOWS_SEH
+#endif
+// This is set by default, because we assume that unix compilers are
+// posix-signal-compatible by default.
+#if defined(CATCH_INTERNAL_CONFIG_POSIX_SIGNALS) &&                                                \
+    !defined(CATCH_INTERNAL_CONFIG_NO_POSIX_SIGNALS) && !defined(CATCH_CONFIG_NO_POSIX_SIGNALS) && \
+    !defined(CATCH_CONFIG_POSIX_SIGNALS)
+#define CATCH_CONFIG_POSIX_SIGNALS
+#endif
+// This is set by default, because we assume that compilers with no wchar_t
+// support are just rare exceptions.
+#if !defined(CATCH_INTERNAL_CONFIG_NO_WCHAR) && !defined(CATCH_CONFIG_NO_WCHAR) &&                 \
+    !defined(CATCH_CONFIG_WCHAR)
+#define CATCH_CONFIG_WCHAR
+#endif
+
+#if !defined(CATCH_INTERNAL_CONFIG_NO_CPP11_TO_STRING) &&                                          \
+    !defined(CATCH_CONFIG_NO_CPP11_TO_STRING) && !defined(CATCH_CONFIG_CPP11_TO_STRING)
+#define CATCH_CONFIG_CPP11_TO_STRING
+#endif
+
+#if defined(CATCH_INTERNAL_CONFIG_CPP17_OPTIONAL) && !defined(CATCH_CONFIG_NO_CPP17_OPTIONAL) &&   \
+    !defined(CATCH_CONFIG_CPP17_OPTIONAL)
+#define CATCH_CONFIG_CPP17_OPTIONAL
+#endif
+
+#if defined(CATCH_INTERNAL_CONFIG_CPP17_STRING_VIEW) &&                                            \
+    !defined(CATCH_CONFIG_NO_CPP17_STRING_VIEW) && !defined(CATCH_CONFIG_CPP17_STRING_VIEW)
+#define CATCH_CONFIG_CPP17_STRING_VIEW
+#endif
+
+#if defined(CATCH_INTERNAL_CONFIG_CPP17_VARIANT) && !defined(CATCH_CONFIG_NO_CPP17_VARIANT) &&     \
+    !defined(CATCH_CONFIG_CPP17_VARIANT)
+#define CATCH_CONFIG_CPP17_VARIANT
+#endif
+
+#if defined(CATCH_INTERNAL_CONFIG_CPP17_BYTE) && !defined(CATCH_CONFIG_NO_CPP17_BYTE) &&           \
+    !defined(CATCH_CONFIG_CPP17_BYTE)
+#define CATCH_CONFIG_CPP17_BYTE
+#endif
+
+#if defined(CATCH_CONFIG_EXPERIMENTAL_REDIRECT)
+#define CATCH_INTERNAL_CONFIG_NEW_CAPTURE
+#endif
+
+#if defined(CATCH_INTERNAL_CONFIG_NEW_CAPTURE) &&                                                  \
+    !defined(CATCH_INTERNAL_CONFIG_NO_NEW_CAPTURE) && !defined(CATCH_CONFIG_NO_NEW_CAPTURE) &&     \
+    !defined(CATCH_CONFIG_NEW_CAPTURE)
+#define CATCH_CONFIG_NEW_CAPTURE
+#endif
+
+#if !defined(CATCH_INTERNAL_CONFIG_EXCEPTIONS_ENABLED) && !defined(CATCH_CONFIG_DISABLE_EXCEPTIONS)
+#define CATCH_CONFIG_DISABLE_EXCEPTIONS
+#endif
+
+#if defined(CATCH_INTERNAL_CONFIG_POLYFILL_ISNAN) && !defined(CATCH_CONFIG_NO_POLYFILL_ISNAN) &&   \
+    !defined(CATCH_CONFIG_POLYFILL_ISNAN)
+#define CATCH_CONFIG_POLYFILL_ISNAN
+#endif
+
+#if defined(CATCH_INTERNAL_CONFIG_USE_ASYNC) && !defined(CATCH_INTERNAL_CONFIG_NO_ASYNC) &&        \
+    !defined(CATCH_CONFIG_NO_USE_ASYNC) && !defined(CATCH_CONFIG_USE_ASYNC)
+#define CATCH_CONFIG_USE_ASYNC
+#endif
+
+#if defined(CATCH_INTERNAL_CONFIG_ANDROID_LOGWRITE) &&                                             \
+    !defined(CATCH_CONFIG_NO_ANDROID_LOGWRITE) && !defined(CATCH_CONFIG_ANDROID_LOGWRITE)
+#define CATCH_CONFIG_ANDROID_LOGWRITE
+#endif
+
+#if defined(CATCH_INTERNAL_CONFIG_GLOBAL_NEXTAFTER) &&                                             \
+    !defined(CATCH_CONFIG_NO_GLOBAL_NEXTAFTER) && !defined(CATCH_CONFIG_GLOBAL_NEXTAFTER)
+#define CATCH_CONFIG_GLOBAL_NEXTAFTER
+#endif
+
+// Even if we do not think the compiler has that warning, we still have
+// to provide a macro that can be used by the code.
+#if !defined(CATCH_INTERNAL_START_WARNINGS_SUPPRESSION)
+#define CATCH_INTERNAL_START_WARNINGS_SUPPRESSION
+#endif
+#if !defined(CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION)
+#define CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION
+#endif
+#if !defined(CATCH_INTERNAL_SUPPRESS_PARENTHESES_WARNINGS)
+#define CATCH_INTERNAL_SUPPRESS_PARENTHESES_WARNINGS
+#endif
+#if !defined(CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS)
+#define CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS
+#endif
+#if !defined(CATCH_INTERNAL_SUPPRESS_UNUSED_WARNINGS)
+#define CATCH_INTERNAL_SUPPRESS_UNUSED_WARNINGS
+#endif
+#if !defined(CATCH_INTERNAL_SUPPRESS_ZERO_VARIADIC_WARNINGS)
+#define CATCH_INTERNAL_SUPPRESS_ZERO_VARIADIC_WARNINGS
+#endif
+
+// The goal of this macro is to avoid evaluation of the arguments, but
+// still have the compiler warn on problems inside...
+#if !defined(CATCH_INTERNAL_IGNORE_BUT_WARN)
+#define CATCH_INTERNAL_IGNORE_BUT_WARN(...)
+#endif
+
+#if defined(__APPLE__) && defined(__apple_build_version__) && (__clang_major__ < 10)
+#undef CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS
+#elif defined(__clang__) && (__clang_major__ < 5)
+#undef CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS
+#endif
+
+#if !defined(CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS)
+#define CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS
+#endif
+
+#if defined(CATCH_CONFIG_DISABLE_EXCEPTIONS)
+#define CATCH_TRY if ((true))
+#define CATCH_CATCH_ALL if ((false))
+#define CATCH_CATCH_ANON(type) if ((false))
+#else
+#define CATCH_TRY try
+#define CATCH_CATCH_ALL catch (...)
+#define CATCH_CATCH_ANON(type) catch (type)
+#endif
+
+#if defined(CATCH_INTERNAL_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR) &&                                \
+    !defined(CATCH_CONFIG_NO_TRADITIONAL_MSVC_PREPROCESSOR) &&                                     \
+    !defined(CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR)
+#define CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#endif
+
+// end catch_compiler_capabilities.h
+#define INTERNAL_CATCH_UNIQUE_NAME_LINE2(name, line) name##line
+#define INTERNAL_CATCH_UNIQUE_NAME_LINE(name, line) INTERNAL_CATCH_UNIQUE_NAME_LINE2(name, line)
+#ifdef CATCH_CONFIG_COUNTER
+#define INTERNAL_CATCH_UNIQUE_NAME(name) INTERNAL_CATCH_UNIQUE_NAME_LINE(name, __COUNTER__)
+#else
+#define INTERNAL_CATCH_UNIQUE_NAME(name) INTERNAL_CATCH_UNIQUE_NAME_LINE(name, __LINE__)
+#endif
+
+#include <iosfwd>
+#include <string>
+#include <cstdint>
+
+// We need a dummy global operator<< so we can bring it into Catch namespace
+// later
+struct Catch_global_namespace_dummy {};
+std::ostream& operator<<(std::ostream&, Catch_global_namespace_dummy);
+
+namespace Catch {
+
+    struct CaseSensitive {
+        enum Choice { Yes, No };
+    };
+
+    class NonCopyable {
+        NonCopyable(NonCopyable const&) = delete;
+        NonCopyable(NonCopyable&&) = delete;
+        NonCopyable& operator=(NonCopyable const&) = delete;
+        NonCopyable& operator=(NonCopyable&&) = delete;
+
+    protected:
+        NonCopyable();
+        virtual ~NonCopyable();
+    };
+
+    struct SourceLineInfo {
+
+        SourceLineInfo() = delete;
+        SourceLineInfo(char const* _file, std::size_t _line) noexcept : file(_file), line(_line) {}
+
+        SourceLineInfo(SourceLineInfo const& other) = default;
+        SourceLineInfo& operator=(SourceLineInfo const&) = default;
+        SourceLineInfo(SourceLineInfo&&) noexcept = default;
+        SourceLineInfo& operator=(SourceLineInfo&&) noexcept = default;
+
+        bool empty() const noexcept { return file[0] == '\0'; }
+        bool operator==(SourceLineInfo const& other) const noexcept;
+        bool operator<(SourceLineInfo const& other) const noexcept;
+
+        char const* file;
+        std::size_t line;
+    };
+
+    std::ostream& operator<<(std::ostream& os, SourceLineInfo const& info);
+
+    // Bring in operator<< from global namespace into Catch namespace
+    // This is necessary because the overload of operator<< above makes
+    // lookup stop at namespace Catch
+    using ::operator<<;
+
+    // Use this in variadic streaming macros to allow
+    //    >> +StreamEndStop
+    // as well as
+    //    >> stuff +StreamEndStop
+    struct StreamEndStop {
+        std::string operator+() const;
+    };
+    template <typename T> T const& operator+(T const& value, StreamEndStop) { return value; }
+} // namespace Catch
+
+#define CATCH_INTERNAL_LINEINFO                                                                    \
+    ::Catch::SourceLineInfo(__FILE__, static_cast<std::size_t>(__LINE__))
+
+// end catch_common.h
+namespace Catch {
+
+    struct RegistrarForTagAliases {
+        RegistrarForTagAliases(char const* alias, char const* tag, SourceLineInfo const& lineInfo);
+    };
+
+} // end namespace Catch
+
+#define CATCH_REGISTER_TAG_ALIAS(alias, spec)                                                      \
+    CATCH_INTERNAL_START_WARNINGS_SUPPRESSION                                                      \
+    CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS                                                       \
+    namespace {                                                                                    \
+        Catch::RegistrarForTagAliases INTERNAL_CATCH_UNIQUE_NAME(AutoRegisterTagAlias)(            \
+            alias, spec, CATCH_INTERNAL_LINEINFO);                                                 \
+    }                                                                                              \
+    CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION
+
+// end catch_tag_alias_autoregistrar.h
+// start catch_test_registry.h
+
+// start catch_interfaces_testcase.h
+
+#include <vector>
+
+namespace Catch {
+
+    class TestSpec;
+
+    struct ITestInvoker {
+        virtual void invoke() const = 0;
+        virtual ~ITestInvoker();
+    };
+
+    class TestCase;
+    struct IConfig;
+
+    struct ITestCaseRegistry {
+        virtual ~ITestCaseRegistry();
+        virtual std::vector<TestCase> const& getAllTests() const = 0;
+        virtual std::vector<TestCase> const& getAllTestsSorted(IConfig const& config) const = 0;
+    };
+
+    bool isThrowSafe(TestCase const& testCase, IConfig const& config);
+    bool matchTest(TestCase const& testCase, TestSpec const& testSpec, IConfig const& config);
+    std::vector<TestCase> filterTests(std::vector<TestCase> const& testCases,
+                                      TestSpec const& testSpec,
+                                      IConfig const& config);
+    std::vector<TestCase> const& getAllTestCasesSorted(IConfig const& config);
+
+} // namespace Catch
+
+// end catch_interfaces_testcase.h
+// start catch_stringref.h
+
+#include <cstddef>
+#include <string>
+#include <iosfwd>
+#include <cassert>
+
+namespace Catch {
+
+    /// A non-owning string class (similar to the forthcoming std::string_view)
+    /// Note that, because a StringRef may be a substring of another string,
+    /// it may not be null terminated.
+    class StringRef {
+    public:
+        using size_type = std::size_t;
+        using const_iterator = const char*;
+
+    private:
+        static constexpr char const* const s_empty = "";
+
+        char const* m_start = s_empty;
+        size_type m_size = 0;
+
+    public: // construction
+        constexpr StringRef() noexcept = default;
+
+        StringRef(char const* rawChars) noexcept;
+
+        constexpr StringRef(char const* rawChars, size_type size) noexcept :
+            m_start(rawChars), m_size(size) {}
+
+        StringRef(std::string const& stdString) noexcept :
+            m_start(stdString.c_str()), m_size(stdString.size()) {}
+
+        explicit operator std::string() const { return std::string(m_start, m_size); }
+
+    public: // operators
+        auto operator==(StringRef const& other) const noexcept -> bool;
+        auto operator!=(StringRef const& other) const noexcept -> bool { return !(*this == other); }
+
+        auto operator[](size_type index) const noexcept -> char {
+            assert(index < m_size);
+            return m_start[index];
+        }
+
+    public: // named queries
+        constexpr auto empty() const noexcept -> bool { return m_size == 0; }
+        constexpr auto size() const noexcept -> size_type { return m_size; }
+
+        // Returns the current start pointer. If the StringRef is not
+        // null-terminated, throws std::domain_exception
+        auto c_str() const -> char const*;
+
+    public: // substrings and searches
+        // Returns a substring of [start, start + length).
+        // If start + length > size(), then the substring is [start, size()).
+        // If start > size(), then the substring is empty.
+        auto substr(size_type start, size_type length) const noexcept -> StringRef;
+
+        // Returns the current start pointer. May not be null-terminated.
+        auto data() const noexcept -> char const*;
+
+        constexpr auto isNullTerminated() const noexcept -> bool { return m_start[m_size] == '\0'; }
+
+    public: // iterators
+        constexpr const_iterator begin() const { return m_start; }
+        constexpr const_iterator end() const { return m_start + m_size; }
+    };
+
+    auto operator+=(std::string& lhs, StringRef const& sr) -> std::string&;
+    auto operator<<(std::ostream& os, StringRef const& sr) -> std::ostream&;
+
+    constexpr auto operator"" _sr(char const* rawChars, std::size_t size) noexcept -> StringRef {
+        return StringRef(rawChars, size);
+    }
+} // namespace Catch
+
+constexpr auto operator"" _catch_sr(char const* rawChars, std::size_t size) noexcept
+    -> Catch::StringRef {
+    return Catch::StringRef(rawChars, size);
+}
+
+// end catch_stringref.h
+// start catch_preprocessor.hpp
+
+#define CATCH_RECURSION_LEVEL0(...) __VA_ARGS__
+#define CATCH_RECURSION_LEVEL1(...)                                                                \
+    CATCH_RECURSION_LEVEL0(CATCH_RECURSION_LEVEL0(CATCH_RECURSION_LEVEL0(__VA_ARGS__)))
+#define CATCH_RECURSION_LEVEL2(...)                                                                \
+    CATCH_RECURSION_LEVEL1(CATCH_RECURSION_LEVEL1(CATCH_RECURSION_LEVEL1(__VA_ARGS__)))
+#define CATCH_RECURSION_LEVEL3(...)                                                                \
+    CATCH_RECURSION_LEVEL2(CATCH_RECURSION_LEVEL2(CATCH_RECURSION_LEVEL2(__VA_ARGS__)))
+#define CATCH_RECURSION_LEVEL4(...)                                                                \
+    CATCH_RECURSION_LEVEL3(CATCH_RECURSION_LEVEL3(CATCH_RECURSION_LEVEL3(__VA_ARGS__)))
+#define CATCH_RECURSION_LEVEL5(...)                                                                \
+    CATCH_RECURSION_LEVEL4(CATCH_RECURSION_LEVEL4(CATCH_RECURSION_LEVEL4(__VA_ARGS__)))
+
+#ifdef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#define INTERNAL_CATCH_EXPAND_VARGS(...) __VA_ARGS__
+// MSVC needs more evaluations
+#define CATCH_RECURSION_LEVEL6(...)                                                                \
+    CATCH_RECURSION_LEVEL5(CATCH_RECURSION_LEVEL5(CATCH_RECURSION_LEVEL5(__VA_ARGS__)))
+#define CATCH_RECURSE(...) CATCH_RECURSION_LEVEL6(CATCH_RECURSION_LEVEL6(__VA_ARGS__))
+#else
+#define CATCH_RECURSE(...) CATCH_RECURSION_LEVEL5(__VA_ARGS__)
+#endif
+
+#define CATCH_REC_END(...)
+#define CATCH_REC_OUT
+
+#define CATCH_EMPTY()
+#define CATCH_DEFER(id) id CATCH_EMPTY()
+
+#define CATCH_REC_GET_END2() 0, CATCH_REC_END
+#define CATCH_REC_GET_END1(...) CATCH_REC_GET_END2
+#define CATCH_REC_GET_END(...) CATCH_REC_GET_END1
+#define CATCH_REC_NEXT0(test, next, ...) next CATCH_REC_OUT
+#define CATCH_REC_NEXT1(test, next) CATCH_DEFER(CATCH_REC_NEXT0)(test, next, 0)
+#define CATCH_REC_NEXT(test, next) CATCH_REC_NEXT1(CATCH_REC_GET_END test, next)
+
+#define CATCH_REC_LIST0(f, x, peek, ...)                                                           \
+    , f(x) CATCH_DEFER(CATCH_REC_NEXT(peek, CATCH_REC_LIST1))(f, peek, __VA_ARGS__)
+#define CATCH_REC_LIST1(f, x, peek, ...)                                                           \
+    , f(x) CATCH_DEFER(CATCH_REC_NEXT(peek, CATCH_REC_LIST0))(f, peek, __VA_ARGS__)
+#define CATCH_REC_LIST2(f, x, peek, ...)                                                           \
+    f(x) CATCH_DEFER(CATCH_REC_NEXT(peek, CATCH_REC_LIST1))(f, peek, __VA_ARGS__)
+
+#define CATCH_REC_LIST0_UD(f, userdata, x, peek, ...)                                              \
+    , f(userdata, x)                                                                               \
+          CATCH_DEFER(CATCH_REC_NEXT(peek, CATCH_REC_LIST1_UD))(f, userdata, peek, __VA_ARGS__)
+#define CATCH_REC_LIST1_UD(f, userdata, x, peek, ...)                                              \
+    , f(userdata, x)                                                                               \
+          CATCH_DEFER(CATCH_REC_NEXT(peek, CATCH_REC_LIST0_UD))(f, userdata, peek, __VA_ARGS__)
+#define CATCH_REC_LIST2_UD(f, userdata, x, peek, ...)                                              \
+    f(userdata, x)                                                                                 \
+        CATCH_DEFER(CATCH_REC_NEXT(peek, CATCH_REC_LIST1_UD))(f, userdata, peek, __VA_ARGS__)
+
+// Applies the function macro `f` to each of the remaining parameters, inserts
+// commas between the results, and passes userdata as the first parameter to
+// each invocation, e.g. CATCH_REC_LIST_UD(f, x, a, b, c) evaluates to f(x, a),
+// f(x, b), f(x, c)
+#define CATCH_REC_LIST_UD(f, userdata, ...)                                                        \
+    CATCH_RECURSE(CATCH_REC_LIST2_UD(f, userdata, __VA_ARGS__, ()()(), ()()(), ()()(), 0))
+
+#define CATCH_REC_LIST(f, ...)                                                                     \
+    CATCH_RECURSE(CATCH_REC_LIST2(f, __VA_ARGS__, ()()(), ()()(), ()()(), 0))
+
+#define INTERNAL_CATCH_EXPAND1(param) INTERNAL_CATCH_EXPAND2(param)
+#define INTERNAL_CATCH_EXPAND2(...) INTERNAL_CATCH_NO##__VA_ARGS__
+#define INTERNAL_CATCH_DEF(...) INTERNAL_CATCH_DEF __VA_ARGS__
+#define INTERNAL_CATCH_NOINTERNAL_CATCH_DEF
+#define INTERNAL_CATCH_STRINGIZE(...) INTERNAL_CATCH_STRINGIZE2(__VA_ARGS__)
+#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#define INTERNAL_CATCH_STRINGIZE2(...) #__VA_ARGS__
+#define INTERNAL_CATCH_STRINGIZE_WITHOUT_PARENS(param)                                             \
+    INTERNAL_CATCH_STRINGIZE(INTERNAL_CATCH_REMOVE_PARENS(param))
+#else
+// MSVC is adding extra space and needs another indirection to expand
+// INTERNAL_CATCH_NOINTERNAL_CATCH_DEF
+#define INTERNAL_CATCH_STRINGIZE2(...) INTERNAL_CATCH_STRINGIZE3(__VA_ARGS__)
+#define INTERNAL_CATCH_STRINGIZE3(...) #__VA_ARGS__
+#define INTERNAL_CATCH_STRINGIZE_WITHOUT_PARENS(param)                                             \
+    (INTERNAL_CATCH_STRINGIZE(INTERNAL_CATCH_REMOVE_PARENS(param)) + 1)
+#endif
+
+#define INTERNAL_CATCH_MAKE_NAMESPACE2(...) ns_##__VA_ARGS__
+#define INTERNAL_CATCH_MAKE_NAMESPACE(name) INTERNAL_CATCH_MAKE_NAMESPACE2(name)
+
+#define INTERNAL_CATCH_REMOVE_PARENS(...) INTERNAL_CATCH_EXPAND1(INTERNAL_CATCH_DEF __VA_ARGS__)
+
+#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#define INTERNAL_CATCH_MAKE_TYPE_LIST2(...)                                                        \
+    decltype(get_wrapper<INTERNAL_CATCH_REMOVE_PARENS_GEN(__VA_ARGS__)>())
+#define INTERNAL_CATCH_MAKE_TYPE_LIST(...)                                                         \
+    INTERNAL_CATCH_MAKE_TYPE_LIST2(INTERNAL_CATCH_REMOVE_PARENS(__VA_ARGS__))
+#else
+#define INTERNAL_CATCH_MAKE_TYPE_LIST2(...)                                                        \
+    INTERNAL_CATCH_EXPAND_VARGS(                                                                   \
+        decltype(get_wrapper<INTERNAL_CATCH_REMOVE_PARENS_GEN(__VA_ARGS__)>()))
+#define INTERNAL_CATCH_MAKE_TYPE_LIST(...)                                                         \
+    INTERNAL_CATCH_EXPAND_VARGS(                                                                   \
+        INTERNAL_CATCH_MAKE_TYPE_LIST2(INTERNAL_CATCH_REMOVE_PARENS(__VA_ARGS__)))
+#endif
+
+#define INTERNAL_CATCH_MAKE_TYPE_LISTS_FROM_TYPES(...)                                             \
+    CATCH_REC_LIST(INTERNAL_CATCH_MAKE_TYPE_LIST, __VA_ARGS__)
+
+#define INTERNAL_CATCH_REMOVE_PARENS_1_ARG(_0) INTERNAL_CATCH_REMOVE_PARENS(_0)
+#define INTERNAL_CATCH_REMOVE_PARENS_2_ARG(_0, _1)                                                 \
+    INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_1_ARG(_1)
+#define INTERNAL_CATCH_REMOVE_PARENS_3_ARG(_0, _1, _2)                                             \
+    INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_2_ARG(_1, _2)
+#define INTERNAL_CATCH_REMOVE_PARENS_4_ARG(_0, _1, _2, _3)                                         \
+    INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_3_ARG(_1, _2, _3)
+#define INTERNAL_CATCH_REMOVE_PARENS_5_ARG(_0, _1, _2, _3, _4)                                     \
+    INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_4_ARG(_1, _2, _3, _4)
+#define INTERNAL_CATCH_REMOVE_PARENS_6_ARG(_0, _1, _2, _3, _4, _5)                                 \
+    INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_5_ARG(_1, _2, _3, _4, _5)
+#define INTERNAL_CATCH_REMOVE_PARENS_7_ARG(_0, _1, _2, _3, _4, _5, _6)                             \
+    INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_6_ARG(_1, _2, _3, _4, _5, _6)
+#define INTERNAL_CATCH_REMOVE_PARENS_8_ARG(_0, _1, _2, _3, _4, _5, _6, _7)                         \
+    INTERNAL_CATCH_REMOVE_PARENS(_0), INTERNAL_CATCH_REMOVE_PARENS_7_ARG(_1, _2, _3, _4, _5, _6, _7)
+#define INTERNAL_CATCH_REMOVE_PARENS_9_ARG(_0, _1, _2, _3, _4, _5, _6, _7, _8)                     \
+    INTERNAL_CATCH_REMOVE_PARENS(_0),                                                              \
+        INTERNAL_CATCH_REMOVE_PARENS_8_ARG(_1, _2, _3, _4, _5, _6, _7, _8)
+#define INTERNAL_CATCH_REMOVE_PARENS_10_ARG(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9)                \
+    INTERNAL_CATCH_REMOVE_PARENS(_0),                                                              \
+        INTERNAL_CATCH_REMOVE_PARENS_9_ARG(_1, _2, _3, _4, _5, _6, _7, _8, _9)
+#define INTERNAL_CATCH_REMOVE_PARENS_11_ARG(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10)           \
+    INTERNAL_CATCH_REMOVE_PARENS(_0),                                                              \
+        INTERNAL_CATCH_REMOVE_PARENS_10_ARG(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10)
+
+#define INTERNAL_CATCH_VA_NARGS_IMPL(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, N, ...) N
+
+#define INTERNAL_CATCH_TYPE_GEN                                                                    \
+    template <typename...> struct TypeList {};                                                     \
+    template <typename... Ts> constexpr auto get_wrapper() noexcept->TypeList<Ts...> {             \
+        return {};                                                                                 \
+    }                                                                                              \
+    template <template <typename...> class...> struct TemplateTypeList {};                         \
+    template <template <typename...> class... Cs>                                                  \
+    constexpr auto get_wrapper() noexcept->TemplateTypeList<Cs...> {                               \
+        return {};                                                                                 \
+    }                                                                                              \
+    template <typename...> struct append;                                                          \
+    template <typename...> struct rewrap;                                                          \
+    template <template <typename...> class, typename...> struct create;                            \
+    template <template <typename...> class, typename> struct convert;                              \
+                                                                                                   \
+    template <typename T> struct append<T> { using type = T; };                                    \
+    template <template <typename...> class L1,                                                     \
+              typename... E1,                                                                      \
+              template <typename...>                                                               \
+              class L2,                                                                            \
+              typename... E2,                                                                      \
+              typename... Rest>                                                                    \
+    struct append<L1<E1...>, L2<E2...>, Rest...> {                                                 \
+        using type = typename append<L1<E1..., E2...>, Rest...>::type;                             \
+    };                                                                                             \
+    template <template <typename...> class L1, typename... E1, typename... Rest>                   \
+    struct append<L1<E1...>, TypeList<mpl_::na>, Rest...> {                                        \
+        using type = L1<E1...>;                                                                    \
+    };                                                                                             \
+                                                                                                   \
+    template <template <typename...> class Container,                                              \
+              template <typename...>                                                               \
+              class List,                                                                          \
+              typename... elems>                                                                   \
+    struct rewrap<TemplateTypeList<Container>, List<elems...>> {                                   \
+        using type = TypeList<Container<elems...>>;                                                \
+    };                                                                                             \
+    template <template <typename...> class Container,                                              \
+              template <typename...>                                                               \
+              class List,                                                                          \
+              class... Elems,                                                                      \
+              typename... Elements>                                                                \
+    struct rewrap<TemplateTypeList<Container>, List<Elems...>, Elements...> {                      \
+        using type = typename append<                                                              \
+            TypeList<Container<Elems...>>,                                                         \
+            typename rewrap<TemplateTypeList<Container>, Elements...>::type>::type;                \
+    };                                                                                             \
+                                                                                                   \
+    template <template <typename...> class Final,                                                  \
+              template <typename...>                                                               \
+              class... Containers,                                                                 \
+              typename... Types>                                                                   \
+    struct create<Final, TemplateTypeList<Containers...>, TypeList<Types...>> {                    \
+        using type = typename append<                                                              \
+            Final<>,                                                                               \
+            typename rewrap<TemplateTypeList<Containers>, Types...>::type...>::type;               \
+    };                                                                                             \
+    template <template <typename...> class Final,                                                  \
+              template <typename...>                                                               \
+              class List,                                                                          \
+              typename... Ts>                                                                      \
+    struct convert<Final, List<Ts...>> {                                                           \
+        using type = typename append<Final<>, TypeList<Ts>...>::type;                              \
+    };
+
+#define INTERNAL_CATCH_NTTP_1(signature, ...)                                                      \
+    template <INTERNAL_CATCH_REMOVE_PARENS(signature)> struct Nttp {};                             \
+    template <INTERNAL_CATCH_REMOVE_PARENS(signature)>                                             \
+    constexpr auto get_wrapper() noexcept->Nttp<__VA_ARGS__> {                                     \
+        return {};                                                                                 \
+    }                                                                                              \
+    template <template <INTERNAL_CATCH_REMOVE_PARENS(signature)> class...>                         \
+    struct NttpTemplateTypeList {};                                                                \
+    template <template <INTERNAL_CATCH_REMOVE_PARENS(signature)> class... Cs>                      \
+    constexpr auto get_wrapper() noexcept->NttpTemplateTypeList<Cs...> {                           \
+        return {};                                                                                 \
+    }                                                                                              \
+                                                                                                   \
+    template <template <INTERNAL_CATCH_REMOVE_PARENS(signature)> class Container,                  \
+              template <INTERNAL_CATCH_REMOVE_PARENS(signature)>                                   \
+              class List,                                                                          \
+              INTERNAL_CATCH_REMOVE_PARENS(signature)>                                             \
+    struct rewrap<NttpTemplateTypeList<Container>, List<__VA_ARGS__>> {                            \
+        using type = TypeList<Container<__VA_ARGS__>>;                                             \
+    };                                                                                             \
+    template <template <INTERNAL_CATCH_REMOVE_PARENS(signature)> class Container,                  \
+              template <INTERNAL_CATCH_REMOVE_PARENS(signature)>                                   \
+              class List,                                                                          \
+              INTERNAL_CATCH_REMOVE_PARENS(signature),                                             \
+              typename... Elements>                                                                \
+    struct rewrap<NttpTemplateTypeList<Container>, List<__VA_ARGS__>, Elements...> {               \
+        using type = typename append<                                                              \
+            TypeList<Container<__VA_ARGS__>>,                                                      \
+            typename rewrap<NttpTemplateTypeList<Container>, Elements...>::type>::type;            \
+    };                                                                                             \
+    template <template <typename...> class Final,                                                  \
+              template <INTERNAL_CATCH_REMOVE_PARENS(signature)>                                   \
+              class... Containers,                                                                 \
+              typename... Types>                                                                   \
+    struct create<Final, NttpTemplateTypeList<Containers...>, TypeList<Types...>> {                \
+        using type = typename append<                                                              \
+            Final<>,                                                                               \
+            typename rewrap<NttpTemplateTypeList<Containers>, Types...>::type...>::type;           \
+    };
+
+#define INTERNAL_CATCH_DECLARE_SIG_TEST0(TestName)
+#define INTERNAL_CATCH_DECLARE_SIG_TEST1(TestName, signature)                                      \
+    template <INTERNAL_CATCH_REMOVE_PARENS(signature)> static void TestName()
+#define INTERNAL_CATCH_DECLARE_SIG_TEST_X(TestName, signature, ...)                                \
+    template <INTERNAL_CATCH_REMOVE_PARENS(signature)> static void TestName()
+
+#define INTERNAL_CATCH_DEFINE_SIG_TEST0(TestName)
+#define INTERNAL_CATCH_DEFINE_SIG_TEST1(TestName, signature)                                       \
+    template <INTERNAL_CATCH_REMOVE_PARENS(signature)> static void TestName()
+#define INTERNAL_CATCH_DEFINE_SIG_TEST_X(TestName, signature, ...)                                 \
+    template <INTERNAL_CATCH_REMOVE_PARENS(signature)> static void TestName()
+
+#define INTERNAL_CATCH_NTTP_REGISTER0(TestFunc, signature)                                         \
+    template <typename Type> void reg_test(TypeList<Type>, Catch::NameAndTags nameAndTags) {       \
+        Catch::AutoReg(Catch::makeTestInvoker(&TestFunc<Type>),                                    \
+                       CATCH_INTERNAL_LINEINFO,                                                    \
+                       Catch::StringRef(),                                                         \
+                       nameAndTags);                                                               \
+    }
+
+#define INTERNAL_CATCH_NTTP_REGISTER(TestFunc, signature, ...)                                     \
+    template <INTERNAL_CATCH_REMOVE_PARENS(signature)>                                             \
+    void reg_test(Nttp<__VA_ARGS__>, Catch::NameAndTags nameAndTags) {                             \
+        Catch::AutoReg(Catch::makeTestInvoker(&TestFunc<__VA_ARGS__>),                             \
+                       CATCH_INTERNAL_LINEINFO,                                                    \
+                       Catch::StringRef(),                                                         \
+                       nameAndTags);                                                               \
+    }
+
+#define INTERNAL_CATCH_NTTP_REGISTER_METHOD0(TestName, signature, ...)                             \
+    template <typename Type>                                                                       \
+    void reg_test(TypeList<Type>, Catch::StringRef className, Catch::NameAndTags nameAndTags) {    \
+        Catch::AutoReg(Catch::makeTestInvoker(&TestName<Type>::test),                              \
+                       CATCH_INTERNAL_LINEINFO,                                                    \
+                       className,                                                                  \
+                       nameAndTags);                                                               \
+    }
+
+#define INTERNAL_CATCH_NTTP_REGISTER_METHOD(TestName, signature, ...)                              \
+    template <INTERNAL_CATCH_REMOVE_PARENS(signature)>                                             \
+    void reg_test(Nttp<__VA_ARGS__>, Catch::StringRef className, Catch::NameAndTags nameAndTags) { \
+        Catch::AutoReg(Catch::makeTestInvoker(&TestName<__VA_ARGS__>::test),                       \
+                       CATCH_INTERNAL_LINEINFO,                                                    \
+                       className,                                                                  \
+                       nameAndTags);                                                               \
+    }
+
+#define INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD0(TestName, ClassName)
+#define INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD1(TestName, ClassName, signature)                    \
+    template <typename TestType>                                                                   \
+    struct TestName : INTERNAL_CATCH_REMOVE_PARENS(ClassName)<TestType> {                          \
+        void test();                                                                               \
+    }
+
+#define INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD_X(TestName, ClassName, signature, ...)              \
+    template <INTERNAL_CATCH_REMOVE_PARENS(signature)>                                             \
+    struct TestName : INTERNAL_CATCH_REMOVE_PARENS(ClassName)<__VA_ARGS__> {                       \
+        void test();                                                                               \
+    }
+
+#define INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD0(TestName)
+#define INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD1(TestName, signature)                                \
+    template <typename TestType>                                                                   \
+    void INTERNAL_CATCH_MAKE_NAMESPACE(TestName)::TestName<TestType>::test()
+#define INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD_X(TestName, signature, ...)                          \
+    template <INTERNAL_CATCH_REMOVE_PARENS(signature)>                                             \
+    void INTERNAL_CATCH_MAKE_NAMESPACE(TestName)::TestName<__VA_ARGS__>::test()
+
+#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#define INTERNAL_CATCH_NTTP_0
+#define INTERNAL_CATCH_NTTP_GEN(...)                                                               \
+    INTERNAL_CATCH_VA_NARGS_IMPL(__VA_ARGS__,                                                      \
+                                 INTERNAL_CATCH_NTTP_1(__VA_ARGS__),                               \
+                                 INTERNAL_CATCH_NTTP_1(__VA_ARGS__),                               \
+                                 INTERNAL_CATCH_NTTP_1(__VA_ARGS__),                               \
+                                 INTERNAL_CATCH_NTTP_1(__VA_ARGS__),                               \
+                                 INTERNAL_CATCH_NTTP_1(__VA_ARGS__),                               \
+                                 INTERNAL_CATCH_NTTP_1(__VA_ARGS__),                               \
+                                 INTERNAL_CATCH_NTTP_1(__VA_ARGS__),                               \
+                                 INTERNAL_CATCH_NTTP_1(__VA_ARGS__),                               \
+                                 INTERNAL_CATCH_NTTP_1(__VA_ARGS__),                               \
+                                 INTERNAL_CATCH_NTTP_1(__VA_ARGS__),                               \
+                                 INTERNAL_CATCH_NTTP_0)
+#define INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD(TestName, ...)                                       \
+    INTERNAL_CATCH_VA_NARGS_IMPL("dummy",                                                          \
+                                 __VA_ARGS__,                                                      \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD_X,                          \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD_X,                          \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD_X,                          \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD_X,                          \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD_X,                          \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD_X,                          \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD_X,                          \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD_X,                          \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD_X,                          \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD1,                           \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD0)                           \
+    (TestName, __VA_ARGS__)
+#define INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD(TestName, ClassName, ...)                           \
+    INTERNAL_CATCH_VA_NARGS_IMPL("dummy",                                                          \
+                                 __VA_ARGS__,                                                      \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD_X,                         \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD_X,                         \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD_X,                         \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD_X,                         \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD_X,                         \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD_X,                         \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD_X,                         \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD_X,                         \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD_X,                         \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD1,                          \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD0)                          \
+    (TestName, ClassName, __VA_ARGS__)
+#define INTERNAL_CATCH_NTTP_REG_METHOD_GEN(TestName, ...)                                          \
+    INTERNAL_CATCH_VA_NARGS_IMPL("dummy",                                                          \
+                                 __VA_ARGS__,                                                      \
+                                 INTERNAL_CATCH_NTTP_REGISTER_METHOD,                              \
+                                 INTERNAL_CATCH_NTTP_REGISTER_METHOD,                              \
+                                 INTERNAL_CATCH_NTTP_REGISTER_METHOD,                              \
+                                 INTERNAL_CATCH_NTTP_REGISTER_METHOD,                              \
+                                 INTERNAL_CATCH_NTTP_REGISTER_METHOD,                              \
+                                 INTERNAL_CATCH_NTTP_REGISTER_METHOD,                              \
+                                 INTERNAL_CATCH_NTTP_REGISTER_METHOD,                              \
+                                 INTERNAL_CATCH_NTTP_REGISTER_METHOD,                              \
+                                 INTERNAL_CATCH_NTTP_REGISTER_METHOD,                              \
+                                 INTERNAL_CATCH_NTTP_REGISTER_METHOD0,                             \
+                                 INTERNAL_CATCH_NTTP_REGISTER_METHOD0)                             \
+    (TestName, __VA_ARGS__)
+#define INTERNAL_CATCH_NTTP_REG_GEN(TestFunc, ...)                                                 \
+    INTERNAL_CATCH_VA_NARGS_IMPL("dummy",                                                          \
+                                 __VA_ARGS__,                                                      \
+                                 INTERNAL_CATCH_NTTP_REGISTER,                                     \
+                                 INTERNAL_CATCH_NTTP_REGISTER,                                     \
+                                 INTERNAL_CATCH_NTTP_REGISTER,                                     \
+                                 INTERNAL_CATCH_NTTP_REGISTER,                                     \
+                                 INTERNAL_CATCH_NTTP_REGISTER,                                     \
+                                 INTERNAL_CATCH_NTTP_REGISTER,                                     \
+                                 INTERNAL_CATCH_NTTP_REGISTER,                                     \
+                                 INTERNAL_CATCH_NTTP_REGISTER,                                     \
+                                 INTERNAL_CATCH_NTTP_REGISTER,                                     \
+                                 INTERNAL_CATCH_NTTP_REGISTER0,                                    \
+                                 INTERNAL_CATCH_NTTP_REGISTER0)                                    \
+    (TestFunc, __VA_ARGS__)
+#define INTERNAL_CATCH_DEFINE_SIG_TEST(TestName, ...)                                              \
+    INTERNAL_CATCH_VA_NARGS_IMPL("dummy",                                                          \
+                                 __VA_ARGS__,                                                      \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_X,                                 \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_X,                                 \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_X,                                 \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_X,                                 \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_X,                                 \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_X,                                 \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_X,                                 \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_X,                                 \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_X,                                 \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST1,                                  \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST0)                                  \
+    (TestName, __VA_ARGS__)
+#define INTERNAL_CATCH_DECLARE_SIG_TEST(TestName, ...)                                             \
+    INTERNAL_CATCH_VA_NARGS_IMPL("dummy",                                                          \
+                                 __VA_ARGS__,                                                      \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST_X,                                \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST_X,                                \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST_X,                                \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST_X,                                \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST_X,                                \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST_X,                                \
+                                 INTERNAL_CATCH_DEFINE_SIG_TEST_X,                                 \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST_X,                                \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST_X,                                \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST1,                                 \
+                                 INTERNAL_CATCH_DECLARE_SIG_TEST0)                                 \
+    (TestName, __VA_ARGS__)
+#define INTERNAL_CATCH_REMOVE_PARENS_GEN(...)                                                      \
+    INTERNAL_CATCH_VA_NARGS_IMPL(__VA_ARGS__,                                                      \
+                                 INTERNAL_CATCH_REMOVE_PARENS_11_ARG,                              \
+                                 INTERNAL_CATCH_REMOVE_PARENS_10_ARG,                              \
+                                 INTERNAL_CATCH_REMOVE_PARENS_9_ARG,                               \
+                                 INTERNAL_CATCH_REMOVE_PARENS_8_ARG,                               \
+                                 INTERNAL_CATCH_REMOVE_PARENS_7_ARG,                               \
+                                 INTERNAL_CATCH_REMOVE_PARENS_6_ARG,                               \
+                                 INTERNAL_CATCH_REMOVE_PARENS_5_ARG,                               \
+                                 INTERNAL_CATCH_REMOVE_PARENS_4_ARG,                               \
+                                 INTERNAL_CATCH_REMOVE_PARENS_3_ARG,                               \
+                                 INTERNAL_CATCH_REMOVE_PARENS_2_ARG,                               \
+                                 INTERNAL_CATCH_REMOVE_PARENS_1_ARG)                               \
+    (__VA_ARGS__)
+#else
+#define INTERNAL_CATCH_NTTP_0(signature)
+#define INTERNAL_CATCH_NTTP_GEN(...)                                                               \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_VA_NARGS_IMPL(__VA_ARGS__,                          \
+                                                             INTERNAL_CATCH_NTTP_1,                \
+                                                             INTERNAL_CATCH_NTTP_1,                \
+                                                             INTERNAL_CATCH_NTTP_1,                \
+                                                             INTERNAL_CATCH_NTTP_1,                \
+                                                             INTERNAL_CATCH_NTTP_1,                \
+                                                             INTERNAL_CATCH_NTTP_1,                \
+                                                             INTERNAL_CATCH_NTTP_1,                \
+                                                             INTERNAL_CATCH_NTTP_1,                \
+                                                             INTERNAL_CATCH_NTTP_1,                \
+                                                             INTERNAL_CATCH_NTTP_1,                \
+                                                             INTERNAL_CATCH_NTTP_0)(__VA_ARGS__))
+#define INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD(TestName, ...)                                       \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_VA_NARGS_IMPL(                                      \
+        "dummy",                                                                                   \
+        __VA_ARGS__,                                                                               \
+        INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD_X,                                                   \
+        INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD_X,                                                   \
+        INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD_X,                                                   \
+        INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD_X,                                                   \
+        INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD_X,                                                   \
+        INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD_X,                                                   \
+        INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD_X,                                                   \
+        INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD_X,                                                   \
+        INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD_X,                                                   \
+        INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD1,                                                    \
+        INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD0)(TestName, __VA_ARGS__))
+#define INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD(TestName, ClassName, ...)                           \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_VA_NARGS_IMPL(                                      \
+        "dummy",                                                                                   \
+        __VA_ARGS__,                                                                               \
+        INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD_X,                                                  \
+        INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD_X,                                                  \
+        INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD_X,                                                  \
+        INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD_X,                                                  \
+        INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD_X,                                                  \
+        INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD_X,                                                  \
+        INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD_X,                                                  \
+        INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD_X,                                                  \
+        INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD_X,                                                  \
+        INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD1,                                                   \
+        INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD0)(TestName, ClassName, __VA_ARGS__))
+#define INTERNAL_CATCH_NTTP_REG_METHOD_GEN(TestName, ...)                                          \
+    INTERNAL_CATCH_EXPAND_VARGS(                                                                   \
+        INTERNAL_CATCH_VA_NARGS_IMPL("dummy",                                                      \
+                                     __VA_ARGS__,                                                  \
+                                     INTERNAL_CATCH_NTTP_REGISTER_METHOD,                          \
+                                     INTERNAL_CATCH_NTTP_REGISTER_METHOD,                          \
+                                     INTERNAL_CATCH_NTTP_REGISTER_METHOD,                          \
+                                     INTERNAL_CATCH_NTTP_REGISTER_METHOD,                          \
+                                     INTERNAL_CATCH_NTTP_REGISTER_METHOD,                          \
+                                     INTERNAL_CATCH_NTTP_REGISTER_METHOD,                          \
+                                     INTERNAL_CATCH_NTTP_REGISTER_METHOD,                          \
+                                     INTERNAL_CATCH_NTTP_REGISTER_METHOD,                          \
+                                     INTERNAL_CATCH_NTTP_REGISTER_METHOD,                          \
+                                     INTERNAL_CATCH_NTTP_REGISTER_METHOD0,                         \
+                                     INTERNAL_CATCH_NTTP_REGISTER_METHOD0)(TestName, __VA_ARGS__))
+#define INTERNAL_CATCH_NTTP_REG_GEN(TestFunc, ...)                                                 \
+    INTERNAL_CATCH_EXPAND_VARGS(                                                                   \
+        INTERNAL_CATCH_VA_NARGS_IMPL("dummy",                                                      \
+                                     __VA_ARGS__,                                                  \
+                                     INTERNAL_CATCH_NTTP_REGISTER,                                 \
+                                     INTERNAL_CATCH_NTTP_REGISTER,                                 \
+                                     INTERNAL_CATCH_NTTP_REGISTER,                                 \
+                                     INTERNAL_CATCH_NTTP_REGISTER,                                 \
+                                     INTERNAL_CATCH_NTTP_REGISTER,                                 \
+                                     INTERNAL_CATCH_NTTP_REGISTER,                                 \
+                                     INTERNAL_CATCH_NTTP_REGISTER,                                 \
+                                     INTERNAL_CATCH_NTTP_REGISTER,                                 \
+                                     INTERNAL_CATCH_NTTP_REGISTER,                                 \
+                                     INTERNAL_CATCH_NTTP_REGISTER0,                                \
+                                     INTERNAL_CATCH_NTTP_REGISTER0)(TestFunc, __VA_ARGS__))
+#define INTERNAL_CATCH_DEFINE_SIG_TEST(TestName, ...)                                              \
+    INTERNAL_CATCH_EXPAND_VARGS(                                                                   \
+        INTERNAL_CATCH_VA_NARGS_IMPL("dummy",                                                      \
+                                     __VA_ARGS__,                                                  \
+                                     INTERNAL_CATCH_DEFINE_SIG_TEST_X,                             \
+                                     INTERNAL_CATCH_DEFINE_SIG_TEST_X,                             \
+                                     INTERNAL_CATCH_DEFINE_SIG_TEST_X,                             \
+                                     INTERNAL_CATCH_DEFINE_SIG_TEST_X,                             \
+                                     INTERNAL_CATCH_DEFINE_SIG_TEST_X,                             \
+                                     INTERNAL_CATCH_DEFINE_SIG_TEST_X,                             \
+                                     INTERNAL_CATCH_DEFINE_SIG_TEST_X,                             \
+                                     INTERNAL_CATCH_DEFINE_SIG_TEST_X,                             \
+                                     INTERNAL_CATCH_DEFINE_SIG_TEST_X,                             \
+                                     INTERNAL_CATCH_DEFINE_SIG_TEST1,                              \
+                                     INTERNAL_CATCH_DEFINE_SIG_TEST0)(TestName, __VA_ARGS__))
+#define INTERNAL_CATCH_DECLARE_SIG_TEST(TestName, ...)                                             \
+    INTERNAL_CATCH_EXPAND_VARGS(                                                                   \
+        INTERNAL_CATCH_VA_NARGS_IMPL("dummy",                                                      \
+                                     __VA_ARGS__,                                                  \
+                                     INTERNAL_CATCH_DECLARE_SIG_TEST_X,                            \
+                                     INTERNAL_CATCH_DECLARE_SIG_TEST_X,                            \
+                                     INTERNAL_CATCH_DECLARE_SIG_TEST_X,                            \
+                                     INTERNAL_CATCH_DECLARE_SIG_TEST_X,                            \
+                                     INTERNAL_CATCH_DECLARE_SIG_TEST_X,                            \
+                                     INTERNAL_CATCH_DECLARE_SIG_TEST_X,                            \
+                                     INTERNAL_CATCH_DEFINE_SIG_TEST_X,                             \
+                                     INTERNAL_CATCH_DECLARE_SIG_TEST_X,                            \
+                                     INTERNAL_CATCH_DECLARE_SIG_TEST_X,                            \
+                                     INTERNAL_CATCH_DECLARE_SIG_TEST1,                             \
+                                     INTERNAL_CATCH_DECLARE_SIG_TEST0)(TestName, __VA_ARGS__))
+#define INTERNAL_CATCH_REMOVE_PARENS_GEN(...)                                                      \
+    INTERNAL_CATCH_EXPAND_VARGS(                                                                   \
+        INTERNAL_CATCH_VA_NARGS_IMPL(__VA_ARGS__,                                                  \
+                                     INTERNAL_CATCH_REMOVE_PARENS_11_ARG,                          \
+                                     INTERNAL_CATCH_REMOVE_PARENS_10_ARG,                          \
+                                     INTERNAL_CATCH_REMOVE_PARENS_9_ARG,                           \
+                                     INTERNAL_CATCH_REMOVE_PARENS_8_ARG,                           \
+                                     INTERNAL_CATCH_REMOVE_PARENS_7_ARG,                           \
+                                     INTERNAL_CATCH_REMOVE_PARENS_6_ARG,                           \
+                                     INTERNAL_CATCH_REMOVE_PARENS_5_ARG,                           \
+                                     INTERNAL_CATCH_REMOVE_PARENS_4_ARG,                           \
+                                     INTERNAL_CATCH_REMOVE_PARENS_3_ARG,                           \
+                                     INTERNAL_CATCH_REMOVE_PARENS_2_ARG,                           \
+                                     INTERNAL_CATCH_REMOVE_PARENS_1_ARG)(__VA_ARGS__))
+#endif
+
+// end catch_preprocessor.hpp
+// start catch_meta.hpp
+
+#include <type_traits>
+
+namespace Catch {
+    template <typename T> struct always_false : std::false_type {};
+
+    template <typename> struct true_given : std::true_type {};
+    struct is_callable_tester {
+        template <typename Fun, typename... Args>
+        true_given<decltype(std::declval<Fun>()(std::declval<Args>()...))> static test(int);
+        template <typename...> std::false_type static test(...);
+    };
+
+    template <typename T> struct is_callable;
+
+    template <typename Fun, typename... Args>
+    struct is_callable<Fun(Args...)> : decltype(is_callable_tester::test<Fun, Args...>(0)) {};
+
+#if defined(__cpp_lib_is_invocable) && __cpp_lib_is_invocable >= 201703
+    // std::result_of is deprecated in C++17 and removed in C++20. Hence, it is
+    // replaced with std::invoke_result here.
+    template <typename Func, typename... U>
+    using FunctionReturnType =
+        std::remove_reference_t<std::remove_cv_t<std::invoke_result_t<Func, U...>>>;
+#else
+    // Keep ::type here because we still support C++11
+    template <typename Func, typename... U>
+    using FunctionReturnType = typename std::remove_reference<
+        typename std::remove_cv<typename std::result_of<Func(U...)>::type>::type>::type;
+#endif
+
+} // namespace Catch
+
+namespace mpl_ {
+    struct na;
+}
+
+// end catch_meta.hpp
+namespace Catch {
+
+    template <typename C> class TestInvokerAsMethod : public ITestInvoker {
+        void (C::*m_testAsMethod)();
+
+    public:
+        TestInvokerAsMethod(void (C::*testAsMethod)()) noexcept : m_testAsMethod(testAsMethod) {}
+
+        void invoke() const override {
+            C obj;
+            (obj.*m_testAsMethod)();
+        }
+    };
+
+    auto makeTestInvoker(void (*testAsFunction)()) noexcept -> ITestInvoker*;
+
+    template <typename C>
+    auto makeTestInvoker(void (C::*testAsMethod)()) noexcept -> ITestInvoker* {
+        return new (std::nothrow) TestInvokerAsMethod<C>(testAsMethod);
+    }
+
+    struct NameAndTags {
+        NameAndTags(StringRef const& name_ = StringRef(),
+                    StringRef const& tags_ = StringRef()) noexcept;
+        StringRef name;
+        StringRef tags;
+    };
+
+    struct AutoReg : NonCopyable {
+        AutoReg(ITestInvoker* invoker,
+                SourceLineInfo const& lineInfo,
+                StringRef const& classOrMethod,
+                NameAndTags const& nameAndTags) noexcept;
+        ~AutoReg();
+    };
+
+} // end namespace Catch
+
+#if defined(CATCH_CONFIG_DISABLE)
+#define INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(TestName, ...) static void TestName()
+#define INTERNAL_CATCH_TESTCASE_METHOD_NO_REGISTRATION(TestName, ClassName, ...)                   \
+    namespace {                                                                                    \
+        struct TestName : INTERNAL_CATCH_REMOVE_PARENS(ClassName) {                                \
+            void test();                                                                           \
+        };                                                                                         \
+    }                                                                                              \
+    void TestName::test()
+#define INTERNAL_CATCH_TEMPLATE_TEST_CASE_NO_REGISTRATION_2(                                       \
+    TestName, TestFunc, Name, Tags, Signature, ...)                                                \
+    INTERNAL_CATCH_DEFINE_SIG_TEST(TestFunc, INTERNAL_CATCH_REMOVE_PARENS(Signature))
+#define INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_NO_REGISTRATION_2(                                \
+    TestNameClass, TestName, ClassName, Name, Tags, Signature, ...)                                \
+    namespace {                                                                                    \
+        namespace INTERNAL_CATCH_MAKE_NAMESPACE(TestName) {                                        \
+            INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD(                                                \
+                TestName, ClassName, INTERNAL_CATCH_REMOVE_PARENS(Signature));                     \
+        }                                                                                          \
+    }                                                                                              \
+    INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD(TestName, INTERNAL_CATCH_REMOVE_PARENS(Signature))
+
+#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#define INTERNAL_CATCH_TEMPLATE_TEST_CASE_NO_REGISTRATION(Name, Tags, ...)                         \
+    INTERNAL_CATCH_TEMPLATE_TEST_CASE_NO_REGISTRATION_2(                                           \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____F_U_N_C____),    \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        typename TestType,                                                                         \
+        __VA_ARGS__)
+#else
+#define INTERNAL_CATCH_TEMPLATE_TEST_CASE_NO_REGISTRATION(Name, Tags, ...)                         \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_TEST_CASE_NO_REGISTRATION_2(               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____F_U_N_C____),    \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        typename TestType,                                                                         \
+        __VA_ARGS__))
+#endif
+
+#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#define INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG_NO_REGISTRATION(Name, Tags, Signature, ...)          \
+    INTERNAL_CATCH_TEMPLATE_TEST_CASE_NO_REGISTRATION_2(                                           \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____F_U_N_C____),    \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        Signature,                                                                                 \
+        __VA_ARGS__)
+#else
+#define INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG_NO_REGISTRATION(Name, Tags, Signature, ...)          \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_TEST_CASE_NO_REGISTRATION_2(               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____F_U_N_C____),    \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        Signature,                                                                                 \
+        __VA_ARGS__))
+#endif
+
+#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#define INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_NO_REGISTRATION(ClassName, Name, Tags, ...)       \
+    INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_NO_REGISTRATION_2(                                    \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____C_L_A_S_S____),  \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        ClassName,                                                                                 \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        typename T,                                                                                \
+        __VA_ARGS__)
+#else
+#define INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_NO_REGISTRATION(ClassName, Name, Tags, ...)       \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_NO_REGISTRATION_2(        \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____C_L_A_S_S____),  \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        ClassName,                                                                                 \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        typename T,                                                                                \
+        __VA_ARGS__))
+#endif
+
+#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#define INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG_NO_REGISTRATION(                              \
+    ClassName, Name, Tags, Signature, ...)                                                         \
+    INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_NO_REGISTRATION_2(                                    \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____C_L_A_S_S____),  \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        ClassName,                                                                                 \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        Signature,                                                                                 \
+        __VA_ARGS__)
+#else
+#define INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG_NO_REGISTRATION(                              \
+    ClassName, Name, Tags, Signature, ...)                                                         \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_NO_REGISTRATION_2(        \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____C_L_A_S_S____),  \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        ClassName,                                                                                 \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        Signature,                                                                                 \
+        __VA_ARGS__))
+#endif
+#endif
+
+///////////////////////////////////////////////////////////////////////////////
+#define INTERNAL_CATCH_TESTCASE2(TestName, ...)                                                    \
+    static void TestName();                                                                        \
+    CATCH_INTERNAL_START_WARNINGS_SUPPRESSION                                                      \
+    CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS                                                       \
+    namespace {                                                                                    \
+        Catch::AutoReg                                                                             \
+            INTERNAL_CATCH_UNIQUE_NAME(autoRegistrar)(Catch::makeTestInvoker(&TestName),           \
+                                                      CATCH_INTERNAL_LINEINFO,                     \
+                                                      Catch::StringRef(),                          \
+                                                      Catch::NameAndTags{__VA_ARGS__});            \
+    } /* NOLINT */                                                                                 \
+    CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION                                                       \
+    static void TestName()
+#define INTERNAL_CATCH_TESTCASE(...)                                                               \
+    INTERNAL_CATCH_TESTCASE2(INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_S_T____), __VA_ARGS__)
+
+///////////////////////////////////////////////////////////////////////////////
+#define INTERNAL_CATCH_METHOD_AS_TEST_CASE(QualifiedMethod, ...)                                   \
+    CATCH_INTERNAL_START_WARNINGS_SUPPRESSION                                                      \
+    CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS                                                       \
+    namespace {                                                                                    \
+        Catch::AutoReg                                                                             \
+            INTERNAL_CATCH_UNIQUE_NAME(autoRegistrar)(Catch::makeTestInvoker(&QualifiedMethod),    \
+                                                      CATCH_INTERNAL_LINEINFO,                     \
+                                                      "&" #QualifiedMethod,                        \
+                                                      Catch::NameAndTags{__VA_ARGS__});            \
+    } /* NOLINT */                                                                                 \
+    CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION
+
+///////////////////////////////////////////////////////////////////////////////
+#define INTERNAL_CATCH_TEST_CASE_METHOD2(TestName, ClassName, ...)                                 \
+    CATCH_INTERNAL_START_WARNINGS_SUPPRESSION                                                      \
+    CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS                                                       \
+    namespace {                                                                                    \
+        struct TestName : INTERNAL_CATCH_REMOVE_PARENS(ClassName) {                                \
+            void test();                                                                           \
+        };                                                                                         \
+        Catch::AutoReg INTERNAL_CATCH_UNIQUE_NAME(autoRegistrar)(                                  \
+            Catch::makeTestInvoker(&TestName::test),                                               \
+            CATCH_INTERNAL_LINEINFO,                                                               \
+            #ClassName,                                                                            \
+            Catch::NameAndTags{__VA_ARGS__}); /* NOLINT */                                         \
+    }                                                                                              \
+    CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION                                                       \
+    void TestName::test()
+#define INTERNAL_CATCH_TEST_CASE_METHOD(ClassName, ...)                                            \
+    INTERNAL_CATCH_TEST_CASE_METHOD2(                                                              \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_S_T____), ClassName, __VA_ARGS__)
+
+///////////////////////////////////////////////////////////////////////////////
+#define INTERNAL_CATCH_REGISTER_TESTCASE(Function, ...)                                            \
+    CATCH_INTERNAL_START_WARNINGS_SUPPRESSION                                                      \
+    CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS                                                       \
+    Catch::AutoReg INTERNAL_CATCH_UNIQUE_NAME(autoRegistrar)(                                      \
+        Catch::makeTestInvoker(Function),                                                          \
+        CATCH_INTERNAL_LINEINFO,                                                                   \
+        Catch::StringRef(),                                                                        \
+        Catch::NameAndTags{__VA_ARGS__}); /* NOLINT */                                             \
+    CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION
+
+///////////////////////////////////////////////////////////////////////////////
+#define INTERNAL_CATCH_TEMPLATE_TEST_CASE_2(TestName, TestFunc, Name, Tags, Signature, ...)        \
+    CATCH_INTERNAL_START_WARNINGS_SUPPRESSION                                                      \
+    CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS                                                       \
+    CATCH_INTERNAL_SUPPRESS_ZERO_VARIADIC_WARNINGS                                                 \
+    CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS                                               \
+    INTERNAL_CATCH_DECLARE_SIG_TEST(TestFunc, INTERNAL_CATCH_REMOVE_PARENS(Signature));            \
+    namespace {                                                                                    \
+        namespace INTERNAL_CATCH_MAKE_NAMESPACE(TestName) {                                        \
+            INTERNAL_CATCH_TYPE_GEN                                                                \
+            INTERNAL_CATCH_NTTP_GEN(INTERNAL_CATCH_REMOVE_PARENS(Signature))                       \
+            INTERNAL_CATCH_NTTP_REG_GEN(TestFunc, INTERNAL_CATCH_REMOVE_PARENS(Signature))         \
+            template <typename... Types> struct TestName {                                         \
+                TestName() {                                                                       \
+                    int index = 0;                                                                 \
+                    constexpr char const* tmpl_types[] = {                                         \
+                        CATCH_REC_LIST(INTERNAL_CATCH_STRINGIZE_WITHOUT_PARENS, __VA_ARGS__)};     \
+                    using expander = int[];                                                        \
+                    (void)expander{                                                                \
+                        (reg_test(Types{},                                                         \
+                                  Catch::NameAndTags{Name " - " + std::string(tmpl_types[index]),  \
+                                                     Tags}),                                       \
+                         index++)...}; /* NOLINT */                                                \
+                }                                                                                  \
+            };                                                                                     \
+            static int INTERNAL_CATCH_UNIQUE_NAME(globalRegistrar) = []() {                        \
+                TestName<INTERNAL_CATCH_MAKE_TYPE_LISTS_FROM_TYPES(__VA_ARGS__)>();                \
+                return 0;                                                                          \
+            }();                                                                                   \
+        }                                                                                          \
+    }                                                                                              \
+    CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION                                                       \
+    INTERNAL_CATCH_DEFINE_SIG_TEST(TestFunc, INTERNAL_CATCH_REMOVE_PARENS(Signature))
+
+#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#define INTERNAL_CATCH_TEMPLATE_TEST_CASE(Name, Tags, ...)                                         \
+    INTERNAL_CATCH_TEMPLATE_TEST_CASE_2(                                                           \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____F_U_N_C____),    \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        typename TestType,                                                                         \
+        __VA_ARGS__)
+#else
+#define INTERNAL_CATCH_TEMPLATE_TEST_CASE(Name, Tags, ...)                                         \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_TEST_CASE_2(                               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____F_U_N_C____),    \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        typename TestType,                                                                         \
+        __VA_ARGS__))
+#endif
+
+#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#define INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG(Name, Tags, Signature, ...)                          \
+    INTERNAL_CATCH_TEMPLATE_TEST_CASE_2(                                                           \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____F_U_N_C____),    \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        Signature,                                                                                 \
+        __VA_ARGS__)
+#else
+#define INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG(Name, Tags, Signature, ...)                          \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_TEST_CASE_2(                               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____F_U_N_C____),    \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        Signature,                                                                                 \
+        __VA_ARGS__))
+#endif
+
+#define INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE2(                                                \
+    TestName, TestFuncName, Name, Tags, Signature, TmplTypes, TypesList)                           \
+    CATCH_INTERNAL_START_WARNINGS_SUPPRESSION                                                      \
+    CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS                                                       \
+    CATCH_INTERNAL_SUPPRESS_ZERO_VARIADIC_WARNINGS                                                 \
+    CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS                                               \
+    template <typename TestType> static void TestFuncName();                                       \
+    namespace {                                                                                    \
+        namespace INTERNAL_CATCH_MAKE_NAMESPACE(TestName) {                                        \
+            INTERNAL_CATCH_TYPE_GEN                                                                \
+            INTERNAL_CATCH_NTTP_GEN(INTERNAL_CATCH_REMOVE_PARENS(Signature))                       \
+            template <typename... Types> struct TestName {                                         \
+                void reg_tests() {                                                                 \
+                    int index = 0;                                                                 \
+                    using expander = int[];                                                        \
+                    constexpr char const* tmpl_types[] = {                                         \
+                        CATCH_REC_LIST(INTERNAL_CATCH_STRINGIZE_WITHOUT_PARENS,                    \
+                                       INTERNAL_CATCH_REMOVE_PARENS(TmplTypes))};                  \
+                    constexpr char const* types_list[] = {                                         \
+                        CATCH_REC_LIST(INTERNAL_CATCH_STRINGIZE_WITHOUT_PARENS,                    \
+                                       INTERNAL_CATCH_REMOVE_PARENS(TypesList))};                  \
+                    constexpr auto num_types = sizeof(types_list) / sizeof(types_list[0]);         \
+                    (void)expander{                                                                \
+                        (Catch::AutoReg(Catch::makeTestInvoker(&TestFuncName<Types>),              \
+                                        CATCH_INTERNAL_LINEINFO,                                   \
+                                        Catch::StringRef(),                                        \
+                                        Catch::NameAndTags{                                        \
+                                            Name " - " +                                           \
+                                                std::string(tmpl_types[index / num_types]) + "<" + \
+                                                std::string(types_list[index % num_types]) + ">",  \
+                                            Tags}),                                                \
+                         index++)...}; /* NOLINT */                                                \
+                }                                                                                  \
+            };                                                                                     \
+            static int INTERNAL_CATCH_UNIQUE_NAME(globalRegistrar) = []() {                        \
+                using TestInit = typename create<                                                  \
+                    TestName,                                                                      \
+                    decltype(get_wrapper<INTERNAL_CATCH_REMOVE_PARENS(TmplTypes)>()),              \
+                    TypeList<INTERNAL_CATCH_MAKE_TYPE_LISTS_FROM_TYPES(                            \
+                        INTERNAL_CATCH_REMOVE_PARENS(TypesList))>>::type;                          \
+                TestInit t;                                                                        \
+                t.reg_tests();                                                                     \
+                return 0;                                                                          \
+            }();                                                                                   \
+        }                                                                                          \
+    }                                                                                              \
+    CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION                                                       \
+    template <typename TestType> static void TestFuncName()
+
+#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#define INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE(Name, Tags, ...)                                 \
+    INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE2(                                                    \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____F_U_N_C____),    \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        typename T,                                                                                \
+        __VA_ARGS__)
+#else
+#define INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE(Name, Tags, ...)                                 \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE2(                        \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____F_U_N_C____),    \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        typename T,                                                                                \
+        __VA_ARGS__))
+#endif
+
+#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#define INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG(Name, Tags, Signature, ...)                  \
+    INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE2(                                                    \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____F_U_N_C____),    \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        Signature,                                                                                 \
+        __VA_ARGS__)
+#else
+#define INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG(Name, Tags, Signature, ...)                  \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE2(                        \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____F_U_N_C____),    \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        Signature,                                                                                 \
+        __VA_ARGS__))
+#endif
+
+#define INTERNAL_CATCH_TEMPLATE_LIST_TEST_CASE_2(TestName, TestFunc, Name, Tags, TmplList)         \
+    CATCH_INTERNAL_START_WARNINGS_SUPPRESSION                                                      \
+    CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS                                                       \
+    CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS                                               \
+    template <typename TestType> static void TestFunc();                                           \
+    namespace {                                                                                    \
+        namespace INTERNAL_CATCH_MAKE_NAMESPACE(TestName) {                                        \
+            INTERNAL_CATCH_TYPE_GEN                                                                \
+            template <typename... Types> struct TestName {                                         \
+                void reg_tests() {                                                                 \
+                    int index = 0;                                                                 \
+                    using expander = int[];                                                        \
+                    (void)expander{                                                                \
+                        (Catch::AutoReg(Catch::makeTestInvoker(&TestFunc<Types>),                  \
+                                        CATCH_INTERNAL_LINEINFO,                                   \
+                                        Catch::StringRef(),                                        \
+                                        Catch::NameAndTags{                                        \
+                                            Name " - " +                                           \
+                                                std::string(INTERNAL_CATCH_STRINGIZE(TmplList)) +  \
+                                                " - " + std::to_string(index),                     \
+                                            Tags}),                                                \
+                         index++)...}; /* NOLINT */                                                \
+                }                                                                                  \
+            };                                                                                     \
+            static int INTERNAL_CATCH_UNIQUE_NAME(globalRegistrar) = []() {                        \
+                using TestInit = typename convert<TestName, TmplList>::type;                       \
+                TestInit t;                                                                        \
+                t.reg_tests();                                                                     \
+                return 0;                                                                          \
+            }();                                                                                   \
+        }                                                                                          \
+    }                                                                                              \
+    CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION                                                       \
+    template <typename TestType> static void TestFunc()
+
+#define INTERNAL_CATCH_TEMPLATE_LIST_TEST_CASE(Name, Tags, TmplList)                               \
+    INTERNAL_CATCH_TEMPLATE_LIST_TEST_CASE_2(                                                      \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____F_U_N_C____),    \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        TmplList)
+
+#define INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_2(                                                \
+    TestNameClass, TestName, ClassName, Name, Tags, Signature, ...)                                \
+    CATCH_INTERNAL_START_WARNINGS_SUPPRESSION                                                      \
+    CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS                                                       \
+    CATCH_INTERNAL_SUPPRESS_ZERO_VARIADIC_WARNINGS                                                 \
+    CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS                                               \
+    namespace {                                                                                    \
+        namespace INTERNAL_CATCH_MAKE_NAMESPACE(TestName) {                                        \
+            INTERNAL_CATCH_TYPE_GEN                                                                \
+            INTERNAL_CATCH_NTTP_GEN(INTERNAL_CATCH_REMOVE_PARENS(Signature))                       \
+            INTERNAL_CATCH_DECLARE_SIG_TEST_METHOD(                                                \
+                TestName, ClassName, INTERNAL_CATCH_REMOVE_PARENS(Signature));                     \
+            INTERNAL_CATCH_NTTP_REG_METHOD_GEN(TestName, INTERNAL_CATCH_REMOVE_PARENS(Signature))  \
+            template <typename... Types> struct TestNameClass {                                    \
+                TestNameClass() {                                                                  \
+                    int index = 0;                                                                 \
+                    constexpr char const* tmpl_types[] = {                                         \
+                        CATCH_REC_LIST(INTERNAL_CATCH_STRINGIZE_WITHOUT_PARENS, __VA_ARGS__)};     \
+                    using expander = int[];                                                        \
+                    (void)expander{                                                                \
+                        (reg_test(Types{},                                                         \
+                                  #ClassName,                                                      \
+                                  Catch::NameAndTags{Name " - " + std::string(tmpl_types[index]),  \
+                                                     Tags}),                                       \
+                         index++)...}; /* NOLINT */                                                \
+                }                                                                                  \
+            };                                                                                     \
+            static int INTERNAL_CATCH_UNIQUE_NAME(globalRegistrar) = []() {                        \
+                TestNameClass<INTERNAL_CATCH_MAKE_TYPE_LISTS_FROM_TYPES(__VA_ARGS__)>();           \
+                return 0;                                                                          \
+            }();                                                                                   \
+        }                                                                                          \
+    }                                                                                              \
+    CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION                                                       \
+    INTERNAL_CATCH_DEFINE_SIG_TEST_METHOD(TestName, INTERNAL_CATCH_REMOVE_PARENS(Signature))
+
+#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#define INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD(ClassName, Name, Tags, ...)                       \
+    INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_2(                                                    \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____C_L_A_S_S____),  \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        ClassName,                                                                                 \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        typename T,                                                                                \
+        __VA_ARGS__)
+#else
+#define INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD(ClassName, Name, Tags, ...)                       \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_2(                        \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____C_L_A_S_S____),  \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        ClassName,                                                                                 \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        typename T,                                                                                \
+        __VA_ARGS__))
+#endif
+
+#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#define INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG(ClassName, Name, Tags, Signature, ...)        \
+    INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_2(                                                    \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____C_L_A_S_S____),  \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        ClassName,                                                                                 \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        Signature,                                                                                 \
+        __VA_ARGS__)
+#else
+#define INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG(ClassName, Name, Tags, Signature, ...)        \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_2(                        \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____C_L_A_S_S____),  \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        ClassName,                                                                                 \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        Signature,                                                                                 \
+        __VA_ARGS__))
+#endif
+
+#define INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_2(                                        \
+    TestNameClass, TestName, ClassName, Name, Tags, Signature, TmplTypes, TypesList)               \
+    CATCH_INTERNAL_START_WARNINGS_SUPPRESSION                                                      \
+    CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS                                                       \
+    CATCH_INTERNAL_SUPPRESS_ZERO_VARIADIC_WARNINGS                                                 \
+    CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS                                               \
+    template <typename TestType>                                                                   \
+    struct TestName : INTERNAL_CATCH_REMOVE_PARENS(ClassName<TestType>) {                          \
+        void test();                                                                               \
+    };                                                                                             \
+    namespace {                                                                                    \
+        namespace INTERNAL_CATCH_MAKE_NAMESPACE(TestNameClass) {                                   \
+            INTERNAL_CATCH_TYPE_GEN                                                                \
+            INTERNAL_CATCH_NTTP_GEN(INTERNAL_CATCH_REMOVE_PARENS(Signature))                       \
+            template <typename... Types> struct TestNameClass {                                    \
+                void reg_tests() {                                                                 \
+                    int index = 0;                                                                 \
+                    using expander = int[];                                                        \
+                    constexpr char const* tmpl_types[] = {                                         \
+                        CATCH_REC_LIST(INTERNAL_CATCH_STRINGIZE_WITHOUT_PARENS,                    \
+                                       INTERNAL_CATCH_REMOVE_PARENS(TmplTypes))};                  \
+                    constexpr char const* types_list[] = {                                         \
+                        CATCH_REC_LIST(INTERNAL_CATCH_STRINGIZE_WITHOUT_PARENS,                    \
+                                       INTERNAL_CATCH_REMOVE_PARENS(TypesList))};                  \
+                    constexpr auto num_types = sizeof(types_list) / sizeof(types_list[0]);         \
+                    (void)expander{                                                                \
+                        (Catch::AutoReg(Catch::makeTestInvoker(&TestName<Types>::test),            \
+                                        CATCH_INTERNAL_LINEINFO,                                   \
+                                        #ClassName,                                                \
+                                        Catch::NameAndTags{                                        \
+                                            Name " - " +                                           \
+                                                std::string(tmpl_types[index / num_types]) + "<" + \
+                                                std::string(types_list[index % num_types]) + ">",  \
+                                            Tags}),                                                \
+                         index++)...}; /* NOLINT */                                                \
+                }                                                                                  \
+            };                                                                                     \
+            static int INTERNAL_CATCH_UNIQUE_NAME(globalRegistrar) = []() {                        \
+                using TestInit = typename create<                                                  \
+                    TestNameClass,                                                                 \
+                    decltype(get_wrapper<INTERNAL_CATCH_REMOVE_PARENS(TmplTypes)>()),              \
+                    TypeList<INTERNAL_CATCH_MAKE_TYPE_LISTS_FROM_TYPES(                            \
+                        INTERNAL_CATCH_REMOVE_PARENS(TypesList))>>::type;                          \
+                TestInit t;                                                                        \
+                t.reg_tests();                                                                     \
+                return 0;                                                                          \
+            }();                                                                                   \
+        }                                                                                          \
+    }                                                                                              \
+    CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION                                                       \
+    template <typename TestType> void TestName<TestType>::test()
+
+#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#define INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD(ClassName, Name, Tags, ...)               \
+    INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_2(                                            \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____F_U_N_C____),    \
+        ClassName,                                                                                 \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        typename T,                                                                                \
+        __VA_ARGS__)
+#else
+#define INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD(ClassName, Name, Tags, ...)               \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_2(                \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____F_U_N_C____),    \
+        ClassName,                                                                                 \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        typename T,                                                                                \
+        __VA_ARGS__))
+#endif
+
+#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#define INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG(                                      \
+    ClassName, Name, Tags, Signature, ...)                                                         \
+    INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_2(                                            \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____F_U_N_C____),    \
+        ClassName,                                                                                 \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        Signature,                                                                                 \
+        __VA_ARGS__)
+#else
+#define INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG(                                      \
+    ClassName, Name, Tags, Signature, ...)                                                         \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_2(                \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____F_U_N_C____),    \
+        ClassName,                                                                                 \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        Signature,                                                                                 \
+        __VA_ARGS__))
+#endif
+
+#define INTERNAL_CATCH_TEMPLATE_LIST_TEST_CASE_METHOD_2(                                           \
+    TestNameClass, TestName, ClassName, Name, Tags, TmplList)                                      \
+    CATCH_INTERNAL_START_WARNINGS_SUPPRESSION                                                      \
+    CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS                                                       \
+    CATCH_INTERNAL_SUPPRESS_UNUSED_TEMPLATE_WARNINGS                                               \
+    template <typename TestType>                                                                   \
+    struct TestName : INTERNAL_CATCH_REMOVE_PARENS(ClassName<TestType>) {                          \
+        void test();                                                                               \
+    };                                                                                             \
+    namespace {                                                                                    \
+        namespace INTERNAL_CATCH_MAKE_NAMESPACE(TestName) {                                        \
+            INTERNAL_CATCH_TYPE_GEN                                                                \
+            template <typename... Types> struct TestNameClass {                                    \
+                void reg_tests() {                                                                 \
+                    int index = 0;                                                                 \
+                    using expander = int[];                                                        \
+                    (void)expander{                                                                \
+                        (Catch::AutoReg(Catch::makeTestInvoker(&TestName<Types>::test),            \
+                                        CATCH_INTERNAL_LINEINFO,                                   \
+                                        #ClassName,                                                \
+                                        Catch::NameAndTags{                                        \
+                                            Name " - " +                                           \
+                                                std::string(INTERNAL_CATCH_STRINGIZE(TmplList)) +  \
+                                                " - " + std::to_string(index),                     \
+                                            Tags}),                                                \
+                         index++)...}; /* NOLINT */                                                \
+                }                                                                                  \
+            };                                                                                     \
+            static int INTERNAL_CATCH_UNIQUE_NAME(globalRegistrar) = []() {                        \
+                using TestInit = typename convert<TestNameClass, TmplList>::type;                  \
+                TestInit t;                                                                        \
+                t.reg_tests();                                                                     \
+                return 0;                                                                          \
+            }();                                                                                   \
+        }                                                                                          \
+    }                                                                                              \
+    CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION                                                       \
+    template <typename TestType> void TestName<TestType>::test()
+
+#define INTERNAL_CATCH_TEMPLATE_LIST_TEST_CASE_METHOD(ClassName, Name, Tags, TmplList)             \
+    INTERNAL_CATCH_TEMPLATE_LIST_TEST_CASE_METHOD_2(                                               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____),               \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_M_P_L_A_T_E____T_E_S_T____F_U_N_C____),    \
+        ClassName,                                                                                 \
+        Name,                                                                                      \
+        Tags,                                                                                      \
+        TmplList)
+
+// end catch_test_registry.h
+// start catch_capture.hpp
+
+// start catch_assertionhandler.h
+
+// start catch_assertioninfo.h
+
+// start catch_result_type.h
+
+namespace Catch {
+
+    // ResultWas::OfType enum
+    struct ResultWas {
+        enum OfType {
+            Unknown = -1,
+            Ok = 0,
+            Info = 1,
+            Warning = 2,
+
+            FailureBit = 0x10,
+
+            ExpressionFailed = FailureBit | 1,
+            ExplicitFailure = FailureBit | 2,
+
+            Exception = 0x100 | FailureBit,
+
+            ThrewException = Exception | 1,
+            DidntThrowException = Exception | 2,
+
+            FatalErrorCondition = 0x200 | FailureBit
+
+        };
+    };
+
+    bool isOk(ResultWas::OfType resultType);
+    bool isJustInfo(int flags);
+
+    // ResultDisposition::Flags enum
+    struct ResultDisposition {
+        enum Flags {
+            Normal = 0x01,
+
+            ContinueOnFailure = 0x02, // Failures fail test, but execution continues
+            FalseTest = 0x04,         // Prefix expression with !
+            SuppressFail = 0x08       // Failures are reported but do not fail the test
+        };
+    };
+
+    ResultDisposition::Flags operator|(ResultDisposition::Flags lhs, ResultDisposition::Flags rhs);
+
+    bool shouldContinueOnFailure(int flags);
+    inline bool isFalseTest(int flags) { return (flags & ResultDisposition::FalseTest) != 0; }
+    bool shouldSuppressFailure(int flags);
+
+} // end namespace Catch
+
+// end catch_result_type.h
+namespace Catch {
+
+    struct AssertionInfo {
+        StringRef macroName;
+        SourceLineInfo lineInfo;
+        StringRef capturedExpression;
+        ResultDisposition::Flags resultDisposition;
+
+        // We want to delete this constructor but a compiler bug in 4.8 means
+        // the struct is then treated as non-aggregate
+        // AssertionInfo() = delete;
+    };
+
+} // end namespace Catch
+
+// end catch_assertioninfo.h
+// start catch_decomposer.h
+
+// start catch_tostring.h
+
+#include <vector>
+#include <cstddef>
+#include <type_traits>
+#include <string>
+// start catch_stream.h
+
+#include <iosfwd>
+#include <cstddef>
+#include <ostream>
+
+namespace Catch {
+
+    std::ostream& cout();
+    std::ostream& cerr();
+    std::ostream& clog();
+
+    class StringRef;
+
+    struct IStream {
+        virtual ~IStream();
+        virtual std::ostream& stream() const = 0;
+    };
+
+    auto makeStream(StringRef const& filename) -> IStream const*;
+
+    class ReusableStringStream : NonCopyable {
+        std::size_t m_index;
+        std::ostream* m_oss;
+
+    public:
+        ReusableStringStream();
+        ~ReusableStringStream();
+
+        auto str() const -> std::string;
+
+        template <typename T> auto operator<<(T const& value) -> ReusableStringStream& {
+            *m_oss << value;
+            return *this;
+        }
+        auto get() -> std::ostream& { return *m_oss; }
+    };
+} // namespace Catch
+
+// end catch_stream.h
+// start catch_interfaces_enum_values_registry.h
+
+#include <vector>
+
+namespace Catch {
+
+    namespace Detail {
+        struct EnumInfo {
+            StringRef m_name;
+            std::vector<std::pair<int, StringRef>> m_values;
+
+            ~EnumInfo();
+
+            StringRef lookup(int value) const;
+        };
+    } // namespace Detail
+
+    struct IMutableEnumValuesRegistry {
+        virtual ~IMutableEnumValuesRegistry();
+
+        virtual Detail::EnumInfo const&
+        registerEnum(StringRef enumName, StringRef allEnums, std::vector<int> const& values) = 0;
+
+        template <typename E>
+        Detail::EnumInfo const&
+        registerEnum(StringRef enumName, StringRef allEnums, std::initializer_list<E> values) {
+            static_assert(sizeof(int) >= sizeof(E), "Cannot serialize enum to int");
+            std::vector<int> intValues;
+            intValues.reserve(values.size());
+            for (auto enumValue : values)
+                intValues.push_back(static_cast<int>(enumValue));
+            return registerEnum(enumName, allEnums, intValues);
+        }
+    };
+
+} // namespace Catch
+
+// end catch_interfaces_enum_values_registry.h
+
+#ifdef CATCH_CONFIG_CPP17_STRING_VIEW
+#include <string_view>
+#endif
+
+#ifdef __OBJC__
+// start catch_objc_arc.hpp
+
+#import <Foundation/Foundation.h>
+
+#ifdef __has_feature
+#define CATCH_ARC_ENABLED __has_feature(objc_arc)
+#else
+#define CATCH_ARC_ENABLED 0
+#endif
+
+void arcSafeRelease(NSObject* obj);
+id performOptionalSelector(id obj, SEL sel);
+
+#if !CATCH_ARC_ENABLED
+inline void arcSafeRelease(NSObject* obj) { [obj release]; }
+inline id performOptionalSelector(id obj, SEL sel) {
+    if ([obj respondsToSelector:sel])
+        return [obj performSelector:sel];
+    return nil;
+}
+#define CATCH_UNSAFE_UNRETAINED
+#define CATCH_ARC_STRONG
+#else
+inline void arcSafeRelease(NSObject*) {}
+inline id performOptionalSelector(id obj, SEL sel) {
+#ifdef __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+#endif
+    if ([obj respondsToSelector:sel])
+        return [obj performSelector:sel];
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
+    return nil;
+}
+#define CATCH_UNSAFE_UNRETAINED __unsafe_unretained
+#define CATCH_ARC_STRONG __strong
+#endif
+
+// end catch_objc_arc.hpp
+#endif
+
+#ifdef _MSC_VER
+#pragma warning(push)
+#pragma warning(disable : 4180) // We attempt to stream a function (address) by const&,
+                                // which MSVC complains about but is harmless
+#endif
+
+namespace Catch {
+    namespace Detail {
+
+        extern const std::string unprintableString;
+
+        std::string rawMemoryToString(const void* object, std::size_t size);
+
+        template <typename T> std::string rawMemoryToString(const T& object) {
+            return rawMemoryToString(&object, sizeof(object));
+        }
+
+        template <typename T> class IsStreamInsertable {
+            template <typename Stream, typename U>
+            static auto test(int)
+                -> decltype(std::declval<Stream&>() << std::declval<U>(), std::true_type());
+
+            template <typename, typename> static auto test(...) -> std::false_type;
+
+        public:
+            static const bool value = decltype(test<std::ostream, const T&>(0))::value;
+        };
+
+        template <typename E> std::string convertUnknownEnumToString(E e);
+
+        template <typename T>
+        typename std::enable_if<!std::is_enum<T>::value &&
+                                    !std::is_base_of<std::exception, T>::value,
+                                std::string>::type
+        convertUnstreamable(T const&) {
+            return Detail::unprintableString;
+        }
+        template <typename T>
+        typename std::enable_if<!std::is_enum<T>::value &&
+                                    std::is_base_of<std::exception, T>::value,
+                                std::string>::type
+        convertUnstreamable(T const& ex) {
+            return ex.what();
+        }
+
+        template <typename T>
+        typename std::enable_if<std::is_enum<T>::value, std::string>::type
+        convertUnstreamable(T const& value) {
+            return convertUnknownEnumToString(value);
+        }
+
+#if defined(_MANAGED)
+        //! Convert a CLR string to a utf8 std::string
+        template <typename T> std::string clrReferenceToString(T ^ ref) {
+            if (ref == nullptr)
+                return std::string("null");
+            auto bytes = System::Text::Encoding::UTF8->GetBytes(ref->ToString());
+            cli::pin_ptr<System::Byte> p = &bytes[0];
+            return std::string(reinterpret_cast<char const*>(p), bytes->Length);
+        }
+#endif
+
+    } // namespace Detail
+
+    // If we decide for C++14, change these to enable_if_ts
+    template <typename T, typename = void> struct StringMaker {
+        template <typename Fake = T>
+        static typename std::enable_if<::Catch::Detail::IsStreamInsertable<Fake>::value,
+                                       std::string>::type
+        convert(const Fake& value) {
+            ReusableStringStream rss;
+            // NB: call using the function-like syntax to avoid ambiguity with
+            // user-defined templated operator<< under clang.
+            rss.operator<<(value);
+            return rss.str();
+        }
+
+        template <typename Fake = T>
+        static typename std::enable_if<!::Catch::Detail::IsStreamInsertable<Fake>::value,
+                                       std::string>::type
+        convert(const Fake& value) {
+#if !defined(CATCH_CONFIG_FALLBACK_STRINGIFIER)
+            return Detail::convertUnstreamable(value);
+#else
+            return CATCH_CONFIG_FALLBACK_STRINGIFIER(value);
+#endif
+        }
+    };
+
+    namespace Detail {
+
+        // This function dispatches all stringification requests inside of
+        // Catch. Should be preferably called fully qualified, like
+        // ::Catch::Detail::stringify
+        template <typename T> std::string stringify(const T& e) {
+            return ::Catch::StringMaker<
+                typename std::remove_cv<typename std::remove_reference<T>::type>::type>::convert(e);
+        }
+
+        template <typename E> std::string convertUnknownEnumToString(E e) {
+            return ::Catch::Detail::stringify(
+                static_cast<typename std::underlying_type<E>::type>(e));
+        }
+
+#if defined(_MANAGED)
+        template <typename T> std::string stringify(T ^ e) {
+            return ::Catch::StringMaker<T ^>::convert(e);
+        }
+#endif
+
+    } // namespace Detail
+
+    // Some predefined specializations
+
+    template <> struct StringMaker<std::string> {
+        static std::string convert(const std::string& str);
+    };
+
+#ifdef CATCH_CONFIG_CPP17_STRING_VIEW
+    template <> struct StringMaker<std::string_view> {
+        static std::string convert(std::string_view str);
+    };
+#endif
+
+    template <> struct StringMaker<char const*> { static std::string convert(char const* str); };
+    template <> struct StringMaker<char*> { static std::string convert(char* str); };
+
+#ifdef CATCH_CONFIG_WCHAR
+    template <> struct StringMaker<std::wstring> {
+        static std::string convert(const std::wstring& wstr);
+    };
+
+#ifdef CATCH_CONFIG_CPP17_STRING_VIEW
+    template <> struct StringMaker<std::wstring_view> {
+        static std::string convert(std::wstring_view str);
+    };
+#endif
+
+    template <> struct StringMaker<wchar_t const*> {
+        static std::string convert(wchar_t const* str);
+    };
+    template <> struct StringMaker<wchar_t*> { static std::string convert(wchar_t* str); };
+#endif
+
+    // TBD: Should we use `strnlen` to ensure that we don't go out of the
+    // buffer,
+    //      while keeping string semantics?
+    template <int SZ> struct StringMaker<char[SZ]> {
+        static std::string convert(char const* str) {
+            return ::Catch::Detail::stringify(std::string{str});
+        }
+    };
+    template <int SZ> struct StringMaker<signed char[SZ]> {
+        static std::string convert(signed char const* str) {
+            return ::Catch::Detail::stringify(std::string{reinterpret_cast<char const*>(str)});
+        }
+    };
+    template <int SZ> struct StringMaker<unsigned char[SZ]> {
+        static std::string convert(unsigned char const* str) {
+            return ::Catch::Detail::stringify(std::string{reinterpret_cast<char const*>(str)});
+        }
+    };
+
+#if defined(CATCH_CONFIG_CPP17_BYTE)
+    template <> struct StringMaker<std::byte> { static std::string convert(std::byte value); };
+#endif // defined(CATCH_CONFIG_CPP17_BYTE)
+    template <> struct StringMaker<int> { static std::string convert(int value); };
+    template <> struct StringMaker<long> { static std::string convert(long value); };
+    template <> struct StringMaker<long long> { static std::string convert(long long value); };
+    template <> struct StringMaker<unsigned int> {
+        static std::string convert(unsigned int value);
+    };
+    template <> struct StringMaker<unsigned long> {
+        static std::string convert(unsigned long value);
+    };
+    template <> struct StringMaker<unsigned long long> {
+        static std::string convert(unsigned long long value);
+    };
+
+    template <> struct StringMaker<bool> { static std::string convert(bool b); };
+
+    template <> struct StringMaker<char> { static std::string convert(char c); };
+    template <> struct StringMaker<signed char> { static std::string convert(signed char c); };
+    template <> struct StringMaker<unsigned char> { static std::string convert(unsigned char c); };
+
+    template <> struct StringMaker<std::nullptr_t> { static std::string convert(std::nullptr_t); };
+
+    template <> struct StringMaker<float> {
+        static std::string convert(float value);
+        static int precision;
+    };
+
+    template <> struct StringMaker<double> {
+        static std::string convert(double value);
+        static int precision;
+    };
+
+    template <typename T> struct StringMaker<T*> {
+        template <typename U> static std::string convert(U* p) {
+            if (p) {
+                return ::Catch::Detail::rawMemoryToString(p);
+            } else {
+                return "nullptr";
+            }
+        }
+    };
+
+    template <typename R, typename C> struct StringMaker<R C::*> {
+        static std::string convert(R C::*p) {
+            if (p) {
+                return ::Catch::Detail::rawMemoryToString(p);
+            } else {
+                return "nullptr";
+            }
+        }
+    };
+
+#if defined(_MANAGED)
+    template <typename T> struct StringMaker<T ^> {
+        static std::string convert(T ^ ref) { return ::Catch::Detail::clrReferenceToString(ref); }
+    };
+#endif
+
+    namespace Detail {
+        template <typename InputIterator, typename Sentinel = InputIterator>
+        std::string rangeToString(InputIterator first, Sentinel last) {
+            ReusableStringStream rss;
+            rss << "{ ";
+            if (first != last) {
+                rss << ::Catch::Detail::stringify(*first);
+                for (++first; first != last; ++first)
+                    rss << ", " << ::Catch::Detail::stringify(*first);
+            }
+            rss << " }";
+            return rss.str();
+        }
+    } // namespace Detail
+
+#ifdef __OBJC__
+    template <> struct StringMaker<NSString*> {
+        static std::string convert(NSString* nsstring) {
+            if (!nsstring)
+                return "nil";
+            return std::string("@") + [nsstring UTF8String];
+        }
+    };
+    template <> struct StringMaker<NSObject*> {
+        static std::string convert(NSObject* nsObject) {
+            return ::Catch::Detail::stringify([nsObject description]);
+        }
+    };
+    namespace Detail {
+        inline std::string stringify(NSString* nsstring) {
+            return StringMaker<NSString*>::convert(nsstring);
+        }
+
+    }  // namespace Detail
+#endif // __OBJC__
+
+} // namespace Catch
+
+//////////////////////////////////////////////////////
+// Separate std-lib types stringification, so it can be selectively enabled
+// This means that we do not bring in
+
+#if defined(CATCH_CONFIG_ENABLE_ALL_STRINGMAKERS)
+#define CATCH_CONFIG_ENABLE_PAIR_STRINGMAKER
+#define CATCH_CONFIG_ENABLE_TUPLE_STRINGMAKER
+#define CATCH_CONFIG_ENABLE_VARIANT_STRINGMAKER
+#define CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER
+#define CATCH_CONFIG_ENABLE_OPTIONAL_STRINGMAKER
+#endif
+
+// Separate std::pair specialization
+#if defined(CATCH_CONFIG_ENABLE_PAIR_STRINGMAKER)
+#include <utility>
+namespace Catch {
+    template <typename T1, typename T2> struct StringMaker<std::pair<T1, T2>> {
+        static std::string convert(const std::pair<T1, T2>& pair) {
+            ReusableStringStream rss;
+            rss << "{ " << ::Catch::Detail::stringify(pair.first) << ", "
+                << ::Catch::Detail::stringify(pair.second) << " }";
+            return rss.str();
+        }
+    };
+} // namespace Catch
+#endif // CATCH_CONFIG_ENABLE_PAIR_STRINGMAKER
+
+#if defined(CATCH_CONFIG_ENABLE_OPTIONAL_STRINGMAKER) && defined(CATCH_CONFIG_CPP17_OPTIONAL)
+#include <optional>
+namespace Catch {
+    template <typename T> struct StringMaker<std::optional<T>> {
+        static std::string convert(const std::optional<T>& optional) {
+            ReusableStringStream rss;
+            if (optional.has_value()) {
+                rss << ::Catch::Detail::stringify(*optional);
+            } else {
+                rss << "{ }";
+            }
+            return rss.str();
+        }
+    };
+} // namespace Catch
+#endif // CATCH_CONFIG_ENABLE_OPTIONAL_STRINGMAKER
+
+// Separate std::tuple specialization
+#if defined(CATCH_CONFIG_ENABLE_TUPLE_STRINGMAKER)
+#include <tuple>
+namespace Catch {
+    namespace Detail {
+        template <typename Tuple, std::size_t N = 0, bool = (N < std::tuple_size<Tuple>::value)>
+        struct TupleElementPrinter {
+            static void print(const Tuple& tuple, std::ostream& os) {
+                os << (N ? ", " : " ") << ::Catch::Detail::stringify(std::get<N>(tuple));
+                TupleElementPrinter<Tuple, N + 1>::print(tuple, os);
+            }
+        };
+
+        template <typename Tuple, std::size_t N> struct TupleElementPrinter<Tuple, N, false> {
+            static void print(const Tuple&, std::ostream&) {}
+        };
+
+    } // namespace Detail
+
+    template <typename... Types> struct StringMaker<std::tuple<Types...>> {
+        static std::string convert(const std::tuple<Types...>& tuple) {
+            ReusableStringStream rss;
+            rss << '{';
+            Detail::TupleElementPrinter<std::tuple<Types...>>::print(tuple, rss.get());
+            rss << " }";
+            return rss.str();
+        }
+    };
+} // namespace Catch
+#endif // CATCH_CONFIG_ENABLE_TUPLE_STRINGMAKER
+
+#if defined(CATCH_CONFIG_ENABLE_VARIANT_STRINGMAKER) && defined(CATCH_CONFIG_CPP17_VARIANT)
+#include <variant>
+namespace Catch {
+    template <> struct StringMaker<std::monostate> {
+        static std::string convert(const std::monostate&) { return "{ }"; }
+    };
+
+    template <typename... Elements> struct StringMaker<std::variant<Elements...>> {
+        static std::string convert(const std::variant<Elements...>& variant) {
+            if (variant.valueless_by_exception()) {
+                return "{valueless variant}";
+            } else {
+                return std::visit(
+                    [](const auto& value) { return ::Catch::Detail::stringify(value); }, variant);
+            }
+        }
+    };
+} // namespace Catch
+#endif // CATCH_CONFIG_ENABLE_VARIANT_STRINGMAKER
+
+namespace Catch {
+    // Import begin/ end from std here
+    using std::begin;
+    using std::end;
+
+    namespace detail {
+        template <typename...> struct void_type { using type = void; };
+
+        template <typename T, typename = void> struct is_range_impl : std::false_type {};
+
+        template <typename T>
+        struct is_range_impl<T, typename void_type<decltype(begin(std::declval<T>()))>::type>
+            : std::true_type {};
+    } // namespace detail
+
+    template <typename T> struct is_range : detail::is_range_impl<T> {};
+
+#if defined(_MANAGED) // Managed types are never ranges
+    template <typename T> struct is_range<T ^> { static const bool value = false; };
+#endif
+
+    template <typename Range> std::string rangeToString(Range const& range) {
+        return ::Catch::Detail::rangeToString(begin(range), end(range));
+    }
+
+    // Handle vector<bool> specially
+    template <typename Allocator> std::string rangeToString(std::vector<bool, Allocator> const& v) {
+        ReusableStringStream rss;
+        rss << "{ ";
+        bool first = true;
+        for (bool b : v) {
+            if (first)
+                first = false;
+            else
+                rss << ", ";
+            rss << ::Catch::Detail::stringify(b);
+        }
+        rss << " }";
+        return rss.str();
+    }
+
+    template <typename R>
+    struct StringMaker<
+        R,
+        typename std::enable_if<is_range<R>::value &&
+                                !::Catch::Detail::IsStreamInsertable<R>::value>::type> {
+        static std::string convert(R const& range) { return rangeToString(range); }
+    };
+
+    template <typename T, int SZ> struct StringMaker<T[SZ]> {
+        static std::string convert(T const (&arr)[SZ]) { return rangeToString(arr); }
+    };
+
+} // namespace Catch
+
+// Separate std::chrono::duration specialization
+#if defined(CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER)
+#include <ctime>
+#include <ratio>
+#include <chrono>
+
+namespace Catch {
+
+    template <class Ratio> struct ratio_string { static std::string symbol(); };
+
+    template <class Ratio> std::string ratio_string<Ratio>::symbol() {
+        Catch::ReusableStringStream rss;
+        rss << '[' << Ratio::num << '/' << Ratio::den << ']';
+        return rss.str();
+    }
+    template <> struct ratio_string<std::atto> { static std::string symbol(); };
+    template <> struct ratio_string<std::femto> { static std::string symbol(); };
+    template <> struct ratio_string<std::pico> { static std::string symbol(); };
+    template <> struct ratio_string<std::nano> { static std::string symbol(); };
+    template <> struct ratio_string<std::micro> { static std::string symbol(); };
+    template <> struct ratio_string<std::milli> { static std::string symbol(); };
+
+    ////////////
+    // std::chrono::duration specializations
+    template <typename Value, typename Ratio>
+    struct StringMaker<std::chrono::duration<Value, Ratio>> {
+        static std::string convert(std::chrono::duration<Value, Ratio> const& duration) {
+            ReusableStringStream rss;
+            rss << duration.count() << ' ' << ratio_string<Ratio>::symbol() << 's';
+            return rss.str();
+        }
+    };
+    template <typename Value> struct StringMaker<std::chrono::duration<Value, std::ratio<1>>> {
+        static std::string convert(std::chrono::duration<Value, std::ratio<1>> const& duration) {
+            ReusableStringStream rss;
+            rss << duration.count() << " s";
+            return rss.str();
+        }
+    };
+    template <typename Value> struct StringMaker<std::chrono::duration<Value, std::ratio<60>>> {
+        static std::string convert(std::chrono::duration<Value, std::ratio<60>> const& duration) {
+            ReusableStringStream rss;
+            rss << duration.count() << " m";
+            return rss.str();
+        }
+    };
+    template <typename Value> struct StringMaker<std::chrono::duration<Value, std::ratio<3600>>> {
+        static std::string convert(std::chrono::duration<Value, std::ratio<3600>> const& duration) {
+            ReusableStringStream rss;
+            rss << duration.count() << " h";
+            return rss.str();
+        }
+    };
+
+    ////////////
+    // std::chrono::time_point specialization
+    // Generic time_point cannot be specialized, only
+    // std::chrono::time_point<system_clock>
+    template <typename Clock, typename Duration>
+    struct StringMaker<std::chrono::time_point<Clock, Duration>> {
+        static std::string convert(std::chrono::time_point<Clock, Duration> const& time_point) {
+            return ::Catch::Detail::stringify(time_point.time_since_epoch()) + " since epoch";
+        }
+    };
+    // std::chrono::time_point<system_clock> specialization
+    template <typename Duration>
+    struct StringMaker<std::chrono::time_point<std::chrono::system_clock, Duration>> {
+        static std::string
+        convert(std::chrono::time_point<std::chrono::system_clock, Duration> const& time_point) {
+            auto converted = std::chrono::system_clock::to_time_t(time_point);
+
+#ifdef _MSC_VER
+            std::tm timeInfo = {};
+            gmtime_s(&timeInfo, &converted);
+#else
+            std::tm* timeInfo = std::gmtime(&converted);
+#endif
+
+            auto const timeStampSize = sizeof("2017-01-16T17:06:45Z");
+            char timeStamp[timeStampSize];
+            const char* const fmt = "%Y-%m-%dT%H:%M:%SZ";
+
+#ifdef _MSC_VER
+            std::strftime(timeStamp, timeStampSize, fmt, &timeInfo);
+#else
+            std::strftime(timeStamp, timeStampSize, fmt, timeInfo);
+#endif
+            return std::string(timeStamp);
+        }
+    };
+} // namespace Catch
+#endif // CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER
+
+#define INTERNAL_CATCH_REGISTER_ENUM(enumName, ...)                                                \
+    namespace Catch {                                                                              \
+        template <> struct StringMaker<enumName> {                                                 \
+            static std::string convert(enumName value) {                                           \
+                static const auto& enumInfo =                                                      \
+                    ::Catch::getMutableRegistryHub().getMutableEnumValuesRegistry().registerEnum(  \
+                        #enumName, #__VA_ARGS__, {__VA_ARGS__});                                   \
+                return static_cast<std::string>(enumInfo.lookup(static_cast<int>(value)));         \
+            }                                                                                      \
+        };                                                                                         \
+    }
+
+#define CATCH_REGISTER_ENUM(enumName, ...) INTERNAL_CATCH_REGISTER_ENUM(enumName, __VA_ARGS__)
+
+#ifdef _MSC_VER
+#pragma warning(pop)
+#endif
+
+// end catch_tostring.h
+#include <iosfwd>
+
+#ifdef _MSC_VER
+#pragma warning(push)
+#pragma warning(disable : 4389) // '==' : signed/unsigned mismatch
+#pragma warning(disable : 4018) // more "signed/unsigned mismatch"
+#pragma warning(disable : 4312) // Converting int to T* using reinterpret_cast
+                                // (issue on x64 platform)
+#pragma warning(disable : 4180) // qualifier applied to function type has no meaning
+#pragma warning(disable : 4800) // Forcing result to true or false
+#endif
+
+namespace Catch {
+
+    struct ITransientExpression {
+        auto isBinaryExpression() const -> bool { return m_isBinaryExpression; }
+        auto getResult() const -> bool { return m_result; }
+        virtual void streamReconstructedExpression(std::ostream& os) const = 0;
+
+        ITransientExpression(bool isBinaryExpression, bool result) :
+            m_isBinaryExpression(isBinaryExpression), m_result(result) {}
+
+        // We don't actually need a virtual destructor, but many static
+        // analysers complain if it's not here :-(
+        virtual ~ITransientExpression();
+
+        bool m_isBinaryExpression;
+        bool m_result;
+    };
+
+    void formatReconstructedExpression(std::ostream& os,
+                                       std::string const& lhs,
+                                       StringRef op,
+                                       std::string const& rhs);
+
+    template <typename LhsT, typename RhsT> class BinaryExpr : public ITransientExpression {
+        LhsT m_lhs;
+        StringRef m_op;
+        RhsT m_rhs;
+
+        void streamReconstructedExpression(std::ostream& os) const override {
+            formatReconstructedExpression(
+                os, Catch::Detail::stringify(m_lhs), m_op, Catch::Detail::stringify(m_rhs));
+        }
+
+    public:
+        BinaryExpr(bool comparisonResult, LhsT lhs, StringRef op, RhsT rhs) :
+            ITransientExpression{true, comparisonResult}, m_lhs(lhs), m_op(op), m_rhs(rhs) {}
+
+        template <typename T> auto operator&&(T) const -> BinaryExpr<LhsT, RhsT const&> const {
+            static_assert(always_false<T>::value,
+                          "chained comparisons are not supported inside assertions, "
+                          "wrap the expression inside parentheses, or decompose it");
+        }
+
+        template <typename T> auto operator||(T) const -> BinaryExpr<LhsT, RhsT const&> const {
+            static_assert(always_false<T>::value,
+                          "chained comparisons are not supported inside assertions, "
+                          "wrap the expression inside parentheses, or decompose it");
+        }
+
+        template <typename T> auto operator==(T) const -> BinaryExpr<LhsT, RhsT const&> const {
+            static_assert(always_false<T>::value,
+                          "chained comparisons are not supported inside assertions, "
+                          "wrap the expression inside parentheses, or decompose it");
+        }
+
+        template <typename T> auto operator!=(T) const -> BinaryExpr<LhsT, RhsT const&> const {
+            static_assert(always_false<T>::value,
+                          "chained comparisons are not supported inside assertions, "
+                          "wrap the expression inside parentheses, or decompose it");
+        }
+
+        template <typename T> auto operator>(T) const -> BinaryExpr<LhsT, RhsT const&> const {
+            static_assert(always_false<T>::value,
+                          "chained comparisons are not supported inside assertions, "
+                          "wrap the expression inside parentheses, or decompose it");
+        }
+
+        template <typename T> auto operator<(T) const -> BinaryExpr<LhsT, RhsT const&> const {
+            static_assert(always_false<T>::value,
+                          "chained comparisons are not supported inside assertions, "
+                          "wrap the expression inside parentheses, or decompose it");
+        }
+
+        template <typename T> auto operator>=(T) const -> BinaryExpr<LhsT, RhsT const&> const {
+            static_assert(always_false<T>::value,
+                          "chained comparisons are not supported inside assertions, "
+                          "wrap the expression inside parentheses, or decompose it");
+        }
+
+        template <typename T> auto operator<=(T) const -> BinaryExpr<LhsT, RhsT const&> const {
+            static_assert(always_false<T>::value,
+                          "chained comparisons are not supported inside assertions, "
+                          "wrap the expression inside parentheses, or decompose it");
+        }
+    };
+
+    template <typename LhsT> class UnaryExpr : public ITransientExpression {
+        LhsT m_lhs;
+
+        void streamReconstructedExpression(std::ostream& os) const override {
+            os << Catch::Detail::stringify(m_lhs);
+        }
+
+    public:
+        explicit UnaryExpr(LhsT lhs) :
+            ITransientExpression{false, static_cast<bool>(lhs)}, m_lhs(lhs) {}
+    };
+
+    // Specialised comparison functions to handle equality comparisons between
+    // ints and pointers (NULL deduces as an int)
+    template <typename LhsT, typename RhsT>
+    auto compareEqual(LhsT const& lhs, RhsT const& rhs) -> bool {
+        return static_cast<bool>(lhs == rhs);
+    }
+    template <typename T> auto compareEqual(T* const& lhs, int rhs) -> bool {
+        return lhs == reinterpret_cast<void const*>(rhs);
+    }
+    template <typename T> auto compareEqual(T* const& lhs, long rhs) -> bool {
+        return lhs == reinterpret_cast<void const*>(rhs);
+    }
+    template <typename T> auto compareEqual(int lhs, T* const& rhs) -> bool {
+        return reinterpret_cast<void const*>(lhs) == rhs;
+    }
+    template <typename T> auto compareEqual(long lhs, T* const& rhs) -> bool {
+        return reinterpret_cast<void const*>(lhs) == rhs;
+    }
+
+    template <typename LhsT, typename RhsT>
+    auto compareNotEqual(LhsT const& lhs, RhsT&& rhs) -> bool {
+        return static_cast<bool>(lhs != rhs);
+    }
+    template <typename T> auto compareNotEqual(T* const& lhs, int rhs) -> bool {
+        return lhs != reinterpret_cast<void const*>(rhs);
+    }
+    template <typename T> auto compareNotEqual(T* const& lhs, long rhs) -> bool {
+        return lhs != reinterpret_cast<void const*>(rhs);
+    }
+    template <typename T> auto compareNotEqual(int lhs, T* const& rhs) -> bool {
+        return reinterpret_cast<void const*>(lhs) != rhs;
+    }
+    template <typename T> auto compareNotEqual(long lhs, T* const& rhs) -> bool {
+        return reinterpret_cast<void const*>(lhs) != rhs;
+    }
+
+    template <typename LhsT> class ExprLhs {
+        LhsT m_lhs;
+
+    public:
+        explicit ExprLhs(LhsT lhs) : m_lhs(lhs) {}
+
+        template <typename RhsT>
+        auto operator==(RhsT const& rhs) -> BinaryExpr<LhsT, RhsT const&> const {
+            return {compareEqual(m_lhs, rhs), m_lhs, "==", rhs};
+        }
+        auto operator==(bool rhs) -> BinaryExpr<LhsT, bool> const {
+            return {m_lhs == rhs, m_lhs, "==", rhs};
+        }
+
+        template <typename RhsT>
+        auto operator!=(RhsT const& rhs) -> BinaryExpr<LhsT, RhsT const&> const {
+            return {compareNotEqual(m_lhs, rhs), m_lhs, "!=", rhs};
+        }
+        auto operator!=(bool rhs) -> BinaryExpr<LhsT, bool> const {
+            return {m_lhs != rhs, m_lhs, "!=", rhs};
+        }
+
+        template <typename RhsT>
+        auto operator>(RhsT const& rhs) -> BinaryExpr<LhsT, RhsT const&> const {
+            return {static_cast<bool>(m_lhs > rhs), m_lhs, ">", rhs};
+        }
+        template <typename RhsT>
+        auto operator<(RhsT const& rhs) -> BinaryExpr<LhsT, RhsT const&> const {
+            return {static_cast<bool>(m_lhs < rhs), m_lhs, "<", rhs};
+        }
+        template <typename RhsT>
+        auto operator>=(RhsT const& rhs) -> BinaryExpr<LhsT, RhsT const&> const {
+            return {static_cast<bool>(m_lhs >= rhs), m_lhs, ">=", rhs};
+        }
+        template <typename RhsT>
+        auto operator<=(RhsT const& rhs) -> BinaryExpr<LhsT, RhsT const&> const {
+            return {static_cast<bool>(m_lhs <= rhs), m_lhs, "<=", rhs};
+        }
+        template <typename RhsT>
+        auto operator|(RhsT const& rhs) -> BinaryExpr<LhsT, RhsT const&> const {
+            return {static_cast<bool>(m_lhs | rhs), m_lhs, "|", rhs};
+        }
+        template <typename RhsT>
+        auto operator&(RhsT const& rhs) -> BinaryExpr<LhsT, RhsT const&> const {
+            return {static_cast<bool>(m_lhs & rhs), m_lhs, "&", rhs};
+        }
+        template <typename RhsT>
+        auto operator^(RhsT const& rhs) -> BinaryExpr<LhsT, RhsT const&> const {
+            return {static_cast<bool>(m_lhs ^ rhs), m_lhs, "^", rhs};
+        }
+
+        template <typename RhsT>
+        auto operator&&(RhsT const&) -> BinaryExpr<LhsT, RhsT const&> const {
+            static_assert(always_false<RhsT>::value,
+                          "operator&& is not supported inside assertions, "
+                          "wrap the expression inside parentheses, or decompose it");
+        }
+
+        template <typename RhsT>
+        auto operator||(RhsT const&) -> BinaryExpr<LhsT, RhsT const&> const {
+            static_assert(always_false<RhsT>::value,
+                          "operator|| is not supported inside assertions, "
+                          "wrap the expression inside parentheses, or decompose it");
+        }
+
+        auto makeUnaryExpr() const -> UnaryExpr<LhsT> { return UnaryExpr<LhsT>{m_lhs}; }
+    };
+
+    void handleExpression(ITransientExpression const& expr);
+
+    template <typename T> void handleExpression(ExprLhs<T> const& expr) {
+        handleExpression(expr.makeUnaryExpr());
+    }
+
+    struct Decomposer {
+        template <typename T> auto operator<=(T const& lhs) -> ExprLhs<T const&> {
+            return ExprLhs<T const&>{lhs};
+        }
+
+        auto operator<=(bool value) -> ExprLhs<bool> { return ExprLhs<bool>{value}; }
+    };
+
+} // end namespace Catch
+
+#ifdef _MSC_VER
+#pragma warning(pop)
+#endif
+
+// end catch_decomposer.h
+// start catch_interfaces_capture.h
+
+#include <string>
+#include <chrono>
+
+namespace Catch {
+
+    class AssertionResult;
+    struct AssertionInfo;
+    struct SectionInfo;
+    struct SectionEndInfo;
+    struct MessageInfo;
+    struct MessageBuilder;
+    struct Counts;
+    struct AssertionReaction;
+    struct SourceLineInfo;
+
+    struct ITransientExpression;
+    struct IGeneratorTracker;
+
+#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING)
+    struct BenchmarkInfo;
+    template <typename Duration = std::chrono::duration<double, std::nano>> struct BenchmarkStats;
+#endif // CATCH_CONFIG_ENABLE_BENCHMARKING
+
+    struct IResultCapture {
+
+        virtual ~IResultCapture();
+
+        virtual bool sectionStarted(SectionInfo const& sectionInfo, Counts& assertions) = 0;
+        virtual void sectionEnded(SectionEndInfo const& endInfo) = 0;
+        virtual void sectionEndedEarly(SectionEndInfo const& endInfo) = 0;
+
+        virtual auto acquireGeneratorTracker(StringRef generatorName,
+                                             SourceLineInfo const& lineInfo)
+            -> IGeneratorTracker& = 0;
+
+#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING)
+        virtual void benchmarkPreparing(std::string const& name) = 0;
+        virtual void benchmarkStarting(BenchmarkInfo const& info) = 0;
+        virtual void benchmarkEnded(BenchmarkStats<> const& stats) = 0;
+        virtual void benchmarkFailed(std::string const& error) = 0;
+#endif // CATCH_CONFIG_ENABLE_BENCHMARKING
+
+        virtual void pushScopedMessage(MessageInfo const& message) = 0;
+        virtual void popScopedMessage(MessageInfo const& message) = 0;
+
+        virtual void emplaceUnscopedMessage(MessageBuilder const& builder) = 0;
+
+        virtual void handleFatalErrorCondition(StringRef message) = 0;
+
+        virtual void handleExpr(AssertionInfo const& info,
+                                ITransientExpression const& expr,
+                                AssertionReaction& reaction) = 0;
+        virtual void handleMessage(AssertionInfo const& info,
+                                   ResultWas::OfType resultType,
+                                   StringRef const& message,
+                                   AssertionReaction& reaction) = 0;
+        virtual void handleUnexpectedExceptionNotThrown(AssertionInfo const& info,
+                                                        AssertionReaction& reaction) = 0;
+        virtual void handleUnexpectedInflightException(AssertionInfo const& info,
+                                                       std::string const& message,
+                                                       AssertionReaction& reaction) = 0;
+        virtual void handleIncomplete(AssertionInfo const& info) = 0;
+        virtual void handleNonExpr(AssertionInfo const& info,
+                                   ResultWas::OfType resultType,
+                                   AssertionReaction& reaction) = 0;
+
+        virtual bool lastAssertionPassed() = 0;
+        virtual void assertionPassed() = 0;
+
+        // Deprecated, do not use:
+        virtual std::string getCurrentTestName() const = 0;
+        virtual const AssertionResult* getLastResult() const = 0;
+        virtual void exceptionEarlyReported() = 0;
+    };
+
+    IResultCapture& getResultCapture();
+} // namespace Catch
+
+// end catch_interfaces_capture.h
+namespace Catch {
+
+    struct TestFailureException {};
+    struct AssertionResultData;
+    struct IResultCapture;
+    class RunContext;
+
+    class LazyExpression {
+        friend class AssertionHandler;
+        friend struct AssertionStats;
+        friend class RunContext;
+
+        ITransientExpression const* m_transientExpression = nullptr;
+        bool m_isNegated;
+
+    public:
+        LazyExpression(bool isNegated);
+        LazyExpression(LazyExpression const& other);
+        LazyExpression& operator=(LazyExpression const&) = delete;
+
+        explicit operator bool() const;
+
+        friend auto operator<<(std::ostream& os, LazyExpression const& lazyExpr) -> std::ostream&;
+    };
+
+    struct AssertionReaction {
+        bool shouldDebugBreak = false;
+        bool shouldThrow = false;
+    };
+
+    class AssertionHandler {
+        AssertionInfo m_assertionInfo;
+        AssertionReaction m_reaction;
+        bool m_completed = false;
+        IResultCapture& m_resultCapture;
+
+    public:
+        AssertionHandler(StringRef const& macroName,
+                         SourceLineInfo const& lineInfo,
+                         StringRef capturedExpression,
+                         ResultDisposition::Flags resultDisposition);
+        ~AssertionHandler() {
+            if (!m_completed) {
+                m_resultCapture.handleIncomplete(m_assertionInfo);
+            }
+        }
+
+        template <typename T> void handleExpr(ExprLhs<T> const& expr) {
+            handleExpr(expr.makeUnaryExpr());
+        }
+        void handleExpr(ITransientExpression const& expr);
+
+        void handleMessage(ResultWas::OfType resultType, StringRef const& message);
+
+        void handleExceptionThrownAsExpected();
+        void handleUnexpectedExceptionNotThrown();
+        void handleExceptionNotThrownAsExpected();
+        void handleThrowingCallSkipped();
+        void handleUnexpectedInflightException();
+
+        void complete();
+        void setCompleted();
+
+        // query
+        auto allowThrows() const -> bool;
+    };
+
+    void handleExceptionMatchExpr(AssertionHandler& handler,
+                                  std::string const& str,
+                                  StringRef const& matcherString);
+
+} // namespace Catch
+
+// end catch_assertionhandler.h
+// start catch_message.h
+
+#include <string>
+#include <vector>
+
+namespace Catch {
+
+    struct MessageInfo {
+        MessageInfo(StringRef const& _macroName,
+                    SourceLineInfo const& _lineInfo,
+                    ResultWas::OfType _type);
+
+        StringRef macroName;
+        std::string message;
+        SourceLineInfo lineInfo;
+        ResultWas::OfType type;
+        unsigned int sequence;
+
+        bool operator==(MessageInfo const& other) const;
+        bool operator<(MessageInfo const& other) const;
+
+    private:
+        static unsigned int globalCount;
+    };
+
+    struct MessageStream {
+
+        template <typename T> MessageStream& operator<<(T const& value) {
+            m_stream << value;
+            return *this;
+        }
+
+        ReusableStringStream m_stream;
+    };
+
+    struct MessageBuilder : MessageStream {
+        MessageBuilder(StringRef const& macroName,
+                       SourceLineInfo const& lineInfo,
+                       ResultWas::OfType type);
+
+        template <typename T> MessageBuilder& operator<<(T const& value) {
+            m_stream << value;
+            return *this;
+        }
+
+        MessageInfo m_info;
+    };
+
+    class ScopedMessage {
+    public:
+        explicit ScopedMessage(MessageBuilder const& builder);
+        ScopedMessage(ScopedMessage& duplicate) = delete;
+        ScopedMessage(ScopedMessage&& old);
+        ~ScopedMessage();
+
+        MessageInfo m_info;
+        bool m_moved;
+    };
+
+    class Capturer {
+        std::vector<MessageInfo> m_messages;
+        IResultCapture& m_resultCapture = getResultCapture();
+        size_t m_captured = 0;
+
+    public:
+        Capturer(StringRef macroName,
+                 SourceLineInfo const& lineInfo,
+                 ResultWas::OfType resultType,
+                 StringRef names);
+        ~Capturer();
+
+        void captureValue(size_t index, std::string const& value);
+
+        template <typename T> void captureValues(size_t index, T const& value) {
+            captureValue(index, Catch::Detail::stringify(value));
+        }
+
+        template <typename T, typename... Ts>
+        void captureValues(size_t index, T const& value, Ts const&... values) {
+            captureValue(index, Catch::Detail::stringify(value));
+            captureValues(index + 1, values...);
+        }
+    };
+
+} // end namespace Catch
+
+// end catch_message.h
+#if !defined(CATCH_CONFIG_DISABLE)
+
+#if !defined(CATCH_CONFIG_DISABLE_STRINGIFICATION)
+#define CATCH_INTERNAL_STRINGIFY(...) #__VA_ARGS__
+#else
+#define CATCH_INTERNAL_STRINGIFY(...) "Disabled by CATCH_CONFIG_DISABLE_STRINGIFICATION"
+#endif
+
+#if defined(CATCH_CONFIG_FAST_COMPILE) || defined(CATCH_CONFIG_DISABLE_EXCEPTIONS)
+
+///////////////////////////////////////////////////////////////////////////////
+// Another way to speed-up compilation is to omit local try-catch for REQUIRE*
+// macros.
+#define INTERNAL_CATCH_TRY
+#define INTERNAL_CATCH_CATCH(capturer)
+
+#else // CATCH_CONFIG_FAST_COMPILE
+
+#define INTERNAL_CATCH_TRY try
+#define INTERNAL_CATCH_CATCH(handler)                                                              \
+    catch (...) {                                                                                  \
+        handler.handleUnexpectedInflightException();                                               \
+    }
+
+#endif
+
+#define INTERNAL_CATCH_REACT(handler) handler.complete();
+
+///////////////////////////////////////////////////////////////////////////////
+#define INTERNAL_CATCH_TEST(macroName, resultDisposition, ...)                                     \
+    do {                                                                                           \
+        CATCH_INTERNAL_IGNORE_BUT_WARN(__VA_ARGS__);                                               \
+        Catch::AssertionHandler catchAssertionHandler(macroName##_catch_sr,                        \
+                                                      CATCH_INTERNAL_LINEINFO,                     \
+                                                      CATCH_INTERNAL_STRINGIFY(__VA_ARGS__),       \
+                                                      resultDisposition);                          \
+        INTERNAL_CATCH_TRY {                                                                       \
+            CATCH_INTERNAL_START_WARNINGS_SUPPRESSION                                              \
+            CATCH_INTERNAL_SUPPRESS_PARENTHESES_WARNINGS                                           \
+            catchAssertionHandler.handleExpr(Catch::Decomposer() <= __VA_ARGS__);                  \
+            CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION                                               \
+        }                                                                                          \
+        INTERNAL_CATCH_CATCH(catchAssertionHandler)                                                \
+        INTERNAL_CATCH_REACT(catchAssertionHandler)                                                \
+    } while ((void)0, (false) && static_cast<bool>(!!(__VA_ARGS__)))
+
+///////////////////////////////////////////////////////////////////////////////
+#define INTERNAL_CATCH_IF(macroName, resultDisposition, ...)                                       \
+    INTERNAL_CATCH_TEST(macroName, resultDisposition, __VA_ARGS__);                                \
+    if (Catch::getResultCapture().lastAssertionPassed())
+
+///////////////////////////////////////////////////////////////////////////////
+#define INTERNAL_CATCH_ELSE(macroName, resultDisposition, ...)                                     \
+    INTERNAL_CATCH_TEST(macroName, resultDisposition, __VA_ARGS__);                                \
+    if (!Catch::getResultCapture().lastAssertionPassed())
+
+///////////////////////////////////////////////////////////////////////////////
+#define INTERNAL_CATCH_NO_THROW(macroName, resultDisposition, ...)                                 \
+    do {                                                                                           \
+        Catch::AssertionHandler catchAssertionHandler(macroName##_catch_sr,                        \
+                                                      CATCH_INTERNAL_LINEINFO,                     \
+                                                      CATCH_INTERNAL_STRINGIFY(__VA_ARGS__),       \
+                                                      resultDisposition);                          \
+        try {                                                                                      \
+            static_cast<void>(__VA_ARGS__);                                                        \
+            catchAssertionHandler.handleExceptionNotThrownAsExpected();                            \
+        } catch (...) {                                                                            \
+            catchAssertionHandler.handleUnexpectedInflightException();                             \
+        }                                                                                          \
+        INTERNAL_CATCH_REACT(catchAssertionHandler)                                                \
+    } while (false)
+
+///////////////////////////////////////////////////////////////////////////////
+#define INTERNAL_CATCH_THROWS(macroName, resultDisposition, ...)                                   \
+    do {                                                                                           \
+        Catch::AssertionHandler catchAssertionHandler(macroName##_catch_sr,                        \
+                                                      CATCH_INTERNAL_LINEINFO,                     \
+                                                      CATCH_INTERNAL_STRINGIFY(__VA_ARGS__),       \
+                                                      resultDisposition);                          \
+        if (catchAssertionHandler.allowThrows())                                                   \
+            try {                                                                                  \
+                static_cast<void>(__VA_ARGS__);                                                    \
+                catchAssertionHandler.handleUnexpectedExceptionNotThrown();                        \
+            } catch (...) {                                                                        \
+                catchAssertionHandler.handleExceptionThrownAsExpected();                           \
+            }                                                                                      \
+        else                                                                                       \
+            catchAssertionHandler.handleThrowingCallSkipped();                                     \
+        INTERNAL_CATCH_REACT(catchAssertionHandler)                                                \
+    } while (false)
+
+///////////////////////////////////////////////////////////////////////////////
+#define INTERNAL_CATCH_THROWS_AS(macroName, exceptionType, resultDisposition, expr)                \
+    do {                                                                                           \
+        Catch::AssertionHandler catchAssertionHandler(                                             \
+            macroName##_catch_sr,                                                                  \
+            CATCH_INTERNAL_LINEINFO,                                                               \
+            CATCH_INTERNAL_STRINGIFY(expr) ", " CATCH_INTERNAL_STRINGIFY(exceptionType),           \
+            resultDisposition);                                                                    \
+        if (catchAssertionHandler.allowThrows())                                                   \
+            try {                                                                                  \
+                static_cast<void>(expr);                                                           \
+                catchAssertionHandler.handleUnexpectedExceptionNotThrown();                        \
+            } catch (exceptionType const&) {                                                       \
+                catchAssertionHandler.handleExceptionThrownAsExpected();                           \
+            } catch (...) {                                                                        \
+                catchAssertionHandler.handleUnexpectedInflightException();                         \
+            }                                                                                      \
+        else                                                                                       \
+            catchAssertionHandler.handleThrowingCallSkipped();                                     \
+        INTERNAL_CATCH_REACT(catchAssertionHandler)                                                \
+    } while (false)
+
+///////////////////////////////////////////////////////////////////////////////
+#define INTERNAL_CATCH_MSG(macroName, messageType, resultDisposition, ...)                         \
+    do {                                                                                           \
+        Catch::AssertionHandler catchAssertionHandler(                                             \
+            macroName##_catch_sr, CATCH_INTERNAL_LINEINFO, Catch::StringRef(), resultDisposition); \
+        catchAssertionHandler.handleMessage(                                                       \
+            messageType,                                                                           \
+            (Catch::MessageStream() << __VA_ARGS__ + ::Catch::StreamEndStop()).m_stream.str());    \
+        INTERNAL_CATCH_REACT(catchAssertionHandler)                                                \
+    } while (false)
+
+///////////////////////////////////////////////////////////////////////////////
+#define INTERNAL_CATCH_CAPTURE(varName, macroName, ...)                                            \
+    auto varName =                                                                                 \
+        Catch::Capturer(macroName, CATCH_INTERNAL_LINEINFO, Catch::ResultWas::Info, #__VA_ARGS__); \
+    varName.captureValues(0, __VA_ARGS__)
+
+///////////////////////////////////////////////////////////////////////////////
+#define INTERNAL_CATCH_INFO(macroName, log)                                                        \
+    Catch::ScopedMessage INTERNAL_CATCH_UNIQUE_NAME(scopedMessage)(                                \
+        Catch::MessageBuilder(                                                                     \
+            macroName##_catch_sr, CATCH_INTERNAL_LINEINFO, Catch::ResultWas::Info)                 \
+        << log);
+
+///////////////////////////////////////////////////////////////////////////////
+#define INTERNAL_CATCH_UNSCOPED_INFO(macroName, log)                                               \
+    Catch::getResultCapture().emplaceUnscopedMessage(                                              \
+        Catch::MessageBuilder(                                                                     \
+            macroName##_catch_sr, CATCH_INTERNAL_LINEINFO, Catch::ResultWas::Info)                 \
+        << log)
+
+///////////////////////////////////////////////////////////////////////////////
+// Although this is matcher-based, it can be used with just a string
+#define INTERNAL_CATCH_THROWS_STR_MATCHES(macroName, resultDisposition, matcher, ...)              \
+    do {                                                                                           \
+        Catch::AssertionHandler catchAssertionHandler(                                             \
+            macroName##_catch_sr,                                                                  \
+            CATCH_INTERNAL_LINEINFO,                                                               \
+            CATCH_INTERNAL_STRINGIFY(__VA_ARGS__) ", " CATCH_INTERNAL_STRINGIFY(matcher),          \
+            resultDisposition);                                                                    \
+        if (catchAssertionHandler.allowThrows())                                                   \
+            try {                                                                                  \
+                static_cast<void>(__VA_ARGS__);                                                    \
+                catchAssertionHandler.handleUnexpectedExceptionNotThrown();                        \
+            } catch (...) {                                                                        \
+                Catch::handleExceptionMatchExpr(                                                   \
+                    catchAssertionHandler, matcher, #matcher##_catch_sr);                          \
+            }                                                                                      \
+        else                                                                                       \
+            catchAssertionHandler.handleThrowingCallSkipped();                                     \
+        INTERNAL_CATCH_REACT(catchAssertionHandler)                                                \
+    } while (false)
+
+#endif // CATCH_CONFIG_DISABLE
+
+// end catch_capture.hpp
+// start catch_section.h
+
+// start catch_section_info.h
+
+// start catch_totals.h
+
+#include <cstddef>
+
+namespace Catch {
+
+    struct Counts {
+        Counts operator-(Counts const& other) const;
+        Counts& operator+=(Counts const& other);
+
+        std::size_t total() const;
+        bool allPassed() const;
+        bool allOk() const;
+
+        std::size_t passed = 0;
+        std::size_t failed = 0;
+        std::size_t failedButOk = 0;
+    };
+
+    struct Totals {
+
+        Totals operator-(Totals const& other) const;
+        Totals& operator+=(Totals const& other);
+
+        Totals delta(Totals const& prevTotals) const;
+
+        int error = 0;
+        Counts assertions;
+        Counts testCases;
+    };
+} // namespace Catch
+
+// end catch_totals.h
+#include <string>
+
+namespace Catch {
+
+    struct SectionInfo {
+        SectionInfo(SourceLineInfo const& _lineInfo, std::string const& _name);
+
+        // Deprecated
+        SectionInfo(SourceLineInfo const& _lineInfo, std::string const& _name, std::string const&) :
+            SectionInfo(_lineInfo, _name) {}
+
+        std::string name;
+        std::string description; // !Deprecated: this will always be empty
+        SourceLineInfo lineInfo;
+    };
+
+    struct SectionEndInfo {
+        SectionInfo sectionInfo;
+        Counts prevAssertions;
+        double durationInSeconds;
+    };
+
+} // end namespace Catch
+
+// end catch_section_info.h
+// start catch_timer.h
+
+#include <cstdint>
+
+namespace Catch {
+
+    auto getCurrentNanosecondsSinceEpoch() -> uint64_t;
+    auto getEstimatedClockResolution() -> uint64_t;
+
+    class Timer {
+        uint64_t m_nanoseconds = 0;
+
+    public:
+        void start();
+        auto getElapsedNanoseconds() const -> uint64_t;
+        auto getElapsedMicroseconds() const -> uint64_t;
+        auto getElapsedMilliseconds() const -> unsigned int;
+        auto getElapsedSeconds() const -> double;
+    };
+
+} // namespace Catch
+
+// end catch_timer.h
+#include <string>
+
+namespace Catch {
+
+    class Section : NonCopyable {
+    public:
+        Section(SectionInfo const& info);
+        ~Section();
+
+        // This indicates whether the section should be executed or not
+        explicit operator bool() const;
+
+    private:
+        SectionInfo m_info;
+
+        std::string m_name;
+        Counts m_assertions;
+        bool m_sectionIncluded;
+        Timer m_timer;
+    };
+
+} // end namespace Catch
+
+#define INTERNAL_CATCH_SECTION(...)                                                                \
+    CATCH_INTERNAL_START_WARNINGS_SUPPRESSION                                                      \
+    CATCH_INTERNAL_SUPPRESS_UNUSED_WARNINGS                                                        \
+    if (Catch::Section const& INTERNAL_CATCH_UNIQUE_NAME(catch_internal_Section) =                 \
+            Catch::SectionInfo(CATCH_INTERNAL_LINEINFO, __VA_ARGS__))                              \
+    CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION
+
+#define INTERNAL_CATCH_DYNAMIC_SECTION(...)                                                        \
+    CATCH_INTERNAL_START_WARNINGS_SUPPRESSION                                                      \
+    CATCH_INTERNAL_SUPPRESS_UNUSED_WARNINGS                                                        \
+    if (Catch::Section const& INTERNAL_CATCH_UNIQUE_NAME(catch_internal_Section) =                 \
+            Catch::SectionInfo(CATCH_INTERNAL_LINEINFO,                                            \
+                               (Catch::ReusableStringStream() << __VA_ARGS__).str()))              \
+    CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION
+
+// end catch_section.h
+// start catch_interfaces_exception.h
+
+// start catch_interfaces_registry_hub.h
+
+#include <string>
+#include <memory>
+
+namespace Catch {
+
+    class TestCase;
+    struct ITestCaseRegistry;
+    struct IExceptionTranslatorRegistry;
+    struct IExceptionTranslator;
+    struct IReporterRegistry;
+    struct IReporterFactory;
+    struct ITagAliasRegistry;
+    struct IMutableEnumValuesRegistry;
+
+    class StartupExceptionRegistry;
+
+    using IReporterFactoryPtr = std::shared_ptr<IReporterFactory>;
+
+    struct IRegistryHub {
+        virtual ~IRegistryHub();
+
+        virtual IReporterRegistry const& getReporterRegistry() const = 0;
+        virtual ITestCaseRegistry const& getTestCaseRegistry() const = 0;
+        virtual ITagAliasRegistry const& getTagAliasRegistry() const = 0;
+        virtual IExceptionTranslatorRegistry const& getExceptionTranslatorRegistry() const = 0;
+
+        virtual StartupExceptionRegistry const& getStartupExceptionRegistry() const = 0;
+    };
+
+    struct IMutableRegistryHub {
+        virtual ~IMutableRegistryHub();
+        virtual void registerReporter(std::string const& name,
+                                      IReporterFactoryPtr const& factory) = 0;
+        virtual void registerListener(IReporterFactoryPtr const& factory) = 0;
+        virtual void registerTest(TestCase const& testInfo) = 0;
+        virtual void registerTranslator(const IExceptionTranslator* translator) = 0;
+        virtual void registerTagAlias(std::string const& alias,
+                                      std::string const& tag,
+                                      SourceLineInfo const& lineInfo) = 0;
+        virtual void registerStartupException() noexcept = 0;
+        virtual IMutableEnumValuesRegistry& getMutableEnumValuesRegistry() = 0;
+    };
+
+    IRegistryHub const& getRegistryHub();
+    IMutableRegistryHub& getMutableRegistryHub();
+    void cleanUp();
+    std::string translateActiveException();
+
+} // namespace Catch
+
+// end catch_interfaces_registry_hub.h
+#if defined(CATCH_CONFIG_DISABLE)
+#define INTERNAL_CATCH_TRANSLATE_EXCEPTION_NO_REG(translatorName, signature)                       \
+    static std::string translatorName(signature)
+#endif
+
+#include <exception>
+#include <string>
+#include <vector>
+
+namespace Catch {
+    using exceptionTranslateFunction = std::string (*)();
+
+    struct IExceptionTranslator;
+    using ExceptionTranslators = std::vector<std::unique_ptr<IExceptionTranslator const>>;
+
+    struct IExceptionTranslator {
+        virtual ~IExceptionTranslator();
+        virtual std::string translate(ExceptionTranslators::const_iterator it,
+                                      ExceptionTranslators::const_iterator itEnd) const = 0;
+    };
+
+    struct IExceptionTranslatorRegistry {
+        virtual ~IExceptionTranslatorRegistry();
+
+        virtual std::string translateActiveException() const = 0;
+    };
+
+    class ExceptionTranslatorRegistrar {
+        template <typename T> class ExceptionTranslator : public IExceptionTranslator {
+        public:
+            ExceptionTranslator(std::string (*translateFunction)(T&)) :
+                m_translateFunction(translateFunction) {}
+
+            std::string translate(ExceptionTranslators::const_iterator it,
+                                  ExceptionTranslators::const_iterator itEnd) const override {
+#if defined(CATCH_CONFIG_DISABLE_EXCEPTIONS)
+                return "";
+#else
+                try {
+                    if (it == itEnd)
+                        std::rethrow_exception(std::current_exception());
+                    else
+                        return (*it)->translate(it + 1, itEnd);
+                } catch (T& ex) {
+                    return m_translateFunction(ex);
+                }
+#endif
+            }
+
+        protected:
+            std::string (*m_translateFunction)(T&);
+        };
+
+    public:
+        template <typename T> ExceptionTranslatorRegistrar(std::string (*translateFunction)(T&)) {
+            getMutableRegistryHub().registerTranslator(
+                new ExceptionTranslator<T>(translateFunction));
+        }
+    };
+} // namespace Catch
+
+///////////////////////////////////////////////////////////////////////////////
+#define INTERNAL_CATCH_TRANSLATE_EXCEPTION2(translatorName, signature)                             \
+    static std::string translatorName(signature);                                                  \
+    CATCH_INTERNAL_START_WARNINGS_SUPPRESSION                                                      \
+    CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS                                                       \
+    namespace {                                                                                    \
+        Catch::ExceptionTranslatorRegistrar                                                        \
+            INTERNAL_CATCH_UNIQUE_NAME(catch_internal_ExceptionRegistrar)(&translatorName);        \
+    }                                                                                              \
+    CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION                                                       \
+    static std::string translatorName(signature)
+
+#define INTERNAL_CATCH_TRANSLATE_EXCEPTION(signature)                                              \
+    INTERNAL_CATCH_TRANSLATE_EXCEPTION2(                                                           \
+        INTERNAL_CATCH_UNIQUE_NAME(catch_internal_ExceptionTranslator), signature)
+
+// end catch_interfaces_exception.h
+// start catch_approx.h
+
+#include <type_traits>
+
+namespace Catch {
+    namespace Detail {
+
+        class Approx {
+        private:
+            bool equalityComparisonImpl(double other) const;
+            // Validates the new margin (margin >= 0)
+            // out-of-line to avoid including stdexcept in the header
+            void setMargin(double margin);
+            // Validates the new epsilon (0 < epsilon < 1)
+            // out-of-line to avoid including stdexcept in the header
+            void setEpsilon(double epsilon);
+
+        public:
+            explicit Approx(double value);
+
+            static Approx custom();
+
+            Approx operator-() const;
+
+            template <
+                typename T,
+                typename = typename std::enable_if<std::is_constructible<double, T>::value>::type>
+            Approx operator()(T const& value) {
+                Approx approx(static_cast<double>(value));
+                approx.m_epsilon = m_epsilon;
+                approx.m_margin = m_margin;
+                approx.m_scale = m_scale;
+                return approx;
+            }
+
+            template <
+                typename T,
+                typename = typename std::enable_if<std::is_constructible<double, T>::value>::type>
+            explicit Approx(T const& value) : Approx(static_cast<double>(value)) {}
+
+            template <
+                typename T,
+                typename = typename std::enable_if<std::is_constructible<double, T>::value>::type>
+            friend bool operator==(const T& lhs, Approx const& rhs) {
+                auto lhs_v = static_cast<double>(lhs);
+                return rhs.equalityComparisonImpl(lhs_v);
+            }
+
+            template <
+                typename T,
+                typename = typename std::enable_if<std::is_constructible<double, T>::value>::type>
+            friend bool operator==(Approx const& lhs, const T& rhs) {
+                return operator==(rhs, lhs);
+            }
+
+            template <
+                typename T,
+                typename = typename std::enable_if<std::is_constructible<double, T>::value>::type>
+            friend bool operator!=(T const& lhs, Approx const& rhs) {
+                return !operator==(lhs, rhs);
+            }
+
+            template <
+                typename T,
+                typename = typename std::enable_if<std::is_constructible<double, T>::value>::type>
+            friend bool operator!=(Approx const& lhs, T const& rhs) {
+                return !operator==(rhs, lhs);
+            }
+
+            template <
+                typename T,
+                typename = typename std::enable_if<std::is_constructible<double, T>::value>::type>
+            friend bool operator<=(T const& lhs, Approx const& rhs) {
+                return static_cast<double>(lhs) < rhs.m_value || lhs == rhs;
+            }
+
+            template <
+                typename T,
+                typename = typename std::enable_if<std::is_constructible<double, T>::value>::type>
+            friend bool operator<=(Approx const& lhs, T const& rhs) {
+                return lhs.m_value < static_cast<double>(rhs) || lhs == rhs;
+            }
+
+            template <
+                typename T,
+                typename = typename std::enable_if<std::is_constructible<double, T>::value>::type>
+            friend bool operator>=(T const& lhs, Approx const& rhs) {
+                return static_cast<double>(lhs) > rhs.m_value || lhs == rhs;
+            }
+
+            template <
+                typename T,
+                typename = typename std::enable_if<std::is_constructible<double, T>::value>::type>
+            friend bool operator>=(Approx const& lhs, T const& rhs) {
+                return lhs.m_value > static_cast<double>(rhs) || lhs == rhs;
+            }
+
+            template <
+                typename T,
+                typename = typename std::enable_if<std::is_constructible<double, T>::value>::type>
+            Approx& epsilon(T const& newEpsilon) {
+                double epsilonAsDouble = static_cast<double>(newEpsilon);
+                setEpsilon(epsilonAsDouble);
+                return *this;
+            }
+
+            template <
+                typename T,
+                typename = typename std::enable_if<std::is_constructible<double, T>::value>::type>
+            Approx& margin(T const& newMargin) {
+                double marginAsDouble = static_cast<double>(newMargin);
+                setMargin(marginAsDouble);
+                return *this;
+            }
+
+            template <
+                typename T,
+                typename = typename std::enable_if<std::is_constructible<double, T>::value>::type>
+            Approx& scale(T const& newScale) {
+                m_scale = static_cast<double>(newScale);
+                return *this;
+            }
+
+            std::string toString() const;
+
+        private:
+            double m_epsilon;
+            double m_margin;
+            double m_scale;
+            double m_value;
+        };
+    } // end namespace Detail
+
+    namespace literals {
+        Detail::Approx operator"" _a(long double val);
+        Detail::Approx operator"" _a(unsigned long long val);
+    } // end namespace literals
+
+    template <> struct StringMaker<Catch::Detail::Approx> {
+        static std::string convert(Catch::Detail::Approx const& value);
+    };
+
+} // end namespace Catch
+
+// end catch_approx.h
+// start catch_string_manip.h
+
+#include <string>
+#include <iosfwd>
+#include <vector>
+
+namespace Catch {
+
+    bool startsWith(std::string const& s, std::string const& prefix);
+    bool startsWith(std::string const& s, char prefix);
+    bool endsWith(std::string const& s, std::string const& suffix);
+    bool endsWith(std::string const& s, char suffix);
+    bool contains(std::string const& s, std::string const& infix);
+    void toLowerInPlace(std::string& s);
+    std::string toLower(std::string const& s);
+    //! Returns a new string without whitespace at the start/end
+    std::string trim(std::string const& str);
+    //! Returns a substring of the original ref without whitespace. Beware
+    //! lifetimes!
+    StringRef trim(StringRef ref);
+
+    // !!! Be aware, returns refs into original string - make sure original
+    // string outlives them
+    std::vector<StringRef> splitStringRef(StringRef str, char delimiter);
+    bool
+    replaceInPlace(std::string& str, std::string const& replaceThis, std::string const& withThis);
+
+    struct pluralise {
+        pluralise(std::size_t count, std::string const& label);
+
+        friend std::ostream& operator<<(std::ostream& os, pluralise const& pluraliser);
+
+        std::size_t m_count;
+        std::string m_label;
+    };
+} // namespace Catch
+
+// end catch_string_manip.h
+#ifndef CATCH_CONFIG_DISABLE_MATCHERS
+// start catch_capture_matchers.h
+
+// start catch_matchers.h
+
+#include <string>
+#include <vector>
+
+namespace Catch {
+    namespace Matchers {
+        namespace Impl {
+
+            template <typename ArgT> struct MatchAllOf;
+            template <typename ArgT> struct MatchAnyOf;
+            template <typename ArgT> struct MatchNotOf;
+
+            class MatcherUntypedBase {
+            public:
+                MatcherUntypedBase() = default;
+                MatcherUntypedBase(MatcherUntypedBase const&) = default;
+                MatcherUntypedBase& operator=(MatcherUntypedBase const&) = delete;
+                std::string toString() const;
+
+            protected:
+                virtual ~MatcherUntypedBase();
+                virtual std::string describe() const = 0;
+                mutable std::string m_cachedToString;
+            };
+
+#ifdef __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wnon-virtual-dtor"
+#endif
+
+            template <typename ObjectT> struct MatcherMethod {
+                virtual bool match(ObjectT const& arg) const = 0;
+            };
+
+#if defined(__OBJC__)
+            // Hack to fix Catch GH issue #1661. Could use id for generic Object
+            // support. use of const for Object pointers is very uncommon and
+            // under ARC it causes some kind of signature mismatch that breaks
+            // compilation
+            template <> struct MatcherMethod<NSString*> {
+                virtual bool match(NSString* arg) const = 0;
+            };
+#endif
+
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
+
+            template <typename T> struct MatcherBase : MatcherUntypedBase, MatcherMethod<T> {
+
+                MatchAllOf<T> operator&&(MatcherBase const& other) const;
+                MatchAnyOf<T> operator||(MatcherBase const& other) const;
+                MatchNotOf<T> operator!() const;
+            };
+
+            template <typename ArgT> struct MatchAllOf : MatcherBase<ArgT> {
+                bool match(ArgT const& arg) const override {
+                    for (auto matcher : m_matchers) {
+                        if (!matcher->match(arg))
+                            return false;
+                    }
+                    return true;
+                }
+                std::string describe() const override {
+                    std::string description;
+                    description.reserve(4 + m_matchers.size() * 32);
+                    description += "( ";
+                    bool first = true;
+                    for (auto matcher : m_matchers) {
+                        if (first)
+                            first = false;
+                        else
+                            description += " and ";
+                        description += matcher->toString();
+                    }
+                    description += " )";
+                    return description;
+                }
+
+                MatchAllOf<ArgT> operator&&(MatcherBase<ArgT> const& other) {
+                    auto copy(*this);
+                    copy.m_matchers.push_back(&other);
+                    return copy;
+                }
+
+                std::vector<MatcherBase<ArgT> const*> m_matchers;
+            };
+            template <typename ArgT> struct MatchAnyOf : MatcherBase<ArgT> {
+
+                bool match(ArgT const& arg) const override {
+                    for (auto matcher : m_matchers) {
+                        if (matcher->match(arg))
+                            return true;
+                    }
+                    return false;
+                }
+                std::string describe() const override {
+                    std::string description;
+                    description.reserve(4 + m_matchers.size() * 32);
+                    description += "( ";
+                    bool first = true;
+                    for (auto matcher : m_matchers) {
+                        if (first)
+                            first = false;
+                        else
+                            description += " or ";
+                        description += matcher->toString();
+                    }
+                    description += " )";
+                    return description;
+                }
+
+                MatchAnyOf<ArgT> operator||(MatcherBase<ArgT> const& other) {
+                    auto copy(*this);
+                    copy.m_matchers.push_back(&other);
+                    return copy;
+                }
+
+                std::vector<MatcherBase<ArgT> const*> m_matchers;
+            };
+
+            template <typename ArgT> struct MatchNotOf : MatcherBase<ArgT> {
+
+                MatchNotOf(MatcherBase<ArgT> const& underlyingMatcher) :
+                    m_underlyingMatcher(underlyingMatcher) {}
+
+                bool match(ArgT const& arg) const override {
+                    return !m_underlyingMatcher.match(arg);
+                }
+
+                std::string describe() const override {
+                    return "not " + m_underlyingMatcher.toString();
+                }
+                MatcherBase<ArgT> const& m_underlyingMatcher;
+            };
+
+            template <typename T>
+            MatchAllOf<T> MatcherBase<T>::operator&&(MatcherBase const& other) const {
+                return MatchAllOf<T>() && *this && other;
+            }
+            template <typename T>
+            MatchAnyOf<T> MatcherBase<T>::operator||(MatcherBase const& other) const {
+                return MatchAnyOf<T>() || *this || other;
+            }
+            template <typename T> MatchNotOf<T> MatcherBase<T>::operator!() const {
+                return MatchNotOf<T>(*this);
+            }
+
+        } // namespace Impl
+
+    } // namespace Matchers
+
+    using namespace Matchers;
+    using Matchers::Impl::MatcherBase;
+
+} // namespace Catch
+
+// end catch_matchers.h
+// start catch_matchers_exception.hpp
+
+namespace Catch {
+    namespace Matchers {
+        namespace Exception {
+
+            class ExceptionMessageMatcher : public MatcherBase<std::exception> {
+                std::string m_message;
+
+            public:
+                ExceptionMessageMatcher(std::string const& message) : m_message(message) {}
+
+                bool match(std::exception const& ex) const override;
+
+                std::string describe() const override;
+            };
+
+        } // namespace Exception
+
+        Exception::ExceptionMessageMatcher Message(std::string const& message);
+
+    } // namespace Matchers
+} // namespace Catch
+
+// end catch_matchers_exception.hpp
+// start catch_matchers_floating.h
+
+namespace Catch {
+    namespace Matchers {
+
+        namespace Floating {
+
+            enum class FloatingPointKind : uint8_t;
+
+            struct WithinAbsMatcher : MatcherBase<double> {
+                WithinAbsMatcher(double target, double margin);
+                bool match(double const& matchee) const override;
+                std::string describe() const override;
+
+            private:
+                double m_target;
+                double m_margin;
+            };
+
+            struct WithinUlpsMatcher : MatcherBase<double> {
+                WithinUlpsMatcher(double target, uint64_t ulps, FloatingPointKind baseType);
+                bool match(double const& matchee) const override;
+                std::string describe() const override;
+
+            private:
+                double m_target;
+                uint64_t m_ulps;
+                FloatingPointKind m_type;
+            };
+
+            // Given IEEE-754 format for floats and doubles, we can assume
+            // that float -> double promotion is lossless. Given this, we can
+            // assume that if we do the standard relative comparison of
+            // |lhs - rhs| <= epsilon * max(fabs(lhs), fabs(rhs)), then we get
+            // the same result if we do this for floats, as if we do this for
+            // doubles that were promoted from floats.
+            struct WithinRelMatcher : MatcherBase<double> {
+                WithinRelMatcher(double target, double epsilon);
+                bool match(double const& matchee) const override;
+                std::string describe() const override;
+
+            private:
+                double m_target;
+                double m_epsilon;
+            };
+
+        } // namespace Floating
+
+        // The following functions create the actual matcher objects.
+        // This allows the types to be inferred
+        Floating::WithinUlpsMatcher WithinULP(double target, uint64_t maxUlpDiff);
+        Floating::WithinUlpsMatcher WithinULP(float target, uint64_t maxUlpDiff);
+        Floating::WithinAbsMatcher WithinAbs(double target, double margin);
+        Floating::WithinRelMatcher WithinRel(double target, double eps);
+        // defaults epsilon to 100*numeric_limits<double>::epsilon()
+        Floating::WithinRelMatcher WithinRel(double target);
+        Floating::WithinRelMatcher WithinRel(float target, float eps);
+        // defaults epsilon to 100*numeric_limits<float>::epsilon()
+        Floating::WithinRelMatcher WithinRel(float target);
+
+    } // namespace Matchers
+} // namespace Catch
+
+// end catch_matchers_floating.h
+// start catch_matchers_generic.hpp
+
+#include <functional>
+#include <string>
+
+namespace Catch {
+    namespace Matchers {
+        namespace Generic {
+
+            namespace Detail {
+                std::string finalizeDescription(const std::string& desc);
+            }
+
+            template <typename T> class PredicateMatcher : public MatcherBase<T> {
+                std::function<bool(T const&)> m_predicate;
+                std::string m_description;
+
+            public:
+                PredicateMatcher(std::function<bool(T const&)> const& elem,
+                                 std::string const& descr) :
+                    m_predicate(std::move(elem)),
+                    m_description(Detail::finalizeDescription(descr)) {}
+
+                bool match(T const& item) const override { return m_predicate(item); }
+
+                std::string describe() const override { return m_description; }
+            };
+
+        } // namespace Generic
+
+        // The following functions create the actual matcher objects.
+        // The user has to explicitly specify type to the function, because
+        // inferring std::function<bool(T const&)> is hard (but possible) and
+        // requires a lot of TMP.
+        template <typename T>
+        Generic::PredicateMatcher<T> Predicate(std::function<bool(T const&)> const& predicate,
+                                               std::string const& description = "") {
+            return Generic::PredicateMatcher<T>(predicate, description);
+        }
+
+    } // namespace Matchers
+} // namespace Catch
+
+// end catch_matchers_generic.hpp
+// start catch_matchers_string.h
+
+#include <string>
+
+namespace Catch {
+    namespace Matchers {
+
+        namespace StdString {
+
+            struct CasedString {
+                CasedString(std::string const& str, CaseSensitive::Choice caseSensitivity);
+                std::string adjustString(std::string const& str) const;
+                std::string caseSensitivitySuffix() const;
+
+                CaseSensitive::Choice m_caseSensitivity;
+                std::string m_str;
+            };
+
+            struct StringMatcherBase : MatcherBase<std::string> {
+                StringMatcherBase(std::string const& operation, CasedString const& comparator);
+                std::string describe() const override;
+
+                CasedString m_comparator;
+                std::string m_operation;
+            };
+
+            struct EqualsMatcher : StringMatcherBase {
+                EqualsMatcher(CasedString const& comparator);
+                bool match(std::string const& source) const override;
+            };
+            struct ContainsMatcher : StringMatcherBase {
+                ContainsMatcher(CasedString const& comparator);
+                bool match(std::string const& source) const override;
+            };
+            struct StartsWithMatcher : StringMatcherBase {
+                StartsWithMatcher(CasedString const& comparator);
+                bool match(std::string const& source) const override;
+            };
+            struct EndsWithMatcher : StringMatcherBase {
+                EndsWithMatcher(CasedString const& comparator);
+                bool match(std::string const& source) const override;
+            };
+
+            struct RegexMatcher : MatcherBase<std::string> {
+                RegexMatcher(std::string regex, CaseSensitive::Choice caseSensitivity);
+                bool match(std::string const& matchee) const override;
+                std::string describe() const override;
+
+            private:
+                std::string m_regex;
+                CaseSensitive::Choice m_caseSensitivity;
+            };
+
+        } // namespace StdString
+
+        // The following functions create the actual matcher objects.
+        // This allows the types to be inferred
+
+        StdString::EqualsMatcher Equals(std::string const& str,
+                                        CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes);
+        StdString::ContainsMatcher
+        Contains(std::string const& str,
+                 CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes);
+        StdString::EndsWithMatcher
+        EndsWith(std::string const& str,
+                 CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes);
+        StdString::StartsWithMatcher
+        StartsWith(std::string const& str,
+                   CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes);
+        StdString::RegexMatcher Matches(std::string const& regex,
+                                        CaseSensitive::Choice caseSensitivity = CaseSensitive::Yes);
+
+    } // namespace Matchers
+} // namespace Catch
+
+// end catch_matchers_string.h
+// start catch_matchers_vector.h
+
+#include <algorithm>
+
+namespace Catch {
+    namespace Matchers {
+
+        namespace Vector {
+            template <typename T, typename Alloc>
+            struct ContainsElementMatcher : MatcherBase<std::vector<T, Alloc>> {
+
+                ContainsElementMatcher(T const& comparator) : m_comparator(comparator) {}
+
+                bool match(std::vector<T, Alloc> const& v) const override {
+                    for (auto const& el : v) {
+                        if (el == m_comparator) {
+                            return true;
+                        }
+                    }
+                    return false;
+                }
+
+                std::string describe() const override {
+                    return "Contains: " + ::Catch::Detail::stringify(m_comparator);
+                }
+
+                T const& m_comparator;
+            };
+
+            template <typename T, typename AllocComp, typename AllocMatch>
+            struct ContainsMatcher : MatcherBase<std::vector<T, AllocMatch>> {
+
+                ContainsMatcher(std::vector<T, AllocComp> const& comparator) :
+                    m_comparator(comparator) {}
+
+                bool match(std::vector<T, AllocMatch> const& v) const override {
+                    // !TBD: see note in EqualsMatcher
+                    if (m_comparator.size() > v.size())
+                        return false;
+                    for (auto const& comparator : m_comparator) {
+                        auto present = false;
+                        for (const auto& el : v) {
+                            if (el == comparator) {
+                                present = true;
+                                break;
+                            }
+                        }
+                        if (!present) {
+                            return false;
+                        }
+                    }
+                    return true;
+                }
+                std::string describe() const override {
+                    return "Contains: " + ::Catch::Detail::stringify(m_comparator);
+                }
+
+                std::vector<T, AllocComp> const& m_comparator;
+            };
+
+            template <typename T, typename AllocComp, typename AllocMatch>
+            struct EqualsMatcher : MatcherBase<std::vector<T, AllocMatch>> {
+
+                EqualsMatcher(std::vector<T, AllocComp> const& comparator) :
+                    m_comparator(comparator) {}
+
+                bool match(std::vector<T, AllocMatch> const& v) const override {
+                    // !TBD: This currently works if all elements can be
+                    // compared using !=
+                    // - a more general approach would be via a compare template
+                    // that defaults to using !=. but could be specialised for,
+                    // e.g. std::vector<T, Alloc> etc
+                    // - then just call that directly
+                    if (m_comparator.size() != v.size())
+                        return false;
+                    for (std::size_t i = 0; i < v.size(); ++i)
+                        if (m_comparator[i] != v[i])
+                            return false;
+                    return true;
+                }
+                std::string describe() const override {
+                    return "Equals: " + ::Catch::Detail::stringify(m_comparator);
+                }
+                std::vector<T, AllocComp> const& m_comparator;
+            };
+
+            template <typename T, typename AllocComp, typename AllocMatch>
+            struct ApproxMatcher : MatcherBase<std::vector<T, AllocMatch>> {
+
+                ApproxMatcher(std::vector<T, AllocComp> const& comparator) :
+                    m_comparator(comparator) {}
+
+                bool match(std::vector<T, AllocMatch> const& v) const override {
+                    if (m_comparator.size() != v.size())
+                        return false;
+                    for (std::size_t i = 0; i < v.size(); ++i)
+                        if (m_comparator[i] != approx(v[i]))
+                            return false;
+                    return true;
+                }
+                std::string describe() const override {
+                    return "is approx: " + ::Catch::Detail::stringify(m_comparator);
+                }
+                template <typename = typename std::enable_if<
+                              std::is_constructible<double, T>::value>::type>
+                ApproxMatcher& epsilon(T const& newEpsilon) {
+                    approx.epsilon(newEpsilon);
+                    return *this;
+                }
+                template <typename = typename std::enable_if<
+                              std::is_constructible<double, T>::value>::type>
+                ApproxMatcher& margin(T const& newMargin) {
+                    approx.margin(newMargin);
+                    return *this;
+                }
+                template <typename = typename std::enable_if<
+                              std::is_constructible<double, T>::value>::type>
+                ApproxMatcher& scale(T const& newScale) {
+                    approx.scale(newScale);
+                    return *this;
+                }
+
+                std::vector<T, AllocComp> const& m_comparator;
+                mutable Catch::Detail::Approx approx = Catch::Detail::Approx::custom();
+            };
+
+            template <typename T, typename AllocComp, typename AllocMatch>
+            struct UnorderedEqualsMatcher : MatcherBase<std::vector<T, AllocMatch>> {
+                UnorderedEqualsMatcher(std::vector<T, AllocComp> const& target) :
+                    m_target(target) {}
+                bool match(std::vector<T, AllocMatch> const& vec) const override {
+                    if (m_target.size() != vec.size()) {
+                        return false;
+                    }
+                    return std::is_permutation(m_target.begin(), m_target.end(), vec.begin());
+                }
+
+                std::string describe() const override {
+                    return "UnorderedEquals: " + ::Catch::Detail::stringify(m_target);
+                }
+
+            private:
+                std::vector<T, AllocComp> const& m_target;
+            };
+
+        } // namespace Vector
+
+        // The following functions create the actual matcher objects.
+        // This allows the types to be inferred
+
+        template <typename T,
+                  typename AllocComp = std::allocator<T>,
+                  typename AllocMatch = AllocComp>
+        Vector::ContainsMatcher<T, AllocComp, AllocMatch>
+        Contains(std::vector<T, AllocComp> const& comparator) {
+            return Vector::ContainsMatcher<T, AllocComp, AllocMatch>(comparator);
+        }
+
+        template <typename T, typename Alloc = std::allocator<T>>
+        Vector::ContainsElementMatcher<T, Alloc> VectorContains(T const& comparator) {
+            return Vector::ContainsElementMatcher<T, Alloc>(comparator);
+        }
+
+        template <typename T,
+                  typename AllocComp = std::allocator<T>,
+                  typename AllocMatch = AllocComp>
+        Vector::EqualsMatcher<T, AllocComp, AllocMatch>
+        Equals(std::vector<T, AllocComp> const& comparator) {
+            return Vector::EqualsMatcher<T, AllocComp, AllocMatch>(comparator);
+        }
+
+        template <typename T,
+                  typename AllocComp = std::allocator<T>,
+                  typename AllocMatch = AllocComp>
+        Vector::ApproxMatcher<T, AllocComp, AllocMatch>
+        Approx(std::vector<T, AllocComp> const& comparator) {
+            return Vector::ApproxMatcher<T, AllocComp, AllocMatch>(comparator);
+        }
+
+        template <typename T,
+                  typename AllocComp = std::allocator<T>,
+                  typename AllocMatch = AllocComp>
+        Vector::UnorderedEqualsMatcher<T, AllocComp, AllocMatch>
+        UnorderedEquals(std::vector<T, AllocComp> const& target) {
+            return Vector::UnorderedEqualsMatcher<T, AllocComp, AllocMatch>(target);
+        }
+
+    } // namespace Matchers
+} // namespace Catch
+
+// end catch_matchers_vector.h
+namespace Catch {
+
+    template <typename ArgT, typename MatcherT> class MatchExpr : public ITransientExpression {
+        ArgT const& m_arg;
+        MatcherT m_matcher;
+        StringRef m_matcherString;
+
+    public:
+        MatchExpr(ArgT const& arg, MatcherT const& matcher, StringRef const& matcherString) :
+            ITransientExpression{true, matcher.match(arg)},
+            m_arg(arg),
+            m_matcher(matcher),
+            m_matcherString(matcherString) {}
+
+        void streamReconstructedExpression(std::ostream& os) const override {
+            auto matcherAsString = m_matcher.toString();
+            os << Catch::Detail::stringify(m_arg) << ' ';
+            if (matcherAsString == Detail::unprintableString)
+                os << m_matcherString;
+            else
+                os << matcherAsString;
+        }
+    };
+
+    using StringMatcher = Matchers::Impl::MatcherBase<std::string>;
+
+    void handleExceptionMatchExpr(AssertionHandler& handler,
+                                  StringMatcher const& matcher,
+                                  StringRef const& matcherString);
+
+    template <typename ArgT, typename MatcherT>
+    auto makeMatchExpr(ArgT const& arg, MatcherT const& matcher, StringRef const& matcherString)
+        -> MatchExpr<ArgT, MatcherT> {
+        return MatchExpr<ArgT, MatcherT>(arg, matcher, matcherString);
+    }
+
+} // namespace Catch
+
+///////////////////////////////////////////////////////////////////////////////
+#define INTERNAL_CHECK_THAT(macroName, matcher, resultDisposition, arg)                            \
+    do {                                                                                           \
+        Catch::AssertionHandler catchAssertionHandler(                                             \
+            macroName##_catch_sr,                                                                  \
+            CATCH_INTERNAL_LINEINFO,                                                               \
+            CATCH_INTERNAL_STRINGIFY(arg) ", " CATCH_INTERNAL_STRINGIFY(matcher),                  \
+            resultDisposition);                                                                    \
+        INTERNAL_CATCH_TRY {                                                                       \
+            catchAssertionHandler.handleExpr(                                                      \
+                Catch::makeMatchExpr(arg, matcher, #matcher##_catch_sr));                          \
+        }                                                                                          \
+        INTERNAL_CATCH_CATCH(catchAssertionHandler)                                                \
+        INTERNAL_CATCH_REACT(catchAssertionHandler)                                                \
+    } while (false)
+
+///////////////////////////////////////////////////////////////////////////////
+#define INTERNAL_CATCH_THROWS_MATCHES(macroName, exceptionType, resultDisposition, matcher, ...)   \
+    do {                                                                                           \
+        Catch::AssertionHandler catchAssertionHandler(                                             \
+            macroName##_catch_sr,                                                                  \
+            CATCH_INTERNAL_LINEINFO,                                                               \
+            CATCH_INTERNAL_STRINGIFY(__VA_ARGS__) ", " CATCH_INTERNAL_STRINGIFY(                   \
+                exceptionType) ","                                                                 \
+                               " " CATCH_INTERNAL_STRINGIFY(matcher),                              \
+            resultDisposition);                                                                    \
+        if (catchAssertionHandler.allowThrows())                                                   \
+            try {                                                                                  \
+                static_cast<void>(__VA_ARGS__);                                                    \
+                catchAssertionHandler.handleUnexpectedExceptionNotThrown();                        \
+            } catch (exceptionType const& ex) {                                                    \
+                catchAssertionHandler.handleExpr(                                                  \
+                    Catch::makeMatchExpr(ex, matcher, #matcher##_catch_sr));                       \
+            } catch (...) {                                                                        \
+                catchAssertionHandler.handleUnexpectedInflightException();                         \
+            }                                                                                      \
+        else                                                                                       \
+            catchAssertionHandler.handleThrowingCallSkipped();                                     \
+        INTERNAL_CATCH_REACT(catchAssertionHandler)                                                \
+    } while (false)
+
+// end catch_capture_matchers.h
+#endif
+// start catch_generators.hpp
+
+// start catch_interfaces_generatortracker.h
+
+#include <memory>
+
+namespace Catch {
+
+    namespace Generators {
+        class GeneratorUntypedBase {
+        public:
+            GeneratorUntypedBase() = default;
+            virtual ~GeneratorUntypedBase();
+            // Attempts to move the generator to the next element
+            //
+            // Returns true iff the move succeeded (and a valid element
+            // can be retrieved).
+            virtual bool next() = 0;
+        };
+        using GeneratorBasePtr = std::unique_ptr<GeneratorUntypedBase>;
+
+    } // namespace Generators
+
+    struct IGeneratorTracker {
+        virtual ~IGeneratorTracker();
+        virtual auto hasGenerator() const -> bool = 0;
+        virtual auto getGenerator() const -> Generators::GeneratorBasePtr const& = 0;
+        virtual void setGenerator(Generators::GeneratorBasePtr&& generator) = 0;
+    };
+
+} // namespace Catch
+
+// end catch_interfaces_generatortracker.h
+// start catch_enforce.h
+
+#include <exception>
+
+namespace Catch {
+#if !defined(CATCH_CONFIG_DISABLE_EXCEPTIONS)
+    template <typename Ex> [[noreturn]] void throw_exception(Ex const& e) { throw e; }
+#else // ^^ Exceptions are enabled //  Exceptions are disabled vv
+    [[noreturn]] void throw_exception(std::exception const& e);
+#endif
+
+    [[noreturn]] void throw_logic_error(std::string const& msg);
+    [[noreturn]] void throw_domain_error(std::string const& msg);
+    [[noreturn]] void throw_runtime_error(std::string const& msg);
+
+} // namespace Catch
+
+#define CATCH_MAKE_MSG(...) (Catch::ReusableStringStream() << __VA_ARGS__).str()
+
+#define CATCH_INTERNAL_ERROR(...)                                                                  \
+    Catch::throw_logic_error(                                                                      \
+        CATCH_MAKE_MSG(CATCH_INTERNAL_LINEINFO << ": Internal Catch2 error: " << __VA_ARGS__))
+
+#define CATCH_ERROR(...) Catch::throw_domain_error(CATCH_MAKE_MSG(__VA_ARGS__))
+
+#define CATCH_RUNTIME_ERROR(...) Catch::throw_runtime_error(CATCH_MAKE_MSG(__VA_ARGS__))
+
+#define CATCH_ENFORCE(condition, ...)                                                              \
+    do {                                                                                           \
+        if (!(condition))                                                                          \
+            CATCH_ERROR(__VA_ARGS__);                                                              \
+    } while (false)
+
+// end catch_enforce.h
+#include <memory>
+#include <vector>
+#include <cassert>
+
+#include <utility>
+#include <exception>
+
+namespace Catch {
+
+    class GeneratorException : public std::exception {
+        const char* const m_msg = "";
+
+    public:
+        GeneratorException(const char* msg) : m_msg(msg) {}
+
+        const char* what() const noexcept override final;
+    };
+
+    namespace Generators {
+
+        // !TBD move this into its own location?
+        namespace pf {
+            template <typename T, typename... Args> std::unique_ptr<T> make_unique(Args&&... args) {
+                return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
+            }
+        } // namespace pf
+
+        template <typename T> struct IGenerator : GeneratorUntypedBase {
+            virtual ~IGenerator() = default;
+
+            // Returns the current element of the generator
+            //
+            // \Precondition The generator is either freshly constructed,
+            // or the last call to `next()` returned true
+            virtual T const& get() const = 0;
+            using type = T;
+        };
+
+        template <typename T> class SingleValueGenerator final : public IGenerator<T> {
+            T m_value;
+
+        public:
+            SingleValueGenerator(T&& value) : m_value(std::move(value)) {}
+
+            T const& get() const override { return m_value; }
+            bool next() override { return false; }
+        };
+
+        template <typename T> class FixedValuesGenerator final : public IGenerator<T> {
+            static_assert(!std::is_same<T, bool>::value,
+                          "FixedValuesGenerator does not support bools because "
+                          "of std::vector<bool>"
+                          "specialization, use SingleValue Generator instead.");
+            std::vector<T> m_values;
+            size_t m_idx = 0;
+
+        public:
+            FixedValuesGenerator(std::initializer_list<T> values) : m_values(values) {}
+
+            T const& get() const override { return m_values[m_idx]; }
+            bool next() override {
+                ++m_idx;
+                return m_idx < m_values.size();
+            }
+        };
+
+        template <typename T> class GeneratorWrapper final {
+            std::unique_ptr<IGenerator<T>> m_generator;
+
+        public:
+            GeneratorWrapper(std::unique_ptr<IGenerator<T>> generator) :
+                m_generator(std::move(generator)) {}
+            T const& get() const { return m_generator->get(); }
+            bool next() { return m_generator->next(); }
+        };
+
+        template <typename T> GeneratorWrapper<T> value(T&& value) {
+            return GeneratorWrapper<T>(
+                pf::make_unique<SingleValueGenerator<T>>(std::forward<T>(value)));
+        }
+        template <typename T> GeneratorWrapper<T> values(std::initializer_list<T> values) {
+            return GeneratorWrapper<T>(pf::make_unique<FixedValuesGenerator<T>>(values));
+        }
+
+        template <typename T> class Generators : public IGenerator<T> {
+            std::vector<GeneratorWrapper<T>> m_generators;
+            size_t m_current = 0;
+
+            void populate(GeneratorWrapper<T>&& generator) {
+                m_generators.emplace_back(std::move(generator));
+            }
+            void populate(T&& val) { m_generators.emplace_back(value(std::forward<T>(val))); }
+            template <typename U> void populate(U&& val) { populate(T(std::forward<U>(val))); }
+            template <typename U, typename... Gs>
+            void populate(U&& valueOrGenerator, Gs&&... moreGenerators) {
+                populate(std::forward<U>(valueOrGenerator));
+                populate(std::forward<Gs>(moreGenerators)...);
+            }
+
+        public:
+            template <typename... Gs> Generators(Gs&&... moreGenerators) {
+                m_generators.reserve(sizeof...(Gs));
+                populate(std::forward<Gs>(moreGenerators)...);
+            }
+
+            T const& get() const override { return m_generators[m_current].get(); }
+
+            bool next() override {
+                if (m_current >= m_generators.size()) {
+                    return false;
+                }
+                const bool current_status = m_generators[m_current].next();
+                if (!current_status) {
+                    ++m_current;
+                }
+                return m_current < m_generators.size();
+            }
+        };
+
+        template <typename... Ts>
+        GeneratorWrapper<std::tuple<Ts...>>
+        table(std::initializer_list<std::tuple<typename std::decay<Ts>::type...>> tuples) {
+            return values<std::tuple<Ts...>>(tuples);
+        }
+
+        // Tag type to signal that a generator sequence should convert arguments
+        // to a specific type
+        template <typename T> struct as {};
+
+        template <typename T, typename... Gs>
+        auto makeGenerators(GeneratorWrapper<T>&& generator, Gs&&... moreGenerators)
+            -> Generators<T> {
+            return Generators<T>(std::move(generator), std::forward<Gs>(moreGenerators)...);
+        }
+        template <typename T>
+        auto makeGenerators(GeneratorWrapper<T>&& generator) -> Generators<T> {
+            return Generators<T>(std::move(generator));
+        }
+        template <typename T, typename... Gs>
+        auto makeGenerators(T&& val, Gs&&... moreGenerators) -> Generators<T> {
+            return makeGenerators(value(std::forward<T>(val)), std::forward<Gs>(moreGenerators)...);
+        }
+        template <typename T, typename U, typename... Gs>
+        auto makeGenerators(as<T>, U&& val, Gs&&... moreGenerators) -> Generators<T> {
+            return makeGenerators(value(T(std::forward<U>(val))),
+                                  std::forward<Gs>(moreGenerators)...);
+        }
+
+        auto acquireGeneratorTracker(StringRef generatorName, SourceLineInfo const& lineInfo)
+            -> IGeneratorTracker&;
+
+        template <typename L>
+        // Note: The type after -> is weird, because VS2015 cannot parse
+        //       the expression used in the typedef inside, when it is in
+        //       return type. Yeah.
+        auto generate(StringRef generatorName,
+                      SourceLineInfo const& lineInfo,
+                      L const& generatorExpression)
+            -> decltype(std::declval<decltype(generatorExpression())>().get()) {
+            using UnderlyingType = typename decltype(generatorExpression())::type;
+
+            IGeneratorTracker& tracker = acquireGeneratorTracker(generatorName, lineInfo);
+            if (!tracker.hasGenerator()) {
+                tracker.setGenerator(
+                    pf::make_unique<Generators<UnderlyingType>>(generatorExpression()));
+            }
+
+            auto const& generator =
+                static_cast<IGenerator<UnderlyingType> const&>(*tracker.getGenerator());
+            return generator.get();
+        }
+
+    } // namespace Generators
+} // namespace Catch
+
+#define GENERATE(...)                                                                              \
+    Catch::Generators::generate(INTERNAL_CATCH_STRINGIZE(INTERNAL_CATCH_UNIQUE_NAME(generator)),   \
+                                CATCH_INTERNAL_LINEINFO,                                           \
+                                [] {                                                               \
+                                    using namespace Catch::Generators;                             \
+                                    return makeGenerators(__VA_ARGS__);                            \
+                                }) // NOLINT(google-build-using-namespace)
+#define GENERATE_COPY(...)                                                                         \
+    Catch::Generators::generate(INTERNAL_CATCH_STRINGIZE(INTERNAL_CATCH_UNIQUE_NAME(generator)),   \
+                                CATCH_INTERNAL_LINEINFO,                                           \
+                                [=] {                                                              \
+                                    using namespace Catch::Generators;                             \
+                                    return makeGenerators(__VA_ARGS__);                            \
+                                }) // NOLINT(google-build-using-namespace)
+#define GENERATE_REF(...)                                                                          \
+    Catch::Generators::generate(INTERNAL_CATCH_STRINGIZE(INTERNAL_CATCH_UNIQUE_NAME(generator)),   \
+                                CATCH_INTERNAL_LINEINFO,                                           \
+                                [&] {                                                              \
+                                    using namespace Catch::Generators;                             \
+                                    return makeGenerators(__VA_ARGS__);                            \
+                                }) // NOLINT(google-build-using-namespace)
+
+// end catch_generators.hpp
+// start catch_generators_generic.hpp
+
+namespace Catch {
+    namespace Generators {
+
+        template <typename T> class TakeGenerator : public IGenerator<T> {
+            GeneratorWrapper<T> m_generator;
+            size_t m_returned = 0;
+            size_t m_target;
+
+        public:
+            TakeGenerator(size_t target, GeneratorWrapper<T>&& generator) :
+                m_generator(std::move(generator)), m_target(target) {
+                assert(target != 0 && "Empty generators are not allowed");
+            }
+            T const& get() const override { return m_generator.get(); }
+            bool next() override {
+                ++m_returned;
+                if (m_returned >= m_target) {
+                    return false;
+                }
+
+                const auto success = m_generator.next();
+                // If the underlying generator does not contain enough values
+                // then we cut short as well
+                if (!success) {
+                    m_returned = m_target;
+                }
+                return success;
+            }
+        };
+
+        template <typename T>
+        GeneratorWrapper<T> take(size_t target, GeneratorWrapper<T>&& generator) {
+            return GeneratorWrapper<T>(
+                pf::make_unique<TakeGenerator<T>>(target, std::move(generator)));
+        }
+
+        template <typename T, typename Predicate> class FilterGenerator : public IGenerator<T> {
+            GeneratorWrapper<T> m_generator;
+            Predicate m_predicate;
+
+        public:
+            template <typename P = Predicate>
+            FilterGenerator(P&& pred, GeneratorWrapper<T>&& generator) :
+                m_generator(std::move(generator)), m_predicate(std::forward<P>(pred)) {
+                if (!m_predicate(m_generator.get())) {
+                    // It might happen that there are no values that pass the
+                    // filter. In that case we throw an exception.
+                    auto has_initial_value = next();
+                    if (!has_initial_value) {
+                        Catch::throw_exception(
+                            GeneratorException("No valid value found in filtered generator"));
+                    }
+                }
+            }
+
+            T const& get() const override { return m_generator.get(); }
+
+            bool next() override {
+                bool success = m_generator.next();
+                if (!success) {
+                    return false;
+                }
+                while (!m_predicate(m_generator.get()) && (success = m_generator.next()) == true)
+                    ;
+                return success;
+            }
+        };
+
+        template <typename T, typename Predicate>
+        GeneratorWrapper<T> filter(Predicate&& pred, GeneratorWrapper<T>&& generator) {
+            return GeneratorWrapper<T>(
+                std::unique_ptr<IGenerator<T>>(pf::make_unique<FilterGenerator<T, Predicate>>(
+                    std::forward<Predicate>(pred), std::move(generator))));
+        }
+
+        template <typename T> class RepeatGenerator : public IGenerator<T> {
+            static_assert(!std::is_same<T, bool>::value,
+                          "RepeatGenerator currently does not support bools"
+                          "because of std::vector<bool> specialization");
+            GeneratorWrapper<T> m_generator;
+            mutable std::vector<T> m_returned;
+            size_t m_target_repeats;
+            size_t m_current_repeat = 0;
+            size_t m_repeat_index = 0;
+
+        public:
+            RepeatGenerator(size_t repeats, GeneratorWrapper<T>&& generator) :
+                m_generator(std::move(generator)), m_target_repeats(repeats) {
+                assert(m_target_repeats > 0 && "Repeat generator must repeat at least once");
+            }
+
+            T const& get() const override {
+                if (m_current_repeat == 0) {
+                    m_returned.push_back(m_generator.get());
+                    return m_returned.back();
+                }
+                return m_returned[m_repeat_index];
+            }
+
+            bool next() override {
+                // There are 2 basic cases:
+                // 1) We are still reading the generator
+                // 2) We are reading our own cache
+
+                // In the first case, we need to poke the underlying generator.
+                // If it happily moves, we are left in that state, otherwise it
+                // is time to start reading from our cache
+                if (m_current_repeat == 0) {
+                    const auto success = m_generator.next();
+                    if (!success) {
+                        ++m_current_repeat;
+                    }
+                    return m_current_repeat < m_target_repeats;
+                }
+
+                // In the second case, we need to move indices forward and check
+                // that we haven't run up against the end
+                ++m_repeat_index;
+                if (m_repeat_index == m_returned.size()) {
+                    m_repeat_index = 0;
+                    ++m_current_repeat;
+                }
+                return m_current_repeat < m_target_repeats;
+            }
+        };
+
+        template <typename T>
+        GeneratorWrapper<T> repeat(size_t repeats, GeneratorWrapper<T>&& generator) {
+            return GeneratorWrapper<T>(
+                pf::make_unique<RepeatGenerator<T>>(repeats, std::move(generator)));
+        }
+
+        template <typename T, typename U, typename Func> class MapGenerator : public IGenerator<T> {
+            // TBD: provide static assert for mapping function, for friendly
+            // error message
+            GeneratorWrapper<U> m_generator;
+            Func m_function;
+            // To avoid returning dangling reference, we have to save the values
+            T m_cache;
+
+        public:
+            template <typename F2 = Func>
+            MapGenerator(F2&& function, GeneratorWrapper<U>&& generator) :
+                m_generator(std::move(generator)),
+                m_function(std::forward<F2>(function)),
+                m_cache(m_function(m_generator.get())) {}
+
+            T const& get() const override { return m_cache; }
+            bool next() override {
+                const auto success = m_generator.next();
+                if (success) {
+                    m_cache = m_function(m_generator.get());
+                }
+                return success;
+            }
+        };
+
+        template <typename Func, typename U, typename T = FunctionReturnType<Func, U>>
+        GeneratorWrapper<T> map(Func&& function, GeneratorWrapper<U>&& generator) {
+            return GeneratorWrapper<T>(pf::make_unique<MapGenerator<T, U, Func>>(
+                std::forward<Func>(function), std::move(generator)));
+        }
+
+        template <typename T, typename U, typename Func>
+        GeneratorWrapper<T> map(Func&& function, GeneratorWrapper<U>&& generator) {
+            return GeneratorWrapper<T>(pf::make_unique<MapGenerator<T, U, Func>>(
+                std::forward<Func>(function), std::move(generator)));
+        }
+
+        template <typename T> class ChunkGenerator final : public IGenerator<std::vector<T>> {
+            std::vector<T> m_chunk;
+            size_t m_chunk_size;
+            GeneratorWrapper<T> m_generator;
+            bool m_used_up = false;
+
+        public:
+            ChunkGenerator(size_t size, GeneratorWrapper<T> generator) :
+                m_chunk_size(size), m_generator(std::move(generator)) {
+                m_chunk.reserve(m_chunk_size);
+                if (m_chunk_size != 0) {
+                    m_chunk.push_back(m_generator.get());
+                    for (size_t i = 1; i < m_chunk_size; ++i) {
+                        if (!m_generator.next()) {
+                            Catch::throw_exception(
+                                GeneratorException("Not enough values to initialize the first "
+                                                   "chunk"));
+                        }
+                        m_chunk.push_back(m_generator.get());
+                    }
+                }
+            }
+            std::vector<T> const& get() const override { return m_chunk; }
+            bool next() override {
+                m_chunk.clear();
+                for (size_t idx = 0; idx < m_chunk_size; ++idx) {
+                    if (!m_generator.next()) {
+                        return false;
+                    }
+                    m_chunk.push_back(m_generator.get());
+                }
+                return true;
+            }
+        };
+
+        template <typename T>
+        GeneratorWrapper<std::vector<T>> chunk(size_t size, GeneratorWrapper<T>&& generator) {
+            return GeneratorWrapper<std::vector<T>>(
+                pf::make_unique<ChunkGenerator<T>>(size, std::move(generator)));
+        }
+
+    } // namespace Generators
+} // namespace Catch
+
+// end catch_generators_generic.hpp
+// start catch_generators_specific.hpp
+
+// start catch_context.h
+
+#include <memory>
+
+namespace Catch {
+
+    struct IResultCapture;
+    struct IRunner;
+    struct IConfig;
+    struct IMutableContext;
+
+    using IConfigPtr = std::shared_ptr<IConfig const>;
+
+    struct IContext {
+        virtual ~IContext();
+
+        virtual IResultCapture* getResultCapture() = 0;
+        virtual IRunner* getRunner() = 0;
+        virtual IConfigPtr const& getConfig() const = 0;
+    };
+
+    struct IMutableContext : IContext {
+        virtual ~IMutableContext();
+        virtual void setResultCapture(IResultCapture* resultCapture) = 0;
+        virtual void setRunner(IRunner* runner) = 0;
+        virtual void setConfig(IConfigPtr const& config) = 0;
+
+    private:
+        static IMutableContext* currentContext;
+        friend IMutableContext& getCurrentMutableContext();
+        friend void cleanUpContext();
+        static void createContext();
+    };
+
+    inline IMutableContext& getCurrentMutableContext() {
+        if (!IMutableContext::currentContext)
+            IMutableContext::createContext();
+        // NOLINTNEXTLINE(clang-analyzer-core.uninitialized.UndefReturn)
+        return *IMutableContext::currentContext;
+    }
+
+    inline IContext& getCurrentContext() { return getCurrentMutableContext(); }
+
+    void cleanUpContext();
+
+    class SimplePcg32;
+    SimplePcg32& rng();
+} // namespace Catch
+
+// end catch_context.h
+// start catch_interfaces_config.h
+
+// start catch_option.hpp
+
+namespace Catch {
+
+    // An optional type
+    template <typename T> class Option {
+    public:
+        Option() : nullableValue(nullptr) {}
+        Option(T const& _value) : nullableValue(new (storage) T(_value)) {}
+        Option(Option const& _other) : nullableValue(_other ? new (storage) T(*_other) : nullptr) {}
+
+        ~Option() { reset(); }
+
+        Option& operator=(Option const& _other) {
+            if (&_other != this) {
+                reset();
+                if (_other)
+                    nullableValue = new (storage) T(*_other);
+            }
+            return *this;
+        }
+        Option& operator=(T const& _value) {
+            reset();
+            nullableValue = new (storage) T(_value);
+            return *this;
+        }
+
+        void reset() {
+            if (nullableValue)
+                nullableValue->~T();
+            nullableValue = nullptr;
+        }
+
+        T& operator*() { return *nullableValue; }
+        T const& operator*() const { return *nullableValue; }
+        T* operator->() { return nullableValue; }
+        const T* operator->() const { return nullableValue; }
+
+        T valueOr(T const& defaultValue) const {
+            return nullableValue ? *nullableValue : defaultValue;
+        }
+
+        bool some() const { return nullableValue != nullptr; }
+        bool none() const { return nullableValue == nullptr; }
+
+        bool operator!() const { return nullableValue == nullptr; }
+        explicit operator bool() const { return some(); }
+
+    private:
+        T* nullableValue;
+        alignas(alignof(T)) char storage[sizeof(T)];
+    };
+
+} // end namespace Catch
+
+// end catch_option.hpp
+#include <chrono>
+#include <iosfwd>
+#include <string>
+#include <vector>
+#include <memory>
+
+namespace Catch {
+
+    enum class Verbosity { Quiet = 0, Normal, High };
+
+    struct WarnAbout {
+        enum What { Nothing = 0x00, NoAssertions = 0x01, NoTests = 0x02 };
+    };
+
+    struct ShowDurations {
+        enum OrNot { DefaultForReporter, Always, Never };
+    };
+    struct RunTests {
+        enum InWhatOrder { InDeclarationOrder, InLexicographicalOrder, InRandomOrder };
+    };
+    struct UseColour {
+        enum YesOrNo { Auto, Yes, No };
+    };
+    struct WaitForKeypress {
+        enum When {
+            Never,
+            BeforeStart = 1,
+            BeforeExit = 2,
+            BeforeStartAndExit = BeforeStart | BeforeExit
+        };
+    };
+
+    class TestSpec;
+
+    struct IConfig : NonCopyable {
+
+        virtual ~IConfig();
+
+        virtual bool allowThrows() const = 0;
+        virtual std::ostream& stream() const = 0;
+        virtual std::string name() const = 0;
+        virtual bool includeSuccessfulResults() const = 0;
+        virtual bool shouldDebugBreak() const = 0;
+        virtual bool warnAboutMissingAssertions() const = 0;
+        virtual bool warnAboutNoTests() const = 0;
+        virtual int abortAfter() const = 0;
+        virtual bool showInvisibles() const = 0;
+        virtual ShowDurations::OrNot showDurations() const = 0;
+        virtual double minDuration() const = 0;
+        virtual TestSpec const& testSpec() const = 0;
+        virtual bool hasTestFilters() const = 0;
+        virtual std::vector<std::string> const& getTestsOrTags() const = 0;
+        virtual RunTests::InWhatOrder runOrder() const = 0;
+        virtual unsigned int rngSeed() const = 0;
+        virtual UseColour::YesOrNo useColour() const = 0;
+        virtual std::vector<std::string> const& getSectionsToRun() const = 0;
+        virtual Verbosity verbosity() const = 0;
+
+        virtual bool benchmarkNoAnalysis() const = 0;
+        virtual int benchmarkSamples() const = 0;
+        virtual double benchmarkConfidenceInterval() const = 0;
+        virtual unsigned int benchmarkResamples() const = 0;
+        virtual std::chrono::milliseconds benchmarkWarmupTime() const = 0;
+    };
+
+    using IConfigPtr = std::shared_ptr<IConfig const>;
+} // namespace Catch
+
+// end catch_interfaces_config.h
+// start catch_random_number_generator.h
+
+#include <cstdint>
+
+namespace Catch {
+
+    // This is a simple implementation of C++11 Uniform Random Number
+    // Generator. It does not provide all operators, because Catch2
+    // does not use it, but it should behave as expected inside stdlib's
+    // distributions.
+    // The implementation is based on the PCG family (http://pcg-random.org)
+    class SimplePcg32 {
+        using state_type = std::uint64_t;
+
+    public:
+        using result_type = std::uint32_t;
+        static constexpr result_type(min)() { return 0; }
+        static constexpr result_type(max)() { return static_cast<result_type>(-1); }
+
+        // Provide some default initial state for the default constructor
+        SimplePcg32() : SimplePcg32(0xed743cc4U) {}
+
+        explicit SimplePcg32(result_type seed_);
+
+        void seed(result_type seed_);
+        void discard(uint64_t skip);
+
+        result_type operator()();
+
+    private:
+        friend bool operator==(SimplePcg32 const& lhs, SimplePcg32 const& rhs);
+        friend bool operator!=(SimplePcg32 const& lhs, SimplePcg32 const& rhs);
+
+        // In theory we also need operator<< and operator>>
+        // In practice we do not use them, so we will skip them for now
+
+        std::uint64_t m_state;
+        // This part of the state determines which "stream" of the numbers
+        // is chosen -- we take it as a constant for Catch2, so we only
+        // need to deal with seeding the main state.
+        // Picked by reading 8 bytes from `/dev/random` :-)
+        static const std::uint64_t s_inc = (0x13ed0cc53f939476ULL << 1ULL) | 1ULL;
+    };
+
+} // end namespace Catch
+
+// end catch_random_number_generator.h
+#include <random>
+
+namespace Catch {
+    namespace Generators {
+
+        template <typename Float> class RandomFloatingGenerator final : public IGenerator<Float> {
+            Catch::SimplePcg32& m_rng;
+            std::uniform_real_distribution<Float> m_dist;
+            Float m_current_number;
+
+        public:
+            RandomFloatingGenerator(Float a, Float b) : m_rng(rng()), m_dist(a, b) {
+                static_cast<void>(next());
+            }
+
+            Float const& get() const override { return m_current_number; }
+            bool next() override {
+                m_current_number = m_dist(m_rng);
+                return true;
+            }
+        };
+
+        template <typename Integer>
+        class RandomIntegerGenerator final : public IGenerator<Integer> {
+            Catch::SimplePcg32& m_rng;
+            std::uniform_int_distribution<Integer> m_dist;
+            Integer m_current_number;
+
+        public:
+            RandomIntegerGenerator(Integer a, Integer b) : m_rng(rng()), m_dist(a, b) {
+                static_cast<void>(next());
+            }
+
+            Integer const& get() const override { return m_current_number; }
+            bool next() override {
+                m_current_number = m_dist(m_rng);
+                return true;
+            }
+        };
+
+        // TODO: Ideally this would be also constrained against the various char
+        // types,
+        //       but I don't expect users to run into that in practice.
+        template <typename T>
+        typename std::enable_if<std::is_integral<T>::value && !std::is_same<T, bool>::value,
+                                GeneratorWrapper<T>>::type
+        random(T a, T b) {
+            return GeneratorWrapper<T>(pf::make_unique<RandomIntegerGenerator<T>>(a, b));
+        }
+
+        template <typename T>
+        typename std::enable_if<std::is_floating_point<T>::value, GeneratorWrapper<T>>::type
+        random(T a, T b) {
+            return GeneratorWrapper<T>(pf::make_unique<RandomFloatingGenerator<T>>(a, b));
+        }
+
+        template <typename T> class RangeGenerator final : public IGenerator<T> {
+            T m_current;
+            T m_end;
+            T m_step;
+            bool m_positive;
+
+        public:
+            RangeGenerator(T const& start, T const& end, T const& step) :
+                m_current(start), m_end(end), m_step(step), m_positive(m_step > T(0)) {
+                assert(m_current != m_end && "Range start and end cannot be equal");
+                assert(m_step != T(0) && "Step size cannot be zero");
+                assert(
+                    ((m_positive && m_current <= m_end) || (!m_positive && m_current >= m_end)) &&
+                    "Step moves away from end");
+            }
+
+            RangeGenerator(T const& start, T const& end) :
+                RangeGenerator(start, end, (start < end) ? T(1) : T(-1)) {}
+
+            T const& get() const override { return m_current; }
+
+            bool next() override {
+                m_current += m_step;
+                return (m_positive) ? (m_current < m_end) : (m_current > m_end);
+            }
+        };
+
+        template <typename T>
+        GeneratorWrapper<T> range(T const& start, T const& end, T const& step) {
+            static_assert(std::is_arithmetic<T>::value && !std::is_same<T, bool>::value,
+                          "Type must be numeric");
+            return GeneratorWrapper<T>(pf::make_unique<RangeGenerator<T>>(start, end, step));
+        }
+
+        template <typename T> GeneratorWrapper<T> range(T const& start, T const& end) {
+            static_assert(std::is_integral<T>::value && !std::is_same<T, bool>::value,
+                          "Type must be an integer");
+            return GeneratorWrapper<T>(pf::make_unique<RangeGenerator<T>>(start, end));
+        }
+
+        template <typename T> class IteratorGenerator final : public IGenerator<T> {
+            static_assert(!std::is_same<T, bool>::value,
+                          "IteratorGenerator currently does not support bools"
+                          "because of std::vector<bool> specialization");
+
+            std::vector<T> m_elems;
+            size_t m_current = 0;
+
+        public:
+            template <typename InputIterator, typename InputSentinel>
+            IteratorGenerator(InputIterator first, InputSentinel last) : m_elems(first, last) {
+                if (m_elems.empty()) {
+                    Catch::throw_exception(
+                        GeneratorException("IteratorGenerator received no valid values"));
+                }
+            }
+
+            T const& get() const override { return m_elems[m_current]; }
+
+            bool next() override {
+                ++m_current;
+                return m_current != m_elems.size();
+            }
+        };
+
+        template <typename InputIterator,
+                  typename InputSentinel,
+                  typename ResultType = typename std::iterator_traits<InputIterator>::value_type>
+        GeneratorWrapper<ResultType> from_range(InputIterator from, InputSentinel to) {
+            return GeneratorWrapper<ResultType>(
+                pf::make_unique<IteratorGenerator<ResultType>>(from, to));
+        }
+
+        template <typename Container, typename ResultType = typename Container::value_type>
+        GeneratorWrapper<ResultType> from_range(Container const& cnt) {
+            return GeneratorWrapper<ResultType>(
+                pf::make_unique<IteratorGenerator<ResultType>>(cnt.begin(), cnt.end()));
+        }
+
+    } // namespace Generators
+} // namespace Catch
+
+// end catch_generators_specific.hpp
+
+// These files are included here so the single_include script doesn't put them
+// in the conditionally compiled sections
+// start catch_test_case_info.h
+
+#include <string>
+#include <vector>
+#include <memory>
+
+#ifdef __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wpadded"
+#endif
+
+namespace Catch {
+
+    struct ITestInvoker;
+
+    struct TestCaseInfo {
+        enum SpecialProperties {
+            None = 0,
+            IsHidden = 1 << 1,
+            ShouldFail = 1 << 2,
+            MayFail = 1 << 3,
+            Throws = 1 << 4,
+            NonPortable = 1 << 5,
+            Benchmark = 1 << 6
+        };
+
+        TestCaseInfo(std::string const& _name,
+                     std::string const& _className,
+                     std::string const& _description,
+                     std::vector<std::string> const& _tags,
+                     SourceLineInfo const& _lineInfo);
+
+        friend void setTags(TestCaseInfo& testCaseInfo, std::vector<std::string> tags);
+
+        bool isHidden() const;
+        bool throws() const;
+        bool okToFail() const;
+        bool expectedToFail() const;
+
+        std::string tagsAsString() const;
+
+        std::string name;
+        std::string className;
+        std::string description;
+        std::vector<std::string> tags;
+        std::vector<std::string> lcaseTags;
+        SourceLineInfo lineInfo;
+        SpecialProperties properties;
+    };
+
+    class TestCase : public TestCaseInfo {
+    public:
+        TestCase(ITestInvoker* testCase, TestCaseInfo&& info);
+
+        TestCase withName(std::string const& _newName) const;
+
+        void invoke() const;
+
+        TestCaseInfo const& getTestCaseInfo() const;
+
+        bool operator==(TestCase const& other) const;
+        bool operator<(TestCase const& other) const;
+
+    private:
+        std::shared_ptr<ITestInvoker> test;
+    };
+
+    TestCase makeTestCase(ITestInvoker* testCase,
+                          std::string const& className,
+                          NameAndTags const& nameAndTags,
+                          SourceLineInfo const& lineInfo);
+} // namespace Catch
+
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
+
+// end catch_test_case_info.h
+// start catch_interfaces_runner.h
+
+namespace Catch {
+
+    struct IRunner {
+        virtual ~IRunner();
+        virtual bool aborting() const = 0;
+    };
+} // namespace Catch
+
+// end catch_interfaces_runner.h
+
+#ifdef __OBJC__
+// start catch_objc.hpp
+
+#import <objc/runtime.h>
+
+#include <string>
+
+// NB. Any general catch headers included here must be included
+// in catch.hpp first to make sure they are included by the single
+// header for non obj-usage
+
+///////////////////////////////////////////////////////////////////////////////
+// This protocol is really only here for (self) documenting purposes, since
+// all its methods are optional.
+@protocol OcFixture
+
+@optional
+
+- (void)setUp;
+- (void)tearDown;
+
+@end
+
+namespace Catch {
+
+    class OcMethod : public ITestInvoker {
+
+    public:
+        OcMethod(Class cls, SEL sel) : m_cls(cls), m_sel(sel) {}
+
+        virtual void invoke() const {
+            id obj = [[m_cls alloc] init];
+
+            performOptionalSelector(obj, @selector(setUp));
+            performOptionalSelector(obj, m_sel);
+            performOptionalSelector(obj, @selector(tearDown));
+
+            arcSafeRelease(obj);
+        }
+
+    private:
+        virtual ~OcMethod() {}
+
+        Class m_cls;
+        SEL m_sel;
+    };
+
+    namespace Detail {
+
+        inline std::string getAnnotation(Class cls,
+                                         std::string const& annotationName,
+                                         std::string const& testCaseName) {
+            NSString* selStr = [[NSString alloc]
+                initWithFormat:@"Catch_%s_%s", annotationName.c_str(), testCaseName.c_str()];
+            SEL sel = NSSelectorFromString(selStr);
+            arcSafeRelease(selStr);
+            id value = performOptionalSelector(cls, sel);
+            if (value)
+                return [(NSString*)value UTF8String];
+            return "";
+        }
+    } // namespace Detail
+
+    inline std::size_t registerTestMethods() {
+        std::size_t noTestMethods = 0;
+        int noClasses = objc_getClassList(nullptr, 0);
+
+        Class* classes = (CATCH_UNSAFE_UNRETAINED Class*)malloc(sizeof(Class) * noClasses);
+        objc_getClassList(classes, noClasses);
+
+        for (int c = 0; c < noClasses; c++) {
+            Class cls = classes[c];
+            {
+                u_int count;
+                Method* methods = class_copyMethodList(cls, &count);
+                for (u_int m = 0; m < count; m++) {
+                    SEL selector = method_getName(methods[m]);
+                    std::string methodName = sel_getName(selector);
+                    if (startsWith(methodName, "Catch_TestCase_")) {
+                        std::string testCaseName = methodName.substr(15);
+                        std::string name = Detail::getAnnotation(cls, "Name", testCaseName);
+                        std::string desc = Detail::getAnnotation(cls, "Description", testCaseName);
+                        const char* className = class_getName(cls);
+
+                        getMutableRegistryHub().registerTest(
+                            makeTestCase(new OcMethod(cls, selector),
+                                         className,
+                                         NameAndTags(name.c_str(), desc.c_str()),
+                                         SourceLineInfo("", 0)));
+                        noTestMethods++;
+                    }
+                }
+                free(methods);
+            }
+        }
+        return noTestMethods;
+    }
+
+#if !defined(CATCH_CONFIG_DISABLE_MATCHERS)
+
+    namespace Matchers {
+        namespace Impl {
+            namespace NSStringMatchers {
+
+                struct StringHolder : MatcherBase<NSString*> {
+                    StringHolder(NSString* substr) : m_substr([substr copy]) {}
+                    StringHolder(StringHolder const& other) : m_substr([other.m_substr copy]) {}
+                    StringHolder() { arcSafeRelease(m_substr); }
+
+                    bool match(NSString* str) const override { return false; }
+
+                    NSString* CATCH_ARC_STRONG m_substr;
+                };
+
+                struct Equals : StringHolder {
+                    Equals(NSString* substr) : StringHolder(substr) {}
+
+                    bool match(NSString* str) const override {
+                        return (str != nil || m_substr == nil) && [str isEqualToString:m_substr];
+                    }
+
+                    std::string describe() const override {
+                        return "equals string: " + Catch::Detail::stringify(m_substr);
+                    }
+                };
+
+                struct Contains : StringHolder {
+                    Contains(NSString* substr) : StringHolder(substr) {}
+
+                    bool match(NSString* str) const override {
+                        return (str != nil || m_substr == nil) &&
+                               [str rangeOfString:m_substr].location != NSNotFound;
+                    }
+
+                    std::string describe() const override {
+                        return "contains string: " + Catch::Detail::stringify(m_substr);
+                    }
+                };
+
+                struct StartsWith : StringHolder {
+                    StartsWith(NSString* substr) : StringHolder(substr) {}
+
+                    bool match(NSString* str) const override {
+                        return (str != nil || m_substr == nil) &&
+                               [str rangeOfString:m_substr].location == 0;
+                    }
+
+                    std::string describe() const override {
+                        return "starts with: " + Catch::Detail::stringify(m_substr);
+                    }
+                };
+                struct EndsWith : StringHolder {
+                    EndsWith(NSString* substr) : StringHolder(substr) {}
+
+                    bool match(NSString* str) const override {
+                        return (str != nil || m_substr == nil) &&
+                               [str rangeOfString:m_substr].location ==
+                                   [str length] - [m_substr length];
+                    }
+
+                    std::string describe() const override {
+                        return "ends with: " + Catch::Detail::stringify(m_substr);
+                    }
+                };
+
+            } // namespace NSStringMatchers
+        }     // namespace Impl
+
+        inline Impl::NSStringMatchers::Equals Equals(NSString* substr) {
+            return Impl::NSStringMatchers::Equals(substr);
+        }
+
+        inline Impl::NSStringMatchers::Contains Contains(NSString* substr) {
+            return Impl::NSStringMatchers::Contains(substr);
+        }
+
+        inline Impl::NSStringMatchers::StartsWith StartsWith(NSString* substr) {
+            return Impl::NSStringMatchers::StartsWith(substr);
+        }
+
+        inline Impl::NSStringMatchers::EndsWith EndsWith(NSString* substr) {
+            return Impl::NSStringMatchers::EndsWith(substr);
+        }
+
+    } // namespace Matchers
+
+    using namespace Matchers;
+
+#endif // CATCH_CONFIG_DISABLE_MATCHERS
+
+} // namespace Catch
+
+///////////////////////////////////////////////////////////////////////////////
+#define OC_MAKE_UNIQUE_NAME(root, uniqueSuffix) root##uniqueSuffix
+#define OC_TEST_CASE2(name, desc, uniqueSuffix)                                                    \
+    +(NSString*)OC_MAKE_UNIQUE_NAME(Catch_Name_test_, uniqueSuffix) {                              \
+        return @name;                                                                              \
+    }                                                                                              \
+    +(NSString*)OC_MAKE_UNIQUE_NAME(Catch_Description_test_, uniqueSuffix) {                       \
+        return @desc;                                                                              \
+    }                                                                                              \
+    -(void)OC_MAKE_UNIQUE_NAME(Catch_TestCase_test_, uniqueSuffix)
+
+#define OC_TEST_CASE(name, desc) OC_TEST_CASE2(name, desc, __LINE__)
+
+// end catch_objc.hpp
+#endif
+
+// Benchmarking needs the externally-facing parts of reporters to work
+#if defined(CATCH_CONFIG_EXTERNAL_INTERFACES) || defined(CATCH_CONFIG_ENABLE_BENCHMARKING)
+// start catch_external_interfaces.h
+
+// start catch_reporter_bases.hpp
+
+// start catch_interfaces_reporter.h
+
+// start catch_config.hpp
+
+// start catch_test_spec_parser.h
+
+#ifdef __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wpadded"
+#endif
+
+// start catch_test_spec.h
+
+#ifdef __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wpadded"
+#endif
+
+// start catch_wildcard_pattern.h
+
+namespace Catch {
+    class WildcardPattern {
+        enum WildcardPosition {
+            NoWildcard = 0,
+            WildcardAtStart = 1,
+            WildcardAtEnd = 2,
+            WildcardAtBothEnds = WildcardAtStart | WildcardAtEnd
+        };
+
+    public:
+        WildcardPattern(std::string const& pattern, CaseSensitive::Choice caseSensitivity);
+        virtual ~WildcardPattern() = default;
+        virtual bool matches(std::string const& str) const;
+
+    private:
+        std::string normaliseString(std::string const& str) const;
+        CaseSensitive::Choice m_caseSensitivity;
+        WildcardPosition m_wildcard = NoWildcard;
+        std::string m_pattern;
+    };
+} // namespace Catch
+
+// end catch_wildcard_pattern.h
+#include <string>
+#include <vector>
+#include <memory>
+
+namespace Catch {
+
+    struct IConfig;
+
+    class TestSpec {
+        class Pattern {
+        public:
+            explicit Pattern(std::string const& name);
+            virtual ~Pattern();
+            virtual bool matches(TestCaseInfo const& testCase) const = 0;
+            std::string const& name() const;
+
+        private:
+            std::string const m_name;
+        };
+        using PatternPtr = std::shared_ptr<Pattern>;
+
+        class NamePattern : public Pattern {
+        public:
+            explicit NamePattern(std::string const& name, std::string const& filterString);
+            bool matches(TestCaseInfo const& testCase) const override;
+
+        private:
+            WildcardPattern m_wildcardPattern;
+        };
+
+        class TagPattern : public Pattern {
+        public:
+            explicit TagPattern(std::string const& tag, std::string const& filterString);
+            bool matches(TestCaseInfo const& testCase) const override;
+
+        private:
+            std::string m_tag;
+        };
+
+        class ExcludedPattern : public Pattern {
+        public:
+            explicit ExcludedPattern(PatternPtr const& underlyingPattern);
+            bool matches(TestCaseInfo const& testCase) const override;
+
+        private:
+            PatternPtr m_underlyingPattern;
+        };
+
+        struct Filter {
+            std::vector<PatternPtr> m_patterns;
+
+            bool matches(TestCaseInfo const& testCase) const;
+            std::string name() const;
+        };
+
+    public:
+        struct FilterMatch {
+            std::string name;
+            std::vector<TestCase const*> tests;
+        };
+        using Matches = std::vector<FilterMatch>;
+        using vectorStrings = std::vector<std::string>;
+
+        bool hasFilters() const;
+        bool matches(TestCaseInfo const& testCase) const;
+        Matches matchesByFilter(std::vector<TestCase> const& testCases,
+                                IConfig const& config) const;
+        const vectorStrings& getInvalidArgs() const;
+
+    private:
+        std::vector<Filter> m_filters;
+        std::vector<std::string> m_invalidArgs;
+        friend class TestSpecParser;
+    };
+} // namespace Catch
+
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
+
+// end catch_test_spec.h
+// start catch_interfaces_tag_alias_registry.h
+
+#include <string>
+
+namespace Catch {
+
+    struct TagAlias;
+
+    struct ITagAliasRegistry {
+        virtual ~ITagAliasRegistry();
+        // Nullptr if not present
+        virtual TagAlias const* find(std::string const& alias) const = 0;
+        virtual std::string expandAliases(std::string const& unexpandedTestSpec) const = 0;
+
+        static ITagAliasRegistry const& get();
+    };
+
+} // end namespace Catch
+
+// end catch_interfaces_tag_alias_registry.h
+namespace Catch {
+
+    class TestSpecParser {
+        enum Mode { None, Name, QuotedName, Tag, EscapedName };
+        Mode m_mode = None;
+        Mode lastMode = None;
+        bool m_exclusion = false;
+        std::size_t m_pos = 0;
+        std::size_t m_realPatternPos = 0;
+        std::string m_arg;
+        std::string m_substring;
+        std::string m_patternName;
+        std::vector<std::size_t> m_escapeChars;
+        TestSpec::Filter m_currentFilter;
+        TestSpec m_testSpec;
+        ITagAliasRegistry const* m_tagAliases = nullptr;
+
+    public:
+        TestSpecParser(ITagAliasRegistry const& tagAliases);
+
+        TestSpecParser& parse(std::string const& arg);
+        TestSpec testSpec();
+
+    private:
+        bool visitChar(char c);
+        void startNewMode(Mode mode);
+        bool processNoneChar(char c);
+        void processNameChar(char c);
+        bool processOtherChar(char c);
+        void endMode();
+        void escape();
+        bool isControlChar(char c) const;
+        void saveLastMode();
+        void revertBackToLastMode();
+        void addFilter();
+        bool separate();
+
+        // Handles common preprocessing of the pattern for name/tag patterns
+        std::string preprocessPattern();
+        // Adds the current pattern as a test name
+        void addNamePattern();
+        // Adds the current pattern as a tag
+        void addTagPattern();
+
+        inline void addCharToPattern(char c) {
+            m_substring += c;
+            m_patternName += c;
+            m_realPatternPos++;
+        }
+    };
+    TestSpec parseTestSpec(std::string const& arg);
+
+} // namespace Catch
+
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
+
+// end catch_test_spec_parser.h
+// Libstdc++ doesn't like incomplete classes for unique_ptr
+
+#include <memory>
+#include <vector>
+#include <string>
+
+#ifndef CATCH_CONFIG_CONSOLE_WIDTH
+#define CATCH_CONFIG_CONSOLE_WIDTH 80
+#endif
+
+namespace Catch {
+
+    struct IStream;
+
+    struct ConfigData {
+        bool listTests = false;
+        bool listTags = false;
+        bool listReporters = false;
+        bool listTestNamesOnly = false;
+
+        bool showSuccessfulTests = false;
+        bool shouldDebugBreak = false;
+        bool noThrow = false;
+        bool showHelp = false;
+        bool showInvisibles = false;
+        bool filenamesAsTags = false;
+        bool libIdentify = false;
+
+        int abortAfter = -1;
+        unsigned int rngSeed = 0;
+
+        bool benchmarkNoAnalysis = false;
+        unsigned int benchmarkSamples = 100;
+        double benchmarkConfidenceInterval = 0.95;
+        unsigned int benchmarkResamples = 100000;
+        std::chrono::milliseconds::rep benchmarkWarmupTime = 100;
+
+        Verbosity verbosity = Verbosity::Normal;
+        WarnAbout::What warnings = WarnAbout::Nothing;
+        ShowDurations::OrNot showDurations = ShowDurations::DefaultForReporter;
+        double minDuration = -1;
+        RunTests::InWhatOrder runOrder = RunTests::InDeclarationOrder;
+        UseColour::YesOrNo useColour = UseColour::Auto;
+        WaitForKeypress::When waitForKeypress = WaitForKeypress::Never;
+
+        std::string outputFilename;
+        std::string name;
+        std::string processName;
+#ifndef CATCH_CONFIG_DEFAULT_REPORTER
+#define CATCH_CONFIG_DEFAULT_REPORTER "console"
+#endif
+        std::string reporterName = CATCH_CONFIG_DEFAULT_REPORTER;
+#undef CATCH_CONFIG_DEFAULT_REPORTER
+
+        std::vector<std::string> testsOrTags;
+        std::vector<std::string> sectionsToRun;
+    };
+
+    class Config : public IConfig {
+    public:
+        Config() = default;
+        Config(ConfigData const& data);
+        virtual ~Config() = default;
+
+        std::string const& getFilename() const;
+
+        bool listTests() const;
+        bool listTestNamesOnly() const;
+        bool listTags() const;
+        bool listReporters() const;
+
+        std::string getProcessName() const;
+        std::string const& getReporterName() const;
+
+        std::vector<std::string> const& getTestsOrTags() const override;
+        std::vector<std::string> const& getSectionsToRun() const override;
+
+        TestSpec const& testSpec() const override;
+        bool hasTestFilters() const override;
+
+        bool showHelp() const;
+
+        // IConfig interface
+        bool allowThrows() const override;
+        std::ostream& stream() const override;
+        std::string name() const override;
+        bool includeSuccessfulResults() const override;
+        bool warnAboutMissingAssertions() const override;
+        bool warnAboutNoTests() const override;
+        ShowDurations::OrNot showDurations() const override;
+        double minDuration() const override;
+        RunTests::InWhatOrder runOrder() const override;
+        unsigned int rngSeed() const override;
+        UseColour::YesOrNo useColour() const override;
+        bool shouldDebugBreak() const override;
+        int abortAfter() const override;
+        bool showInvisibles() const override;
+        Verbosity verbosity() const override;
+        bool benchmarkNoAnalysis() const override;
+        int benchmarkSamples() const override;
+        double benchmarkConfidenceInterval() const override;
+        unsigned int benchmarkResamples() const override;
+        std::chrono::milliseconds benchmarkWarmupTime() const override;
+
+    private:
+        IStream const* openStream();
+        ConfigData m_data;
+
+        std::unique_ptr<IStream const> m_stream;
+        TestSpec m_testSpec;
+        bool m_hasTestFilters = false;
+    };
+
+} // end namespace Catch
+
+// end catch_config.hpp
+// start catch_assertionresult.h
+
+#include <string>
+
+namespace Catch {
+
+    struct AssertionResultData {
+        AssertionResultData() = delete;
+
+        AssertionResultData(ResultWas::OfType _resultType, LazyExpression const& _lazyExpression);
+
+        std::string message;
+        mutable std::string reconstructedExpression;
+        LazyExpression lazyExpression;
+        ResultWas::OfType resultType;
+
+        std::string reconstructExpression() const;
+    };
+
+    class AssertionResult {
+    public:
+        AssertionResult() = delete;
+        AssertionResult(AssertionInfo const& info, AssertionResultData const& data);
+
+        bool isOk() const;
+        bool succeeded() const;
+        ResultWas::OfType getResultType() const;
+        bool hasExpression() const;
+        bool hasMessage() const;
+        std::string getExpression() const;
+        std::string getExpressionInMacro() const;
+        bool hasExpandedExpression() const;
+        std::string getExpandedExpression() const;
+        std::string getMessage() const;
+        SourceLineInfo getSourceInfo() const;
+        StringRef getTestMacroName() const;
+
+        // protected:
+        AssertionInfo m_info;
+        AssertionResultData m_resultData;
+    };
+
+} // end namespace Catch
+
+// end catch_assertionresult.h
+#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING)
+// start catch_estimate.hpp
+
+// Statistics estimates
+
+namespace Catch {
+    namespace Benchmark {
+        template <typename Duration> struct Estimate {
+            Duration point;
+            Duration lower_bound;
+            Duration upper_bound;
+            double confidence_interval;
+
+            template <typename Duration2> operator Estimate<Duration2>() const {
+                return {point, lower_bound, upper_bound, confidence_interval};
+            }
+        };
+    } // namespace Benchmark
+} // namespace Catch
+
+// end catch_estimate.hpp
+// start catch_outlier_classification.hpp
+
+// Outlier information
+
+namespace Catch {
+    namespace Benchmark {
+        struct OutlierClassification {
+            int samples_seen = 0;
+            int low_severe = 0;  // more than 3 times IQR below Q1
+            int low_mild = 0;    // 1.5 to 3 times IQR below Q1
+            int high_mild = 0;   // 1.5 to 3 times IQR above Q3
+            int high_severe = 0; // more than 3 times IQR above Q3
+
+            int total() const { return low_severe + low_mild + high_mild + high_severe; }
+        };
+    } // namespace Benchmark
+} // namespace Catch
+
+// end catch_outlier_classification.hpp
+
+#include <iterator>
+#endif // CATCH_CONFIG_ENABLE_BENCHMARKING
+
+#include <string>
+#include <iosfwd>
+#include <map>
+#include <set>
+#include <memory>
+#include <algorithm>
+
+namespace Catch {
+
+    struct ReporterConfig {
+        explicit ReporterConfig(IConfigPtr const& _fullConfig);
+
+        ReporterConfig(IConfigPtr const& _fullConfig, std::ostream& _stream);
+
+        std::ostream& stream() const;
+        IConfigPtr fullConfig() const;
+
+    private:
+        std::ostream* m_stream;
+        IConfigPtr m_fullConfig;
+    };
+
+    struct ReporterPreferences {
+        bool shouldRedirectStdOut = false;
+        bool shouldReportAllAssertions = false;
+    };
+
+    template <typename T> struct LazyStat : Option<T> {
+        LazyStat& operator=(T const& _value) {
+            Option<T>::operator=(_value);
+            used = false;
+            return *this;
+        }
+        void reset() {
+            Option<T>::reset();
+            used = false;
+        }
+        bool used = false;
+    };
+
+    struct TestRunInfo {
+        TestRunInfo(std::string const& _name);
+        std::string name;
+    };
+    struct GroupInfo {
+        GroupInfo(std::string const& _name, std::size_t _groupIndex, std::size_t _groupsCount);
+
+        std::string name;
+        std::size_t groupIndex;
+        std::size_t groupsCounts;
+    };
+
+    struct AssertionStats {
+        AssertionStats(AssertionResult const& _assertionResult,
+                       std::vector<MessageInfo> const& _infoMessages,
+                       Totals const& _totals);
+
+        AssertionStats(AssertionStats const&) = default;
+        AssertionStats(AssertionStats&&) = default;
+        AssertionStats& operator=(AssertionStats const&) = delete;
+        AssertionStats& operator=(AssertionStats&&) = delete;
+        virtual ~AssertionStats();
+
+        AssertionResult assertionResult;
+        std::vector<MessageInfo> infoMessages;
+        Totals totals;
+    };
+
+    struct SectionStats {
+        SectionStats(SectionInfo const& _sectionInfo,
+                     Counts const& _assertions,
+                     double _durationInSeconds,
+                     bool _missingAssertions);
+        SectionStats(SectionStats const&) = default;
+        SectionStats(SectionStats&&) = default;
+        SectionStats& operator=(SectionStats const&) = default;
+        SectionStats& operator=(SectionStats&&) = default;
+        virtual ~SectionStats();
+
+        SectionInfo sectionInfo;
+        Counts assertions;
+        double durationInSeconds;
+        bool missingAssertions;
+    };
+
+    struct TestCaseStats {
+        TestCaseStats(TestCaseInfo const& _testInfo,
+                      Totals const& _totals,
+                      std::string const& _stdOut,
+                      std::string const& _stdErr,
+                      bool _aborting);
+
+        TestCaseStats(TestCaseStats const&) = default;
+        TestCaseStats(TestCaseStats&&) = default;
+        TestCaseStats& operator=(TestCaseStats const&) = default;
+        TestCaseStats& operator=(TestCaseStats&&) = default;
+        virtual ~TestCaseStats();
+
+        TestCaseInfo testInfo;
+        Totals totals;
+        std::string stdOut;
+        std::string stdErr;
+        bool aborting;
+    };
+
+    struct TestGroupStats {
+        TestGroupStats(GroupInfo const& _groupInfo, Totals const& _totals, bool _aborting);
+        TestGroupStats(GroupInfo const& _groupInfo);
+
+        TestGroupStats(TestGroupStats const&) = default;
+        TestGroupStats(TestGroupStats&&) = default;
+        TestGroupStats& operator=(TestGroupStats const&) = default;
+        TestGroupStats& operator=(TestGroupStats&&) = default;
+        virtual ~TestGroupStats();
+
+        GroupInfo groupInfo;
+        Totals totals;
+        bool aborting;
+    };
+
+    struct TestRunStats {
+        TestRunStats(TestRunInfo const& _runInfo, Totals const& _totals, bool _aborting);
+
+        TestRunStats(TestRunStats const&) = default;
+        TestRunStats(TestRunStats&&) = default;
+        TestRunStats& operator=(TestRunStats const&) = default;
+        TestRunStats& operator=(TestRunStats&&) = default;
+        virtual ~TestRunStats();
+
+        TestRunInfo runInfo;
+        Totals totals;
+        bool aborting;
+    };
+
+#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING)
+    struct BenchmarkInfo {
+        std::string name;
+        double estimatedDuration;
+        int iterations;
+        int samples;
+        unsigned int resamples;
+        double clockResolution;
+        double clockCost;
+    };
+
+    template <class Duration> struct BenchmarkStats {
+        BenchmarkInfo info;
+
+        std::vector<Duration> samples;
+        Benchmark::Estimate<Duration> mean;
+        Benchmark::Estimate<Duration> standardDeviation;
+        Benchmark::OutlierClassification outliers;
+        double outlierVariance;
+
+        template <typename Duration2> operator BenchmarkStats<Duration2>() const {
+            std::vector<Duration2> samples2;
+            samples2.reserve(samples.size());
+            std::transform(samples.begin(),
+                           samples.end(),
+                           std::back_inserter(samples2),
+                           [](Duration d) { return Duration2(d); });
+            return {
+                info,
+                std::move(samples2),
+                mean,
+                standardDeviation,
+                outliers,
+                outlierVariance,
+            };
+        }
+    };
+#endif // CATCH_CONFIG_ENABLE_BENCHMARKING
+
+    struct IStreamingReporter {
+        virtual ~IStreamingReporter() = default;
+
+        // Implementing class must also provide the following static methods:
+        // static std::string getDescription();
+        // static std::set<Verbosity> getSupportedVerbosities()
+
+        virtual ReporterPreferences getPreferences() const = 0;
+
+        virtual void noMatchingTestCases(std::string const& spec) = 0;
+
+        virtual void reportInvalidArguments(std::string const&) {}
+
+        virtual void testRunStarting(TestRunInfo const& testRunInfo) = 0;
+        virtual void testGroupStarting(GroupInfo const& groupInfo) = 0;
+
+        virtual void testCaseStarting(TestCaseInfo const& testInfo) = 0;
+        virtual void sectionStarting(SectionInfo const& sectionInfo) = 0;
+
+#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING)
+        virtual void benchmarkPreparing(std::string const&) {}
+        virtual void benchmarkStarting(BenchmarkInfo const&) {}
+        virtual void benchmarkEnded(BenchmarkStats<> const&) {}
+        virtual void benchmarkFailed(std::string const&) {}
+#endif // CATCH_CONFIG_ENABLE_BENCHMARKING
+
+        virtual void assertionStarting(AssertionInfo const& assertionInfo) = 0;
+
+        // The return value indicates if the messages buffer should be cleared:
+        virtual bool assertionEnded(AssertionStats const& assertionStats) = 0;
+
+        virtual void sectionEnded(SectionStats const& sectionStats) = 0;
+        virtual void testCaseEnded(TestCaseStats const& testCaseStats) = 0;
+        virtual void testGroupEnded(TestGroupStats const& testGroupStats) = 0;
+        virtual void testRunEnded(TestRunStats const& testRunStats) = 0;
+
+        virtual void skipTest(TestCaseInfo const& testInfo) = 0;
+
+        // Default empty implementation provided
+        virtual void fatalErrorEncountered(StringRef name);
+
+        virtual bool isMulti() const;
+    };
+    using IStreamingReporterPtr = std::unique_ptr<IStreamingReporter>;
+
+    struct IReporterFactory {
+        virtual ~IReporterFactory();
+        virtual IStreamingReporterPtr create(ReporterConfig const& config) const = 0;
+        virtual std::string getDescription() const = 0;
+    };
+    using IReporterFactoryPtr = std::shared_ptr<IReporterFactory>;
+
+    struct IReporterRegistry {
+        using FactoryMap = std::map<std::string, IReporterFactoryPtr>;
+        using Listeners = std::vector<IReporterFactoryPtr>;
+
+        virtual ~IReporterRegistry();
+        virtual IStreamingReporterPtr create(std::string const& name,
+                                             IConfigPtr const& config) const = 0;
+        virtual FactoryMap const& getFactories() const = 0;
+        virtual Listeners const& getListeners() const = 0;
+    };
+
+} // end namespace Catch
+
+// end catch_interfaces_reporter.h
+#include <algorithm>
+#include <cstring>
+#include <cfloat>
+#include <cstdio>
+#include <cassert>
+#include <memory>
+#include <ostream>
+
+namespace Catch {
+    void prepareExpandedExpression(AssertionResult& result);
+
+    // Returns double formatted as %.3f (format expected on output)
+    std::string getFormattedDuration(double duration);
+
+    //! Should the reporter show
+    bool shouldShowDuration(IConfig const& config, double duration);
+
+    std::string serializeFilters(std::vector<std::string> const& container);
+
+    template <typename DerivedT> struct StreamingReporterBase : IStreamingReporter {
+
+        StreamingReporterBase(ReporterConfig const& _config) :
+            m_config(_config.fullConfig()), stream(_config.stream()) {
+            m_reporterPrefs.shouldRedirectStdOut = false;
+            if (!DerivedT::getSupportedVerbosities().count(m_config->verbosity()))
+                CATCH_ERROR("Verbosity level not supported by this reporter");
+        }
+
+        ReporterPreferences getPreferences() const override { return m_reporterPrefs; }
+
+        static std::set<Verbosity> getSupportedVerbosities() { return {Verbosity::Normal}; }
+
+        ~StreamingReporterBase() override = default;
+
+        void noMatchingTestCases(std::string const&) override {}
+
+        void reportInvalidArguments(std::string const&) override {}
+
+        void testRunStarting(TestRunInfo const& _testRunInfo) override {
+            currentTestRunInfo = _testRunInfo;
+        }
+
+        void testGroupStarting(GroupInfo const& _groupInfo) override {
+            currentGroupInfo = _groupInfo;
+        }
+
+        void testCaseStarting(TestCaseInfo const& _testInfo) override {
+            currentTestCaseInfo = _testInfo;
+        }
+        void sectionStarting(SectionInfo const& _sectionInfo) override {
+            m_sectionStack.push_back(_sectionInfo);
+        }
+
+        void sectionEnded(SectionStats const& /* _sectionStats */) override {
+            m_sectionStack.pop_back();
+        }
+        void testCaseEnded(TestCaseStats const& /* _testCaseStats */) override {
+            currentTestCaseInfo.reset();
+        }
+        void testGroupEnded(TestGroupStats const& /* _testGroupStats */) override {
+            currentGroupInfo.reset();
+        }
+        void testRunEnded(TestRunStats const& /* _testRunStats */) override {
+            currentTestCaseInfo.reset();
+            currentGroupInfo.reset();
+            currentTestRunInfo.reset();
+        }
+
+        void skipTest(TestCaseInfo const&) override {
+            // Don't do anything with this by default.
+            // It can optionally be overridden in the derived class.
+        }
+
+        IConfigPtr m_config;
+        std::ostream& stream;
+
+        LazyStat<TestRunInfo> currentTestRunInfo;
+        LazyStat<GroupInfo> currentGroupInfo;
+        LazyStat<TestCaseInfo> currentTestCaseInfo;
+
+        std::vector<SectionInfo> m_sectionStack;
+        ReporterPreferences m_reporterPrefs;
+    };
+
+    template <typename DerivedT> struct CumulativeReporterBase : IStreamingReporter {
+        template <typename T, typename ChildNodeT> struct Node {
+            explicit Node(T const& _value) : value(_value) {}
+            virtual ~Node() {}
+
+            using ChildNodes = std::vector<std::shared_ptr<ChildNodeT>>;
+            T value;
+            ChildNodes children;
+        };
+        struct SectionNode {
+            explicit SectionNode(SectionStats const& _stats) : stats(_stats) {}
+            virtual ~SectionNode() = default;
+
+            bool operator==(SectionNode const& other) const {
+                return stats.sectionInfo.lineInfo == other.stats.sectionInfo.lineInfo;
+            }
+            bool operator==(std::shared_ptr<SectionNode> const& other) const {
+                return operator==(*other);
+            }
+
+            SectionStats stats;
+            using ChildSections = std::vector<std::shared_ptr<SectionNode>>;
+            using Assertions = std::vector<AssertionStats>;
+            ChildSections childSections;
+            Assertions assertions;
+            std::string stdOut;
+            std::string stdErr;
+        };
+
+        struct BySectionInfo {
+            BySectionInfo(SectionInfo const& other) : m_other(other) {}
+            BySectionInfo(BySectionInfo const& other) : m_other(other.m_other) {}
+            bool operator()(std::shared_ptr<SectionNode> const& node) const {
+                return ((node->stats.sectionInfo.name == m_other.name) &&
+                        (node->stats.sectionInfo.lineInfo == m_other.lineInfo));
+            }
+            void operator=(BySectionInfo const&) = delete;
+
+        private:
+            SectionInfo const& m_other;
+        };
+
+        using TestCaseNode = Node<TestCaseStats, SectionNode>;
+        using TestGroupNode = Node<TestGroupStats, TestCaseNode>;
+        using TestRunNode = Node<TestRunStats, TestGroupNode>;
+
+        CumulativeReporterBase(ReporterConfig const& _config) :
+            m_config(_config.fullConfig()), stream(_config.stream()) {
+            m_reporterPrefs.shouldRedirectStdOut = false;
+            if (!DerivedT::getSupportedVerbosities().count(m_config->verbosity()))
+                CATCH_ERROR("Verbosity level not supported by this reporter");
+        }
+        ~CumulativeReporterBase() override = default;
+
+        ReporterPreferences getPreferences() const override { return m_reporterPrefs; }
+
+        static std::set<Verbosity> getSupportedVerbosities() { return {Verbosity::Normal}; }
+
+        void testRunStarting(TestRunInfo const&) override {}
+        void testGroupStarting(GroupInfo const&) override {}
+
+        void testCaseStarting(TestCaseInfo const&) override {}
+
+        void sectionStarting(SectionInfo const& sectionInfo) override {
+            SectionStats incompleteStats(sectionInfo, Counts(), 0, false);
+            std::shared_ptr<SectionNode> node;
+            if (m_sectionStack.empty()) {
+                if (!m_rootSection)
+                    m_rootSection = std::make_shared<SectionNode>(incompleteStats);
+                node = m_rootSection;
+            } else {
+                SectionNode& parentNode = *m_sectionStack.back();
+                auto it = std::find_if(parentNode.childSections.begin(),
+                                       parentNode.childSections.end(),
+                                       BySectionInfo(sectionInfo));
+                if (it == parentNode.childSections.end()) {
+                    node = std::make_shared<SectionNode>(incompleteStats);
+                    parentNode.childSections.push_back(node);
+                } else
+                    node = *it;
+            }
+            m_sectionStack.push_back(node);
+            m_deepestSection = std::move(node);
+        }
+
+        void assertionStarting(AssertionInfo const&) override {}
+
+        bool assertionEnded(AssertionStats const& assertionStats) override {
+            assert(!m_sectionStack.empty());
+            // AssertionResult holds a pointer to a temporary
+            // DecomposedExpression, which getExpandedExpression() calls to
+            // build the expression string. Our section stack copy of the
+            // assertionResult will likely outlive the temporary, so it must be
+            // expanded or discarded now to avoid calling a destroyed object
+            // later.
+            prepareExpandedExpression(const_cast<AssertionResult&>(assertionStats.assertionResult));
+            SectionNode& sectionNode = *m_sectionStack.back();
+            sectionNode.assertions.push_back(assertionStats);
+            return true;
+        }
+        void sectionEnded(SectionStats const& sectionStats) override {
+            assert(!m_sectionStack.empty());
+            SectionNode& node = *m_sectionStack.back();
+            node.stats = sectionStats;
+            m_sectionStack.pop_back();
+        }
+        void testCaseEnded(TestCaseStats const& testCaseStats) override {
+            auto node = std::make_shared<TestCaseNode>(testCaseStats);
+            assert(m_sectionStack.size() == 0);
+            node->children.push_back(m_rootSection);
+            m_testCases.push_back(node);
+            m_rootSection.reset();
+
+            assert(m_deepestSection);
+            m_deepestSection->stdOut = testCaseStats.stdOut;
+            m_deepestSection->stdErr = testCaseStats.stdErr;
+        }
+        void testGroupEnded(TestGroupStats const& testGroupStats) override {
+            auto node = std::make_shared<TestGroupNode>(testGroupStats);
+            node->children.swap(m_testCases);
+            m_testGroups.push_back(node);
+        }
+        void testRunEnded(TestRunStats const& testRunStats) override {
+            auto node = std::make_shared<TestRunNode>(testRunStats);
+            node->children.swap(m_testGroups);
+            m_testRuns.push_back(node);
+            testRunEndedCumulative();
+        }
+        virtual void testRunEndedCumulative() = 0;
+
+        void skipTest(TestCaseInfo const&) override {}
+
+        IConfigPtr m_config;
+        std::ostream& stream;
+        std::vector<AssertionStats> m_assertions;
+        std::vector<std::vector<std::shared_ptr<SectionNode>>> m_sections;
+        std::vector<std::shared_ptr<TestCaseNode>> m_testCases;
+        std::vector<std::shared_ptr<TestGroupNode>> m_testGroups;
+
+        std::vector<std::shared_ptr<TestRunNode>> m_testRuns;
+
+        std::shared_ptr<SectionNode> m_rootSection;
+        std::shared_ptr<SectionNode> m_deepestSection;
+        std::vector<std::shared_ptr<SectionNode>> m_sectionStack;
+        ReporterPreferences m_reporterPrefs;
+    };
+
+    template <char C> char const* getLineOfChars() {
+        static char line[CATCH_CONFIG_CONSOLE_WIDTH] = {0};
+        if (!*line) {
+            std::memset(line, C, CATCH_CONFIG_CONSOLE_WIDTH - 1);
+            line[CATCH_CONFIG_CONSOLE_WIDTH - 1] = 0;
+        }
+        return line;
+    }
+
+    struct TestEventListenerBase : StreamingReporterBase<TestEventListenerBase> {
+        TestEventListenerBase(ReporterConfig const& _config);
+
+        static std::set<Verbosity> getSupportedVerbosities();
+
+        void assertionStarting(AssertionInfo const&) override;
+        bool assertionEnded(AssertionStats const&) override;
+    };
+
+} // end namespace Catch
+
+// end catch_reporter_bases.hpp
+// start catch_console_colour.h
+
+namespace Catch {
+
+    struct Colour {
+        enum Code {
+            None = 0,
+
+            White,
+            Red,
+            Green,
+            Blue,
+            Cyan,
+            Yellow,
+            Grey,
+
+            Bright = 0x10,
+
+            BrightRed = Bright | Red,
+            BrightGreen = Bright | Green,
+            LightGrey = Bright | Grey,
+            BrightWhite = Bright | White,
+            BrightYellow = Bright | Yellow,
+
+            // By intention
+            FileName = LightGrey,
+            Warning = BrightYellow,
+            ResultError = BrightRed,
+            ResultSuccess = BrightGreen,
+            ResultExpectedFailure = Warning,
+
+            Error = BrightRed,
+            Success = Green,
+
+            OriginalExpression = Cyan,
+            ReconstructedExpression = BrightYellow,
+
+            SecondaryText = LightGrey,
+            Headers = White
+        };
+
+        // Use constructed object for RAII guard
+        Colour(Code _colourCode);
+        Colour(Colour&& other) noexcept;
+        Colour& operator=(Colour&& other) noexcept;
+        ~Colour();
+
+        // Use static method for one-shot changes
+        static void use(Code _colourCode);
+
+    private:
+        bool m_moved = false;
+    };
+
+    std::ostream& operator<<(std::ostream& os, Colour const&);
+
+} // end namespace Catch
+
+// end catch_console_colour.h
+// start catch_reporter_registrars.hpp
+
+namespace Catch {
+
+    template <typename T> class ReporterRegistrar {
+
+        class ReporterFactory : public IReporterFactory {
+
+            IStreamingReporterPtr create(ReporterConfig const& config) const override {
+                return std::unique_ptr<T>(new T(config));
+            }
+
+            std::string getDescription() const override { return T::getDescription(); }
+        };
+
+    public:
+        explicit ReporterRegistrar(std::string const& name) {
+            getMutableRegistryHub().registerReporter(name, std::make_shared<ReporterFactory>());
+        }
+    };
+
+    template <typename T> class ListenerRegistrar {
+
+        class ListenerFactory : public IReporterFactory {
+
+            IStreamingReporterPtr create(ReporterConfig const& config) const override {
+                return std::unique_ptr<T>(new T(config));
+            }
+            std::string getDescription() const override { return std::string(); }
+        };
+
+    public:
+        ListenerRegistrar() {
+            getMutableRegistryHub().registerListener(std::make_shared<ListenerFactory>());
+        }
+    };
+} // namespace Catch
+
+#if !defined(CATCH_CONFIG_DISABLE)
+
+#define CATCH_REGISTER_REPORTER(name, reporterType)                                                \
+    CATCH_INTERNAL_START_WARNINGS_SUPPRESSION                                                      \
+    CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS                                                       \
+    namespace {                                                                                    \
+        Catch::ReporterRegistrar<reporterType> catch_internal_RegistrarFor##reporterType(name);    \
+    }                                                                                              \
+    CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION
+
+#define CATCH_REGISTER_LISTENER(listenerType)                                                      \
+    CATCH_INTERNAL_START_WARNINGS_SUPPRESSION                                                      \
+    CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS                                                       \
+    namespace {                                                                                    \
+        Catch::ListenerRegistrar<listenerType> catch_internal_RegistrarFor##listenerType;          \
+    }                                                                                              \
+    CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION
+#else // CATCH_CONFIG_DISABLE
+
+#define CATCH_REGISTER_REPORTER(name, reporterType)
+#define CATCH_REGISTER_LISTENER(listenerType)
+
+#endif // CATCH_CONFIG_DISABLE
+
+// end catch_reporter_registrars.hpp
+// Allow users to base their work off existing reporters
+// start catch_reporter_compact.h
+
+namespace Catch {
+
+    struct CompactReporter : StreamingReporterBase<CompactReporter> {
+
+        using StreamingReporterBase::StreamingReporterBase;
+
+        ~CompactReporter() override;
+
+        static std::string getDescription();
+
+        void noMatchingTestCases(std::string const& spec) override;
+
+        void assertionStarting(AssertionInfo const&) override;
+
+        bool assertionEnded(AssertionStats const& _assertionStats) override;
+
+        void sectionEnded(SectionStats const& _sectionStats) override;
+
+        void testRunEnded(TestRunStats const& _testRunStats) override;
+    };
+
+} // end namespace Catch
+
+// end catch_reporter_compact.h
+// start catch_reporter_console.h
+
+#if defined(_MSC_VER)
+#pragma warning(push)
+#pragma warning(disable : 4061) // Not all labels are EXPLICITLY handled in
+                                // switch Note that 4062 (not all labels are
+                                // handled and default is missing) is enabled
+#endif
+
+namespace Catch {
+    // Fwd decls
+    struct SummaryColumn;
+    class TablePrinter;
+
+    struct ConsoleReporter : StreamingReporterBase<ConsoleReporter> {
+        std::unique_ptr<TablePrinter> m_tablePrinter;
+
+        ConsoleReporter(ReporterConfig const& config);
+        ~ConsoleReporter() override;
+        static std::string getDescription();
+
+        void noMatchingTestCases(std::string const& spec) override;
+
+        void reportInvalidArguments(std::string const& arg) override;
+
+        void assertionStarting(AssertionInfo const&) override;
+
+        bool assertionEnded(AssertionStats const& _assertionStats) override;
+
+        void sectionStarting(SectionInfo const& _sectionInfo) override;
+        void sectionEnded(SectionStats const& _sectionStats) override;
+
+#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING)
+        void benchmarkPreparing(std::string const& name) override;
+        void benchmarkStarting(BenchmarkInfo const& info) override;
+        void benchmarkEnded(BenchmarkStats<> const& stats) override;
+        void benchmarkFailed(std::string const& error) override;
+#endif // CATCH_CONFIG_ENABLE_BENCHMARKING
+
+        void testCaseEnded(TestCaseStats const& _testCaseStats) override;
+        void testGroupEnded(TestGroupStats const& _testGroupStats) override;
+        void testRunEnded(TestRunStats const& _testRunStats) override;
+        void testRunStarting(TestRunInfo const& _testRunInfo) override;
+
+    private:
+        void lazyPrint();
+
+        void lazyPrintWithoutClosingBenchmarkTable();
+        void lazyPrintRunInfo();
+        void lazyPrintGroupInfo();
+        void printTestCaseAndSectionHeader();
+
+        void printClosedHeader(std::string const& _name);
+        void printOpenHeader(std::string const& _name);
+
+        // if string has a : in first line will set indent to follow it on
+        // subsequent lines
+        void printHeaderString(std::string const& _string, std::size_t indent = 0);
+
+        void printTotals(Totals const& totals);
+        void printSummaryRow(std::string const& label,
+                             std::vector<SummaryColumn> const& cols,
+                             std::size_t row);
+
+        void printTotalsDivider(Totals const& totals);
+        void printSummaryDivider();
+        void printTestFilters();
+
+    private:
+        bool m_headerPrinted = false;
+    };
+
+} // end namespace Catch
+
+#if defined(_MSC_VER)
+#pragma warning(pop)
+#endif
+
+// end catch_reporter_console.h
+// start catch_reporter_junit.h
+
+// start catch_xmlwriter.h
+
+#include <vector>
+
+namespace Catch {
+    enum class XmlFormatting {
+        None = 0x00,
+        Indent = 0x01,
+        Newline = 0x02,
+    };
+
+    XmlFormatting operator|(XmlFormatting lhs, XmlFormatting rhs);
+    XmlFormatting operator&(XmlFormatting lhs, XmlFormatting rhs);
+
+    class XmlEncode {
+    public:
+        enum ForWhat { ForTextNodes, ForAttributes };
+
+        XmlEncode(std::string const& str, ForWhat forWhat = ForTextNodes);
+
+        void encodeTo(std::ostream& os) const;
+
+        friend std::ostream& operator<<(std::ostream& os, XmlEncode const& xmlEncode);
+
+    private:
+        std::string m_str;
+        ForWhat m_forWhat;
+    };
+
+    class XmlWriter {
+    public:
+        class ScopedElement {
+        public:
+            ScopedElement(XmlWriter* writer, XmlFormatting fmt);
+
+            ScopedElement(ScopedElement&& other) noexcept;
+            ScopedElement& operator=(ScopedElement&& other) noexcept;
+
+            ~ScopedElement();
+
+            ScopedElement& writeText(std::string const& text,
+                                     XmlFormatting fmt = XmlFormatting::Newline |
+                                                         XmlFormatting::Indent);
+
+            template <typename T>
+            ScopedElement& writeAttribute(std::string const& name, T const& attribute) {
+                m_writer->writeAttribute(name, attribute);
+                return *this;
+            }
+
+        private:
+            mutable XmlWriter* m_writer = nullptr;
+            XmlFormatting m_fmt;
+        };
+
+        XmlWriter(std::ostream& os = Catch::cout());
+        ~XmlWriter();
+
+        XmlWriter(XmlWriter const&) = delete;
+        XmlWriter& operator=(XmlWriter const&) = delete;
+
+        XmlWriter& startElement(std::string const& name,
+                                XmlFormatting fmt = XmlFormatting::Newline | XmlFormatting::Indent);
+
+        ScopedElement scopedElement(std::string const& name,
+                                    XmlFormatting fmt = XmlFormatting::Newline |
+                                                        XmlFormatting::Indent);
+
+        XmlWriter& endElement(XmlFormatting fmt = XmlFormatting::Newline | XmlFormatting::Indent);
+
+        XmlWriter& writeAttribute(std::string const& name, std::string const& attribute);
+
+        XmlWriter& writeAttribute(std::string const& name, bool attribute);
+
+        template <typename T>
+        XmlWriter& writeAttribute(std::string const& name, T const& attribute) {
+            ReusableStringStream rss;
+            rss << attribute;
+            return writeAttribute(name, rss.str());
+        }
+
+        XmlWriter& writeText(std::string const& text,
+                             XmlFormatting fmt = XmlFormatting::Newline | XmlFormatting::Indent);
+
+        XmlWriter& writeComment(std::string const& text,
+                                XmlFormatting fmt = XmlFormatting::Newline | XmlFormatting::Indent);
+
+        void writeStylesheetRef(std::string const& url);
+
+        XmlWriter& writeBlankLine();
+
+        void ensureTagClosed();
+
+    private:
+        void applyFormatting(XmlFormatting fmt);
+
+        void writeDeclaration();
+
+        void newlineIfNecessary();
+
+        bool m_tagIsOpen = false;
+        bool m_needsNewline = false;
+        std::vector<std::string> m_tags;
+        std::string m_indent;
+        std::ostream& m_os;
+    };
+
+} // namespace Catch
+
+// end catch_xmlwriter.h
+namespace Catch {
+
+    class JunitReporter : public CumulativeReporterBase<JunitReporter> {
+    public:
+        JunitReporter(ReporterConfig const& _config);
+
+        ~JunitReporter() override;
+
+        static std::string getDescription();
+
+        void noMatchingTestCases(std::string const& /*spec*/) override;
+
+        void testRunStarting(TestRunInfo const& runInfo) override;
+
+        void testGroupStarting(GroupInfo const& groupInfo) override;
+
+        void testCaseStarting(TestCaseInfo const& testCaseInfo) override;
+        bool assertionEnded(AssertionStats const& assertionStats) override;
+
+        void testCaseEnded(TestCaseStats const& testCaseStats) override;
+
+        void testGroupEnded(TestGroupStats const& testGroupStats) override;
+
+        void testRunEndedCumulative() override;
+
+        void writeGroup(TestGroupNode const& groupNode, double suiteTime);
+
+        void writeTestCase(TestCaseNode const& testCaseNode);
+
+        void writeSection(std::string const& className,
+                          std::string const& rootName,
+                          SectionNode const& sectionNode,
+                          bool testOkToFail);
+
+        void writeAssertions(SectionNode const& sectionNode);
+        void writeAssertion(AssertionStats const& stats);
+
+        XmlWriter xml;
+        Timer suiteTimer;
+        std::string stdOutForSuite;
+        std::string stdErrForSuite;
+        unsigned int unexpectedExceptions = 0;
+        bool m_okToFail = false;
+    };
+
+} // end namespace Catch
+
+// end catch_reporter_junit.h
+// start catch_reporter_xml.h
+
+namespace Catch {
+    class XmlReporter : public StreamingReporterBase<XmlReporter> {
+    public:
+        XmlReporter(ReporterConfig const& _config);
+
+        ~XmlReporter() override;
+
+        static std::string getDescription();
+
+        virtual std::string getStylesheetRef() const;
+
+        void writeSourceInfo(SourceLineInfo const& sourceInfo);
+
+    public: // StreamingReporterBase
+        void noMatchingTestCases(std::string const& s) override;
+
+        void testRunStarting(TestRunInfo const& testInfo) override;
+
+        void testGroupStarting(GroupInfo const& groupInfo) override;
+
+        void testCaseStarting(TestCaseInfo const& testInfo) override;
+
+        void sectionStarting(SectionInfo const& sectionInfo) override;
+
+        void assertionStarting(AssertionInfo const&) override;
+
+        bool assertionEnded(AssertionStats const& assertionStats) override;
+
+        void sectionEnded(SectionStats const& sectionStats) override;
+
+        void testCaseEnded(TestCaseStats const& testCaseStats) override;
+
+        void testGroupEnded(TestGroupStats const& testGroupStats) override;
+
+        void testRunEnded(TestRunStats const& testRunStats) override;
+
+#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING)
+        void benchmarkPreparing(std::string const& name) override;
+        void benchmarkStarting(BenchmarkInfo const&) override;
+        void benchmarkEnded(BenchmarkStats<> const&) override;
+        void benchmarkFailed(std::string const&) override;
+#endif // CATCH_CONFIG_ENABLE_BENCHMARKING
+
+    private:
+        Timer m_testCaseTimer;
+        XmlWriter m_xml;
+        int m_sectionDepth = 0;
+    };
+
+} // end namespace Catch
+
+// end catch_reporter_xml.h
+
+// end catch_external_interfaces.h
+#endif
+
+#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING)
+// start catch_benchmarking_all.hpp
+
+// A proxy header that includes all of the benchmarking headers to allow
+// concise include of the benchmarking features. You should prefer the
+// individual includes in standard use.
+
+// start catch_benchmark.hpp
+
+// Benchmark
+
+// start catch_chronometer.hpp
+
+// User-facing chronometer
+
+// start catch_clock.hpp
+
+// Clocks
+
+#include <chrono>
+#include <ratio>
+
+namespace Catch {
+    namespace Benchmark {
+        template <typename Clock> using ClockDuration = typename Clock::duration;
+        template <typename Clock>
+        using FloatDuration = std::chrono::duration<double, typename Clock::period>;
+
+        template <typename Clock> using TimePoint = typename Clock::time_point;
+
+        using default_clock = std::chrono::steady_clock;
+
+        template <typename Clock> struct now {
+            TimePoint<Clock> operator()() const { return Clock::now(); }
+        };
+
+        using fp_seconds = std::chrono::duration<double, std::ratio<1>>;
+    } // namespace Benchmark
+} // namespace Catch
+
+// end catch_clock.hpp
+// start catch_optimizer.hpp
+
+// Hinting the optimizer
+
+#if defined(_MSC_VER)
+#include <atomic> // atomic_thread_fence
+#endif
+
+namespace Catch {
+    namespace Benchmark {
+#if defined(__GNUC__) || defined(__clang__)
+        template <typename T> inline void keep_memory(T* p) {
+            asm volatile("" : : "g"(p) : "memory");
+        }
+        inline void keep_memory() { asm volatile("" : : : "memory"); }
+
+        namespace Detail {
+            inline void optimizer_barrier() { keep_memory(); }
+        } // namespace Detail
+#elif defined(_MSC_VER)
+
+#pragma optimize("", off)
+        template <typename T> inline void keep_memory(T* p) {
+            // thanks @milleniumbug
+            *reinterpret_cast<char volatile*>(p) = *reinterpret_cast<char const volatile*>(p);
+        }
+        // TODO equivalent keep_memory()
+#pragma optimize("", on)
+
+        namespace Detail {
+            inline void optimizer_barrier() { std::atomic_thread_fence(std::memory_order_seq_cst); }
+        } // namespace Detail
+
+#endif
+
+        template <typename T> inline void deoptimize_value(T&& x) { keep_memory(&x); }
+
+        template <typename Fn, typename... Args>
+        inline auto invoke_deoptimized(Fn&& fn, Args&&... args) ->
+            typename std::enable_if<!std::is_same<void, decltype(fn(args...))>::value>::type {
+            deoptimize_value(std::forward<Fn>(fn)(std::forward<Args...>(args...)));
+        }
+
+        template <typename Fn, typename... Args>
+        inline auto invoke_deoptimized(Fn&& fn, Args&&... args) ->
+            typename std::enable_if<std::is_same<void, decltype(fn(args...))>::value>::type {
+            std::forward<Fn>(fn)(std::forward<Args...>(args...));
+        }
+    } // namespace Benchmark
+} // namespace Catch
+
+// end catch_optimizer.hpp
+// start catch_complete_invoke.hpp
+
+// Invoke with a special case for void
+
+#include <type_traits>
+#include <utility>
+
+namespace Catch {
+    namespace Benchmark {
+        namespace Detail {
+            template <typename T> struct CompleteType { using type = T; };
+            template <> struct CompleteType<void> {
+                struct type {};
+            };
+
+            template <typename T> using CompleteType_t = typename CompleteType<T>::type;
+
+            template <typename Result> struct CompleteInvoker {
+                template <typename Fun, typename... Args>
+                static Result invoke(Fun&& fun, Args&&... args) {
+                    return std::forward<Fun>(fun)(std::forward<Args>(args)...);
+                }
+            };
+            template <> struct CompleteInvoker<void> {
+                template <typename Fun, typename... Args>
+                static CompleteType_t<void> invoke(Fun&& fun, Args&&... args) {
+                    std::forward<Fun>(fun)(std::forward<Args>(args)...);
+                    return {};
+                }
+            };
+
+            // invoke and not return void :(
+            template <typename Fun, typename... Args>
+            CompleteType_t<FunctionReturnType<Fun, Args...>> complete_invoke(Fun&& fun,
+                                                                             Args&&... args) {
+                return CompleteInvoker<FunctionReturnType<Fun, Args...>>::invoke(
+                    std::forward<Fun>(fun), std::forward<Args>(args)...);
+            }
+
+            const std::string benchmarkErrorMsg = "a benchmark failed to run successfully";
+        } // namespace Detail
+
+        template <typename Fun>
+        Detail::CompleteType_t<FunctionReturnType<Fun>> user_code(Fun&& fun) {
+            CATCH_TRY { return Detail::complete_invoke(std::forward<Fun>(fun)); }
+            CATCH_CATCH_ALL {
+                getResultCapture().benchmarkFailed(translateActiveException());
+                CATCH_RUNTIME_ERROR(Detail::benchmarkErrorMsg);
+            }
+        }
+    } // namespace Benchmark
+} // namespace Catch
+
+// end catch_complete_invoke.hpp
+namespace Catch {
+    namespace Benchmark {
+        namespace Detail {
+            struct ChronometerConcept {
+                virtual void start() = 0;
+                virtual void finish() = 0;
+                virtual ~ChronometerConcept() = default;
+            };
+            template <typename Clock> struct ChronometerModel final : public ChronometerConcept {
+                void start() override { started = Clock::now(); }
+                void finish() override { finished = Clock::now(); }
+
+                ClockDuration<Clock> elapsed() const { return finished - started; }
+
+                TimePoint<Clock> started;
+                TimePoint<Clock> finished;
+            };
+        } // namespace Detail
+
+        struct Chronometer {
+        public:
+            template <typename Fun> void measure(Fun&& fun) {
+                measure(std::forward<Fun>(fun), is_callable<Fun(int)>());
+            }
+
+            int runs() const { return k; }
+
+            Chronometer(Detail::ChronometerConcept& meter, int k) : impl(&meter), k(k) {}
+
+        private:
+            template <typename Fun> void measure(Fun&& fun, std::false_type) {
+                measure([&fun](int) { return fun(); }, std::true_type());
+            }
+
+            template <typename Fun> void measure(Fun&& fun, std::true_type) {
+                Detail::optimizer_barrier();
+                impl->start();
+                for (int i = 0; i < k; ++i)
+                    invoke_deoptimized(fun, i);
+                impl->finish();
+                Detail::optimizer_barrier();
+            }
+
+            Detail::ChronometerConcept* impl;
+            int k;
+        };
+    } // namespace Benchmark
+} // namespace Catch
+
+// end catch_chronometer.hpp
+// start catch_environment.hpp
+
+// Environment information
+
+namespace Catch {
+    namespace Benchmark {
+        template <typename Duration> struct EnvironmentEstimate {
+            Duration mean;
+            OutlierClassification outliers;
+
+            template <typename Duration2> operator EnvironmentEstimate<Duration2>() const {
+                return {mean, outliers};
+            }
+        };
+        template <typename Clock> struct Environment {
+            using clock_type = Clock;
+            EnvironmentEstimate<FloatDuration<Clock>> clock_resolution;
+            EnvironmentEstimate<FloatDuration<Clock>> clock_cost;
+        };
+    } // namespace Benchmark
+} // namespace Catch
+
+// end catch_environment.hpp
+// start catch_execution_plan.hpp
+
+// Execution plan
+
+// start catch_benchmark_function.hpp
+
+// Dumb std::function implementation for consistent call overhead
+
+#include <cassert>
+#include <type_traits>
+#include <utility>
+#include <memory>
+
+namespace Catch {
+    namespace Benchmark {
+        namespace Detail {
+            template <typename T> using Decay = typename std::decay<T>::type;
+            template <typename T, typename U>
+            struct is_related : std::is_same<Decay<T>, Decay<U>> {};
+
+            /// We need to reinvent std::function because every piece of code
+            /// that might add overhead in a measurement context needs to have
+            /// consistent performance characteristics so that we can account
+            /// for it in the measurement. Implementations of std::function with
+            /// optimizations that aren't always applicable, like small buffer
+            /// optimizations, are not uncommon. This is effectively an
+            /// implementation of std::function without any such optimizations;
+            /// it may be slow, but it is consistently slow.
+            struct BenchmarkFunction {
+            private:
+                struct callable {
+                    virtual void call(Chronometer meter) const = 0;
+                    virtual callable* clone() const = 0;
+                    virtual ~callable() = default;
+                };
+                template <typename Fun> struct model : public callable {
+                    model(Fun&& fun) : fun(std::move(fun)) {}
+                    model(Fun const& fun) : fun(fun) {}
+
+                    model<Fun>* clone() const override { return new model<Fun>(*this); }
+
+                    void call(Chronometer meter) const override {
+                        call(meter, is_callable<Fun(Chronometer)>());
+                    }
+                    void call(Chronometer meter, std::true_type) const { fun(meter); }
+                    void call(Chronometer meter, std::false_type) const { meter.measure(fun); }
+
+                    Fun fun;
+                };
+
+                struct do_nothing {
+                    void operator()() const {}
+                };
+
+                template <typename T> BenchmarkFunction(model<T>* c) : f(c) {}
+
+            public:
+                BenchmarkFunction() : f(new model<do_nothing>{{}}) {}
+
+                template <typename Fun,
+                          typename std::enable_if<!is_related<Fun, BenchmarkFunction>::value,
+                                                  int>::type = 0>
+                BenchmarkFunction(Fun&& fun) :
+                    f(new model<typename std::decay<Fun>::type>(std::forward<Fun>(fun))) {}
+
+                BenchmarkFunction(BenchmarkFunction&& that) : f(std::move(that.f)) {}
+
+                BenchmarkFunction(BenchmarkFunction const& that) : f(that.f->clone()) {}
+
+                BenchmarkFunction& operator=(BenchmarkFunction&& that) {
+                    f = std::move(that.f);
+                    return *this;
+                }
+
+                BenchmarkFunction& operator=(BenchmarkFunction const& that) {
+                    f.reset(that.f->clone());
+                    return *this;
+                }
+
+                void operator()(Chronometer meter) const { f->call(meter); }
+
+            private:
+                std::unique_ptr<callable> f;
+            };
+        } // namespace Detail
+    }     // namespace Benchmark
+} // namespace Catch
+
+// end catch_benchmark_function.hpp
+// start catch_repeat.hpp
+
+// repeat algorithm
+
+#include <type_traits>
+#include <utility>
+
+namespace Catch {
+    namespace Benchmark {
+        namespace Detail {
+            template <typename Fun> struct repeater {
+                void operator()(int k) const {
+                    for (int i = 0; i < k; ++i) {
+                        fun();
+                    }
+                }
+                Fun fun;
+            };
+            template <typename Fun> repeater<typename std::decay<Fun>::type> repeat(Fun&& fun) {
+                return {std::forward<Fun>(fun)};
+            }
+        } // namespace Detail
+    }     // namespace Benchmark
+} // namespace Catch
+
+// end catch_repeat.hpp
+// start catch_run_for_at_least.hpp
+
+// Run a function for a minimum amount of time
+
+// start catch_measure.hpp
+
+// Measure
+
+// start catch_timing.hpp
+
+// Timing
+
+#include <tuple>
+#include <type_traits>
+
+namespace Catch {
+    namespace Benchmark {
+        template <typename Duration, typename Result> struct Timing {
+            Duration elapsed;
+            Result result;
+            int iterations;
+        };
+        template <typename Clock, typename Func, typename... Args>
+        using TimingOf =
+            Timing<ClockDuration<Clock>, Detail::CompleteType_t<FunctionReturnType<Func, Args...>>>;
+    } // namespace Benchmark
+} // namespace Catch
+
+// end catch_timing.hpp
+#include <utility>
+
+namespace Catch {
+    namespace Benchmark {
+        namespace Detail {
+            template <typename Clock, typename Fun, typename... Args>
+            TimingOf<Clock, Fun, Args...> measure(Fun&& fun, Args&&... args) {
+                auto start = Clock::now();
+                auto&& r = Detail::complete_invoke(fun, std::forward<Args>(args)...);
+                auto end = Clock::now();
+                auto delta = end - start;
+                return {delta, std::forward<decltype(r)>(r), 1};
+            }
+        } // namespace Detail
+    }     // namespace Benchmark
+} // namespace Catch
+
+// end catch_measure.hpp
+#include <utility>
+#include <type_traits>
+
+namespace Catch {
+    namespace Benchmark {
+        namespace Detail {
+            template <typename Clock, typename Fun>
+            TimingOf<Clock, Fun, int> measure_one(Fun&& fun, int iters, std::false_type) {
+                return Detail::measure<Clock>(fun, iters);
+            }
+            template <typename Clock, typename Fun>
+            TimingOf<Clock, Fun, Chronometer> measure_one(Fun&& fun, int iters, std::true_type) {
+                Detail::ChronometerModel<Clock> meter;
+                auto&& result = Detail::complete_invoke(fun, Chronometer(meter, iters));
+
+                return {meter.elapsed(), std::move(result), iters};
+            }
+
+            template <typename Clock, typename Fun>
+            using run_for_at_least_argument_t = typename std::
+                conditional<is_callable<Fun(Chronometer)>::value, Chronometer, int>::type;
+
+            struct optimized_away_error : std::exception {
+                const char* what() const noexcept override {
+                    return "could not measure benchmark, maybe it was "
+                           "optimized away";
+                }
+            };
+
+            template <typename Clock, typename Fun>
+            TimingOf<Clock, Fun, run_for_at_least_argument_t<Clock, Fun>>
+            run_for_at_least(ClockDuration<Clock> how_long, int seed, Fun&& fun) {
+                auto iters = seed;
+                while (iters < (1 << 30)) {
+                    auto&& Timing = measure_one<Clock>(fun, iters, is_callable<Fun(Chronometer)>());
+
+                    if (Timing.elapsed >= how_long) {
+                        return {Timing.elapsed, std::move(Timing.result), iters};
+                    }
+                    iters *= 2;
+                }
+                Catch::throw_exception(optimized_away_error{});
+            }
+        } // namespace Detail
+    }     // namespace Benchmark
+} // namespace Catch
+
+// end catch_run_for_at_least.hpp
+#include <algorithm>
+#include <iterator>
+
+namespace Catch {
+    namespace Benchmark {
+        template <typename Duration> struct ExecutionPlan {
+            int iterations_per_sample;
+            Duration estimated_duration;
+            Detail::BenchmarkFunction benchmark;
+            Duration warmup_time;
+            int warmup_iterations;
+
+            template <typename Duration2> operator ExecutionPlan<Duration2>() const {
+                return {iterations_per_sample,
+                        estimated_duration,
+                        benchmark,
+                        warmup_time,
+                        warmup_iterations};
+            }
+
+            template <typename Clock>
+            std::vector<FloatDuration<Clock>> run(const IConfig& cfg,
+                                                  Environment<FloatDuration<Clock>> env) const {
+                // warmup a bit
+                Detail::run_for_at_least<Clock>(
+                    std::chrono::duration_cast<ClockDuration<Clock>>(warmup_time),
+                    warmup_iterations,
+                    Detail::repeat(now<Clock>{}));
+
+                std::vector<FloatDuration<Clock>> times;
+                times.reserve(cfg.benchmarkSamples());
+                std::generate_n(std::back_inserter(times), cfg.benchmarkSamples(), [this, env] {
+                    Detail::ChronometerModel<Clock> model;
+                    this->benchmark(Chronometer(model, iterations_per_sample));
+                    auto sample_time = model.elapsed() - env.clock_cost.mean;
+                    if (sample_time < FloatDuration<Clock>::zero())
+                        sample_time = FloatDuration<Clock>::zero();
+                    return sample_time / iterations_per_sample;
+                });
+                return times;
+            }
+        };
+    } // namespace Benchmark
+} // namespace Catch
+
+// end catch_execution_plan.hpp
+// start catch_estimate_clock.hpp
+
+// Environment measurement
+
+// start catch_stats.hpp
+
+// Statistical analysis tools
+
+#include <algorithm>
+#include <functional>
+#include <vector>
+#include <iterator>
+#include <numeric>
+#include <tuple>
+#include <cmath>
+#include <utility>
+#include <cstddef>
+#include <random>
+
+namespace Catch {
+    namespace Benchmark {
+        namespace Detail {
+            using sample = std::vector<double>;
+
+            double weighted_average_quantile(int k,
+                                             int q,
+                                             std::vector<double>::iterator first,
+                                             std::vector<double>::iterator last);
+
+            template <typename Iterator>
+            OutlierClassification classify_outliers(Iterator first, Iterator last) {
+                std::vector<double> copy(first, last);
+
+                auto q1 = weighted_average_quantile(1, 4, copy.begin(), copy.end());
+                auto q3 = weighted_average_quantile(3, 4, copy.begin(), copy.end());
+                auto iqr = q3 - q1;
+                auto los = q1 - (iqr * 3.);
+                auto lom = q1 - (iqr * 1.5);
+                auto him = q3 + (iqr * 1.5);
+                auto his = q3 + (iqr * 3.);
+
+                OutlierClassification o;
+                for (; first != last; ++first) {
+                    auto&& t = *first;
+                    if (t < los)
+                        ++o.low_severe;
+                    else if (t < lom)
+                        ++o.low_mild;
+                    else if (t > his)
+                        ++o.high_severe;
+                    else if (t > him)
+                        ++o.high_mild;
+                    ++o.samples_seen;
+                }
+                return o;
+            }
+
+            template <typename Iterator> double mean(Iterator first, Iterator last) {
+                auto count = last - first;
+                double sum = std::accumulate(first, last, 0.);
+                return sum / count;
+            }
+
+            template <typename URng, typename Iterator, typename Estimator>
+            sample resample(
+                URng& rng, int resamples, Iterator first, Iterator last, Estimator& estimator) {
+                auto n = last - first;
+                std::uniform_int_distribution<decltype(n)> dist(0, n - 1);
+
+                sample out;
+                out.reserve(resamples);
+                std::generate_n(
+                    std::back_inserter(out), resamples, [n, first, &estimator, &dist, &rng] {
+                        std::vector<double> resampled;
+                        resampled.reserve(n);
+                        std::generate_n(std::back_inserter(resampled), n, [first, &dist, &rng] {
+                            return first[dist(rng)];
+                        });
+                        return estimator(resampled.begin(), resampled.end());
+                    });
+                std::sort(out.begin(), out.end());
+                return out;
+            }
+
+            template <typename Estimator, typename Iterator>
+            sample jackknife(Estimator&& estimator, Iterator first, Iterator last) {
+                auto n = last - first;
+                auto second = std::next(first);
+                sample results;
+                results.reserve(n);
+
+                for (auto it = first; it != last; ++it) {
+                    std::iter_swap(it, first);
+                    results.push_back(estimator(second, last));
+                }
+
+                return results;
+            }
+
+            inline double normal_cdf(double x) { return std::erfc(-x / std::sqrt(2.0)) / 2.0; }
+
+            double erfc_inv(double x);
+
+            double normal_quantile(double p);
+
+            template <typename Iterator, typename Estimator>
+            Estimate<double> bootstrap(double confidence_level,
+                                       Iterator first,
+                                       Iterator last,
+                                       sample const& resample,
+                                       Estimator&& estimator) {
+                auto n_samples = last - first;
+
+                double point = estimator(first, last);
+                // Degenerate case with a single sample
+                if (n_samples == 1)
+                    return {point, point, point, confidence_level};
+
+                sample jack = jackknife(estimator, first, last);
+                double jack_mean = mean(jack.begin(), jack.end());
+                double sum_squares, sum_cubes;
+                std::tie(sum_squares, sum_cubes) =
+                    std::accumulate(jack.begin(),
+                                    jack.end(),
+                                    std::make_pair(0., 0.),
+                                    [jack_mean](std::pair<double, double> sqcb,
+                                                double x) -> std::pair<double, double> {
+                                        auto d = jack_mean - x;
+                                        auto d2 = d * d;
+                                        auto d3 = d2 * d;
+                                        return {sqcb.first + d2, sqcb.second + d3};
+                                    });
+
+                double accel = sum_cubes / (6 * std::pow(sum_squares, 1.5));
+                int n = static_cast<int>(resample.size());
+                double prob_n = std::count_if(resample.begin(),
+                                              resample.end(),
+                                              [point](double x) { return x < point; }) /
+                                (double)n;
+                // degenerate case with uniform samples
+                if (prob_n == 0)
+                    return {point, point, point, confidence_level};
+
+                double bias = normal_quantile(prob_n);
+                double z1 = normal_quantile((1. - confidence_level) / 2.);
+
+                auto cumn = [n](double x) -> int { return std::lround(normal_cdf(x) * n); };
+                auto a = [bias, accel](double b) { return bias + b / (1. - accel * b); };
+                double b1 = bias + z1;
+                double b2 = bias - z1;
+                double a1 = a(b1);
+                double a2 = a(b2);
+                auto lo = (std::max)(cumn(a1), 0);
+                auto hi = (std::min)(cumn(a2), n - 1);
+
+                return {point, resample[lo], resample[hi], confidence_level};
+            }
+
+            double outlier_variance(Estimate<double> mean, Estimate<double> stddev, int n);
+
+            struct bootstrap_analysis {
+                Estimate<double> mean;
+                Estimate<double> standard_deviation;
+                double outlier_variance;
+            };
+
+            bootstrap_analysis analyse_samples(double confidence_level,
+                                               int n_resamples,
+                                               std::vector<double>::iterator first,
+                                               std::vector<double>::iterator last);
+        } // namespace Detail
+    }     // namespace Benchmark
+} // namespace Catch
+
+// end catch_stats.hpp
+#include <algorithm>
+#include <iterator>
+#include <tuple>
+#include <vector>
+#include <cmath>
+
+namespace Catch {
+    namespace Benchmark {
+        namespace Detail {
+            template <typename Clock> std::vector<double> resolution(int k) {
+                std::vector<TimePoint<Clock>> times;
+                times.reserve(k + 1);
+                std::generate_n(std::back_inserter(times), k + 1, now<Clock>{});
+
+                std::vector<double> deltas;
+                deltas.reserve(k);
+                std::transform(std::next(times.begin()),
+                               times.end(),
+                               times.begin(),
+                               std::back_inserter(deltas),
+                               [](TimePoint<Clock> a, TimePoint<Clock> b) {
+                                   return static_cast<double>((a - b).count());
+                               });
+
+                return deltas;
+            }
+
+            const auto warmup_iterations = 10000;
+            const auto warmup_time = std::chrono::milliseconds(100);
+            const auto minimum_ticks = 1000;
+            const auto warmup_seed = 10000;
+            const auto clock_resolution_estimation_time = std::chrono::milliseconds(500);
+            const auto clock_cost_estimation_time_limit = std::chrono::seconds(1);
+            const auto clock_cost_estimation_tick_limit = 100000;
+            const auto clock_cost_estimation_time = std::chrono::milliseconds(10);
+            const auto clock_cost_estimation_iterations = 10000;
+
+            template <typename Clock> int warmup() {
+                return run_for_at_least<Clock>(
+                           std::chrono::duration_cast<ClockDuration<Clock>>(warmup_time),
+                           warmup_seed,
+                           &resolution<Clock>)
+                    .iterations;
+            }
+            template <typename Clock>
+            EnvironmentEstimate<FloatDuration<Clock>> estimate_clock_resolution(int iterations) {
+                auto r = run_for_at_least<Clock>(std::chrono::duration_cast<ClockDuration<Clock>>(
+                                                     clock_resolution_estimation_time),
+                                                 iterations,
+                                                 &resolution<Clock>)
+                             .result;
+                return {
+                    FloatDuration<Clock>(mean(r.begin(), r.end())),
+                    classify_outliers(r.begin(), r.end()),
+                };
+            }
+            template <typename Clock>
+            EnvironmentEstimate<FloatDuration<Clock>>
+            estimate_clock_cost(FloatDuration<Clock> resolution) {
+                auto time_limit =
+                    (std::min)(resolution * clock_cost_estimation_tick_limit,
+                               FloatDuration<Clock>(clock_cost_estimation_time_limit));
+                auto time_clock = [](int k) {
+                    return Detail::measure<Clock>([k] {
+                               for (int i = 0; i < k; ++i) {
+                                   volatile auto ignored = Clock::now();
+                                   (void)ignored;
+                               }
+                           })
+                        .elapsed;
+                };
+                time_clock(1);
+                int iters = clock_cost_estimation_iterations;
+                auto&& r = run_for_at_least<Clock>(
+                    std::chrono::duration_cast<ClockDuration<Clock>>(clock_cost_estimation_time),
+                    iters,
+                    time_clock);
+                std::vector<double> times;
+                int nsamples = static_cast<int>(std::ceil(time_limit / r.elapsed));
+                times.reserve(nsamples);
+                std::generate_n(std::back_inserter(times), nsamples, [time_clock, &r] {
+                    return static_cast<double>((time_clock(r.iterations) / r.iterations).count());
+                });
+                return {
+                    FloatDuration<Clock>(mean(times.begin(), times.end())),
+                    classify_outliers(times.begin(), times.end()),
+                };
+            }
+
+            template <typename Clock> Environment<FloatDuration<Clock>> measure_environment() {
+                static Environment<FloatDuration<Clock>>* env = nullptr;
+                if (env) {
+                    return *env;
+                }
+
+                auto iters = Detail::warmup<Clock>();
+                auto resolution = Detail::estimate_clock_resolution<Clock>(iters);
+                auto cost = Detail::estimate_clock_cost<Clock>(resolution.mean);
+
+                env = new Environment<FloatDuration<Clock>>{resolution, cost};
+                return *env;
+            }
+        } // namespace Detail
+    }     // namespace Benchmark
+} // namespace Catch
+
+// end catch_estimate_clock.hpp
+// start catch_analyse.hpp
+
+// Run and analyse one benchmark
+
+// start catch_sample_analysis.hpp
+
+// Benchmark results
+
+#include <algorithm>
+#include <vector>
+#include <string>
+#include <iterator>
+
+namespace Catch {
+    namespace Benchmark {
+        template <typename Duration> struct SampleAnalysis {
+            std::vector<Duration> samples;
+            Estimate<Duration> mean;
+            Estimate<Duration> standard_deviation;
+            OutlierClassification outliers;
+            double outlier_variance;
+
+            template <typename Duration2> operator SampleAnalysis<Duration2>() const {
+                std::vector<Duration2> samples2;
+                samples2.reserve(samples.size());
+                std::transform(samples.begin(),
+                               samples.end(),
+                               std::back_inserter(samples2),
+                               [](Duration d) { return Duration2(d); });
+                return {
+                    std::move(samples2),
+                    mean,
+                    standard_deviation,
+                    outliers,
+                    outlier_variance,
+                };
+            }
+        };
+    } // namespace Benchmark
+} // namespace Catch
+
+// end catch_sample_analysis.hpp
+#include <algorithm>
+#include <iterator>
+#include <vector>
+
+namespace Catch {
+    namespace Benchmark {
+        namespace Detail {
+            template <typename Duration, typename Iterator>
+            SampleAnalysis<Duration>
+            analyse(const IConfig& cfg, Environment<Duration>, Iterator first, Iterator last) {
+                if (!cfg.benchmarkNoAnalysis()) {
+                    std::vector<double> samples;
+                    samples.reserve(last - first);
+                    std::transform(first, last, std::back_inserter(samples), [](Duration d) {
+                        return d.count();
+                    });
+
+                    auto analysis =
+                        Catch::Benchmark::Detail::analyse_samples(cfg.benchmarkConfidenceInterval(),
+                                                                  cfg.benchmarkResamples(),
+                                                                  samples.begin(),
+                                                                  samples.end());
+                    auto outliers =
+                        Catch::Benchmark::Detail::classify_outliers(samples.begin(), samples.end());
+
+                    auto wrap_estimate = [](Estimate<double> e) {
+                        return Estimate<Duration>{
+                            Duration(e.point),
+                            Duration(e.lower_bound),
+                            Duration(e.upper_bound),
+                            e.confidence_interval,
+                        };
+                    };
+                    std::vector<Duration> samples2;
+                    samples2.reserve(samples.size());
+                    std::transform(samples.begin(),
+                                   samples.end(),
+                                   std::back_inserter(samples2),
+                                   [](double d) { return Duration(d); });
+                    return {
+                        std::move(samples2),
+                        wrap_estimate(analysis.mean),
+                        wrap_estimate(analysis.standard_deviation),
+                        outliers,
+                        analysis.outlier_variance,
+                    };
+                } else {
+                    std::vector<Duration> samples;
+                    samples.reserve(last - first);
+
+                    Duration mean = Duration(0);
+                    int i = 0;
+                    for (auto it = first; it < last; ++it, ++i) {
+                        samples.push_back(Duration(*it));
+                        mean += Duration(*it);
+                    }
+                    mean /= i;
+
+                    return {std::move(samples),
+                            Estimate<Duration>{mean, mean, mean, 0.0},
+                            Estimate<Duration>{Duration(0), Duration(0), Duration(0), 0.0},
+                            OutlierClassification{},
+                            0.0};
+                }
+            }
+        } // namespace Detail
+    }     // namespace Benchmark
+} // namespace Catch
+
+// end catch_analyse.hpp
+#include <algorithm>
+#include <functional>
+#include <string>
+#include <vector>
+#include <cmath>
+
+namespace Catch {
+    namespace Benchmark {
+        struct Benchmark {
+            Benchmark(std::string&& name) : name(std::move(name)) {}
+
+            template <class FUN>
+            Benchmark(std::string&& name, FUN&& func) :
+                fun(std::move(func)), name(std::move(name)) {}
+
+            template <typename Clock>
+            ExecutionPlan<FloatDuration<Clock>>
+            prepare(const IConfig& cfg, Environment<FloatDuration<Clock>> env) const {
+                auto min_time = env.clock_resolution.mean * Detail::minimum_ticks;
+                auto run_time = std::max(
+                    min_time,
+                    std::chrono::duration_cast<decltype(min_time)>(cfg.benchmarkWarmupTime()));
+                auto&& test = Detail::run_for_at_least<Clock>(
+                    std::chrono::duration_cast<ClockDuration<Clock>>(run_time), 1, fun);
+                int new_iters =
+                    static_cast<int>(std::ceil(min_time * test.iterations / test.elapsed));
+                return {new_iters,
+                        test.elapsed / test.iterations * new_iters * cfg.benchmarkSamples(),
+                        fun,
+                        std::chrono::duration_cast<FloatDuration<Clock>>(cfg.benchmarkWarmupTime()),
+                        Detail::warmup_iterations};
+            }
+
+            template <typename Clock = default_clock> void run() {
+                IConfigPtr cfg = getCurrentContext().getConfig();
+
+                auto env = Detail::measure_environment<Clock>();
+
+                getResultCapture().benchmarkPreparing(name);
+                CATCH_TRY {
+                    auto plan = user_code([&] { return prepare<Clock>(*cfg, env); });
+
+                    BenchmarkInfo info{name,
+                                       plan.estimated_duration.count(),
+                                       plan.iterations_per_sample,
+                                       cfg->benchmarkSamples(),
+                                       cfg->benchmarkResamples(),
+                                       env.clock_resolution.mean.count(),
+                                       env.clock_cost.mean.count()};
+
+                    getResultCapture().benchmarkStarting(info);
+
+                    auto samples = user_code([&] { return plan.template run<Clock>(*cfg, env); });
+
+                    auto analysis = Detail::analyse(*cfg, env, samples.begin(), samples.end());
+                    BenchmarkStats<FloatDuration<Clock>> stats{info,
+                                                               analysis.samples,
+                                                               analysis.mean,
+                                                               analysis.standard_deviation,
+                                                               analysis.outliers,
+                                                               analysis.outlier_variance};
+                    getResultCapture().benchmarkEnded(stats);
+                }
+                CATCH_CATCH_ALL {
+                    if (translateActiveException() !=
+                        Detail::benchmarkErrorMsg) // benchmark errors have been
+                                                   // reported, otherwise
+                                                   // rethrow.
+                        std::rethrow_exception(std::current_exception());
+                }
+            }
+
+            // sets lambda to be used in fun *and* executes benchmark!
+            template <
+                typename Fun,
+                typename std::enable_if<!Detail::is_related<Fun, Benchmark>::value, int>::type = 0>
+            Benchmark& operator=(Fun func) {
+                fun = Detail::BenchmarkFunction(func);
+                run();
+                return *this;
+            }
+
+            explicit operator bool() { return true; }
+
+        private:
+            Detail::BenchmarkFunction fun;
+            std::string name;
+        };
+    } // namespace Benchmark
+} // namespace Catch
+
+#define INTERNAL_CATCH_GET_1_ARG(arg1, arg2, ...) arg1
+#define INTERNAL_CATCH_GET_2_ARG(arg1, arg2, ...) arg2
+
+#define INTERNAL_CATCH_BENCHMARK(BenchmarkName, name, benchmarkIndex)                              \
+    if (Catch::Benchmark::Benchmark BenchmarkName{name})                                           \
+    BenchmarkName = [&](int benchmarkIndex)
+
+#define INTERNAL_CATCH_BENCHMARK_ADVANCED(BenchmarkName, name)                                     \
+    if (Catch::Benchmark::Benchmark BenchmarkName{name})                                           \
+    BenchmarkName = [&]
+
+// end catch_benchmark.hpp
+// start catch_constructor.hpp
+
+// Constructor and destructor helpers
+
+#include <type_traits>
+
+namespace Catch {
+    namespace Benchmark {
+        namespace Detail {
+            template <typename T, bool Destruct> struct ObjectStorage {
+                using TStorage =
+                    typename std::aligned_storage<sizeof(T), std::alignment_of<T>::value>::type;
+
+                ObjectStorage() : data() {}
+
+                ObjectStorage(const ObjectStorage& other) { new (&data) T(other.stored_object()); }
+
+                ObjectStorage(ObjectStorage&& other) {
+                    new (&data) T(std::move(other.stored_object()));
+                }
+
+                ~ObjectStorage() { destruct_on_exit<T>(); }
+
+                template <typename... Args> void construct(Args&&... args) {
+                    new (&data) T(std::forward<Args>(args)...);
+                }
+
+                template <bool AllowManualDestruction = !Destruct>
+                typename std::enable_if<AllowManualDestruction>::type destruct() {
+                    stored_object().~T();
+                }
+
+            private:
+                // If this is a constructor benchmark, destruct the underlying
+                // object
+                template <typename U>
+                void destruct_on_exit(typename std::enable_if<Destruct, U>::type* = 0) {
+                    destruct<true>();
+                }
+                // Otherwise, don't
+                template <typename U>
+                void destruct_on_exit(typename std::enable_if<!Destruct, U>::type* = 0) {}
+
+                T& stored_object() { return *static_cast<T*>(static_cast<void*>(&data)); }
+
+                T const& stored_object() const {
+                    return *static_cast<T*>(static_cast<void*>(&data));
+                }
+
+                TStorage data;
+            };
+        } // namespace Detail
+
+        template <typename T> using storage_for = Detail::ObjectStorage<T, true>;
+
+        template <typename T> using destructable_object = Detail::ObjectStorage<T, false>;
+    } // namespace Benchmark
+} // namespace Catch
+
+// end catch_constructor.hpp
+// end catch_benchmarking_all.hpp
+#endif
+
+#endif // ! CATCH_CONFIG_IMPL_ONLY
+
+#ifdef CATCH_IMPL
+// start catch_impl.hpp
+
+#ifdef __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wweak-vtables"
+#endif
+
+// Keep these here for external reporters
+// start catch_test_case_tracker.h
+
+#include <string>
+#include <vector>
+#include <memory>
+
+namespace Catch {
+    namespace TestCaseTracking {
+
+        struct NameAndLocation {
+            std::string name;
+            SourceLineInfo location;
+
+            NameAndLocation(std::string const& _name, SourceLineInfo const& _location);
+            friend bool operator==(NameAndLocation const& lhs, NameAndLocation const& rhs) {
+                return lhs.name == rhs.name && lhs.location == rhs.location;
+            }
+        };
+
+        class ITracker;
+
+        using ITrackerPtr = std::shared_ptr<ITracker>;
+
+        class ITracker {
+            NameAndLocation m_nameAndLocation;
+
+        public:
+            ITracker(NameAndLocation const& nameAndLoc) : m_nameAndLocation(nameAndLoc) {}
+
+            // static queries
+            NameAndLocation const& nameAndLocation() const { return m_nameAndLocation; }
+
+            virtual ~ITracker();
+
+            // dynamic queries
+            virtual bool isComplete() const = 0; // Successfully completed or failed
+            virtual bool isSuccessfullyCompleted() const = 0;
+            virtual bool isOpen() const = 0; // Started but not complete
+            virtual bool hasChildren() const = 0;
+            virtual bool hasStarted() const = 0;
+
+            virtual ITracker& parent() = 0;
+
+            // actions
+            virtual void close() = 0; // Successfully complete
+            virtual void fail() = 0;
+            virtual void markAsNeedingAnotherRun() = 0;
+
+            virtual void addChild(ITrackerPtr const& child) = 0;
+            virtual ITrackerPtr findChild(NameAndLocation const& nameAndLocation) = 0;
+            virtual void openChild() = 0;
+
+            // Debug/ checking
+            virtual bool isSectionTracker() const = 0;
+            virtual bool isGeneratorTracker() const = 0;
+        };
+
+        class TrackerContext {
+
+            enum RunState { NotStarted, Executing, CompletedCycle };
+
+            ITrackerPtr m_rootTracker;
+            ITracker* m_currentTracker = nullptr;
+            RunState m_runState = NotStarted;
+
+        public:
+            ITracker& startRun();
+            void endRun();
+
+            void startCycle();
+            void completeCycle();
+
+            bool completedCycle() const;
+            ITracker& currentTracker();
+            void setCurrentTracker(ITracker* tracker);
+        };
+
+        class TrackerBase : public ITracker {
+        protected:
+            enum CycleState {
+                NotStarted,
+                Executing,
+                ExecutingChildren,
+                NeedsAnotherRun,
+                CompletedSuccessfully,
+                Failed
+            };
+
+            using Children = std::vector<ITrackerPtr>;
+            TrackerContext& m_ctx;
+            ITracker* m_parent;
+            Children m_children;
+            CycleState m_runState = NotStarted;
+
+        public:
+            TrackerBase(NameAndLocation const& nameAndLocation,
+                        TrackerContext& ctx,
+                        ITracker* parent);
+
+            bool isComplete() const override;
+            bool isSuccessfullyCompleted() const override;
+            bool isOpen() const override;
+            bool hasChildren() const override;
+            bool hasStarted() const override { return m_runState != NotStarted; }
+
+            void addChild(ITrackerPtr const& child) override;
+
+            ITrackerPtr findChild(NameAndLocation const& nameAndLocation) override;
+            ITracker& parent() override;
+
+            void openChild() override;
+
+            bool isSectionTracker() const override;
+            bool isGeneratorTracker() const override;
+
+            void open();
+
+            void close() override;
+            void fail() override;
+            void markAsNeedingAnotherRun() override;
+
+        private:
+            void moveToParent();
+            void moveToThis();
+        };
+
+        class SectionTracker : public TrackerBase {
+            std::vector<std::string> m_filters;
+            std::string m_trimmed_name;
+
+        public:
+            SectionTracker(NameAndLocation const& nameAndLocation,
+                           TrackerContext& ctx,
+                           ITracker* parent);
+
+            bool isSectionTracker() const override;
+
+            bool isComplete() const override;
+
+            static SectionTracker& acquire(TrackerContext& ctx,
+                                           NameAndLocation const& nameAndLocation);
+
+            void tryOpen();
+
+            void addInitialFilters(std::vector<std::string> const& filters);
+            void addNextFilters(std::vector<std::string> const& filters);
+            //! Returns filters active in this tracker
+            std::vector<std::string> const& getFilters() const;
+            //! Returns whitespace-trimmed name of the tracked section
+            std::string const& trimmedName() const;
+        };
+
+    } // namespace TestCaseTracking
+
+    using TestCaseTracking::ITracker;
+    using TestCaseTracking::SectionTracker;
+    using TestCaseTracking::TrackerContext;
+
+} // namespace Catch
+
+// end catch_test_case_tracker.h
+
+// start catch_leak_detector.h
+
+namespace Catch {
+
+    struct LeakDetector {
+        LeakDetector();
+        ~LeakDetector();
+    };
+
+} // namespace Catch
+// end catch_leak_detector.h
+// Cpp files will be included in the single-header file here
+// start catch_stats.cpp
+
+// Statistical analysis tools
+
+#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING)
+
+#include <cassert>
+#include <random>
+
+#if defined(CATCH_CONFIG_USE_ASYNC)
+#include <future>
+#endif
+
+namespace {
+    double erf_inv(double x) {
+        // Code accompanying the article "Approximating the erfinv function" in
+        // GPU Computing Gems, Volume 2
+        double w, p;
+
+        w = -log((1.0 - x) * (1.0 + x));
+
+        if (w < 6.250000) {
+            w = w - 3.125000;
+            p = -3.6444120640178196996e-21;
+            p = -1.685059138182016589e-19 + p * w;
+            p = 1.2858480715256400167e-18 + p * w;
+            p = 1.115787767802518096e-17 + p * w;
+            p = -1.333171662854620906e-16 + p * w;
+            p = 2.0972767875968561637e-17 + p * w;
+            p = 6.6376381343583238325e-15 + p * w;
+            p = -4.0545662729752068639e-14 + p * w;
+            p = -8.1519341976054721522e-14 + p * w;
+            p = 2.6335093153082322977e-12 + p * w;
+            p = -1.2975133253453532498e-11 + p * w;
+            p = -5.4154120542946279317e-11 + p * w;
+            p = 1.051212273321532285e-09 + p * w;
+            p = -4.1126339803469836976e-09 + p * w;
+            p = -2.9070369957882005086e-08 + p * w;
+            p = 4.2347877827932403518e-07 + p * w;
+            p = -1.3654692000834678645e-06 + p * w;
+            p = -1.3882523362786468719e-05 + p * w;
+            p = 0.0001867342080340571352 + p * w;
+            p = -0.00074070253416626697512 + p * w;
+            p = -0.0060336708714301490533 + p * w;
+            p = 0.24015818242558961693 + p * w;
+            p = 1.6536545626831027356 + p * w;
+        } else if (w < 16.000000) {
+            w = sqrt(w) - 3.250000;
+            p = 2.2137376921775787049e-09;
+            p = 9.0756561938885390979e-08 + p * w;
+            p = -2.7517406297064545428e-07 + p * w;
+            p = 1.8239629214389227755e-08 + p * w;
+            p = 1.5027403968909827627e-06 + p * w;
+            p = -4.013867526981545969e-06 + p * w;
+            p = 2.9234449089955446044e-06 + p * w;
+            p = 1.2475304481671778723e-05 + p * w;
+            p = -4.7318229009055733981e-05 + p * w;
+            p = 6.8284851459573175448e-05 + p * w;
+            p = 2.4031110387097893999e-05 + p * w;
+            p = -0.0003550375203628474796 + p * w;
+            p = 0.00095328937973738049703 + p * w;
+            p = -0.0016882755560235047313 + p * w;
+            p = 0.0024914420961078508066 + p * w;
+            p = -0.0037512085075692412107 + p * w;
+            p = 0.005370914553590063617 + p * w;
+            p = 1.0052589676941592334 + p * w;
+            p = 3.0838856104922207635 + p * w;
+        } else {
+            w = sqrt(w) - 5.000000;
+            p = -2.7109920616438573243e-11;
+            p = -2.5556418169965252055e-10 + p * w;
+            p = 1.5076572693500548083e-09 + p * w;
+            p = -3.7894654401267369937e-09 + p * w;
+            p = 7.6157012080783393804e-09 + p * w;
+            p = -1.4960026627149240478e-08 + p * w;
+            p = 2.9147953450901080826e-08 + p * w;
+            p = -6.7711997758452339498e-08 + p * w;
+            p = 2.2900482228026654717e-07 + p * w;
+            p = -9.9298272942317002539e-07 + p * w;
+            p = 4.5260625972231537039e-06 + p * w;
+            p = -1.9681778105531670567e-05 + p * w;
+            p = 7.5995277030017761139e-05 + p * w;
+            p = -0.00021503011930044477347 + p * w;
+            p = -0.00013871931833623122026 + p * w;
+            p = 1.0103004648645343977 + p * w;
+            p = 4.8499064014085844221 + p * w;
+        }
+        return p * x;
+    }
+
+    double standard_deviation(std::vector<double>::iterator first,
+                              std::vector<double>::iterator last) {
+        auto m = Catch::Benchmark::Detail::mean(first, last);
+        double variance = std::accumulate(first,
+                                          last,
+                                          0.,
+                                          [m](double a, double b) {
+                                              double diff = b - m;
+                                              return a + diff * diff;
+                                          }) /
+                          (last - first);
+        return std::sqrt(variance);
+    }
+
+} // namespace
+
+namespace Catch {
+    namespace Benchmark {
+        namespace Detail {
+
+            double weighted_average_quantile(int k,
+                                             int q,
+                                             std::vector<double>::iterator first,
+                                             std::vector<double>::iterator last) {
+                auto count = last - first;
+                double idx = (count - 1) * k / static_cast<double>(q);
+                int j = static_cast<int>(idx);
+                double g = idx - j;
+                std::nth_element(first, first + j, last);
+                auto xj = first[j];
+                if (g == 0)
+                    return xj;
+
+                auto xj1 = *std::min_element(first + (j + 1), last);
+                return xj + g * (xj1 - xj);
+            }
+
+            double erfc_inv(double x) { return erf_inv(1.0 - x); }
+
+            double normal_quantile(double p) {
+                static const double ROOT_TWO = std::sqrt(2.0);
+
+                double result = 0.0;
+                assert(p >= 0 && p <= 1);
+                if (p < 0 || p > 1) {
+                    return result;
+                }
+
+                result = -erfc_inv(2.0 * p);
+                // result *= normal distribution standard deviation (1.0) *
+                // sqrt(2)
+                result *= /*sd * */ ROOT_TWO;
+                // result += normal disttribution mean (0)
+                return result;
+            }
+
+            double outlier_variance(Estimate<double> mean, Estimate<double> stddev, int n) {
+                double sb = stddev.point;
+                double mn = mean.point / n;
+                double mg_min = mn / 2.;
+                double sg = (std::min)(mg_min / 4., sb / std::sqrt(n));
+                double sg2 = sg * sg;
+                double sb2 = sb * sb;
+
+                auto c_max = [n, mn, sb2, sg2](double x) -> double {
+                    double k = mn - x;
+                    double d = k * k;
+                    double nd = n * d;
+                    double k0 = -n * nd;
+                    double k1 = sb2 - n * sg2 + nd;
+                    double det = k1 * k1 - 4 * sg2 * k0;
+                    return (int)(-2. * k0 / (k1 + std::sqrt(det)));
+                };
+
+                auto var_out = [n, sb2, sg2](double c) {
+                    double nc = n - c;
+                    return (nc / n) * (sb2 - nc * sg2);
+                };
+
+                return (std::min)(var_out(1), var_out((std::min)(c_max(0.), c_max(mg_min)))) / sb2;
+            }
+
+            bootstrap_analysis analyse_samples(double confidence_level,
+                                               int n_resamples,
+                                               std::vector<double>::iterator first,
+                                               std::vector<double>::iterator last) {
+                CATCH_INTERNAL_START_WARNINGS_SUPPRESSION
+                CATCH_INTERNAL_SUPPRESS_GLOBALS_WARNINGS
+                static std::random_device entropy;
+                CATCH_INTERNAL_STOP_WARNINGS_SUPPRESSION
+
+                auto n = static_cast<int>(last - first); // seriously, one can't use integral types
+                                                         // without hell in C++
+
+                auto mean = &Detail::mean<std::vector<double>::iterator>;
+                auto stddev = &standard_deviation;
+
+#if defined(CATCH_CONFIG_USE_ASYNC)
+                auto Estimate =
+                    [=](double (*f)(std::vector<double>::iterator, std::vector<double>::iterator)) {
+                        auto seed = entropy();
+                        return std::async(std::launch::async, [=] {
+                            std::mt19937 rng(seed);
+                            auto resampled = resample(rng, n_resamples, first, last, f);
+                            return bootstrap(confidence_level, first, last, resampled, f);
+                        });
+                    };
+
+                auto mean_future = Estimate(mean);
+                auto stddev_future = Estimate(stddev);
+
+                auto mean_estimate = mean_future.get();
+                auto stddev_estimate = stddev_future.get();
+#else
+                auto Estimate =
+                    [=](double (*f)(std::vector<double>::iterator, std::vector<double>::iterator)) {
+                        auto seed = entropy();
+                        std::mt19937 rng(seed);
+                        auto resampled = resample(rng, n_resamples, first, last, f);
+                        return bootstrap(confidence_level, first, last, resampled, f);
+                    };
+
+                auto mean_estimate = Estimate(mean);
+                auto stddev_estimate = Estimate(stddev);
+#endif // CATCH_USE_ASYNC
+
+                double outlier_variance =
+                    Detail::outlier_variance(mean_estimate, stddev_estimate, n);
+
+                return {mean_estimate, stddev_estimate, outlier_variance};
+            }
+        } // namespace Detail
+    }     // namespace Benchmark
+} // namespace Catch
+
+#endif // CATCH_CONFIG_ENABLE_BENCHMARKING
+// end catch_stats.cpp
+// start catch_approx.cpp
+
+#include <cmath>
+#include <limits>
+
+namespace {
+
+    // Performs equivalent check of std::fabs(lhs - rhs) <= margin
+    // But without the subtraction to allow for INFINITY in comparison
+    bool marginComparison(double lhs, double rhs, double margin) {
+        return (lhs + margin >= rhs) && (rhs + margin >= lhs);
+    }
+
+} // namespace
+
+namespace Catch {
+    namespace Detail {
+
+        Approx::Approx(double value) :
+            m_epsilon(std::numeric_limits<float>::epsilon() * 100),
+            m_margin(0.0),
+            m_scale(0.0),
+            m_value(value) {}
+
+        Approx Approx::custom() { return Approx(0); }
+
+        Approx Approx::operator-() const {
+            auto temp(*this);
+            temp.m_value = -temp.m_value;
+            return temp;
+        }
+
+        std::string Approx::toString() const {
+            ReusableStringStream rss;
+            rss << "Approx( " << ::Catch::Detail::stringify(m_value) << " )";
+            return rss.str();
+        }
+
+        bool Approx::equalityComparisonImpl(const double other) const {
+            // First try with fixed margin, then compute margin based on
+            // epsilon, scale and Approx's value Thanks to Richard Harris for
+            // his help refining the scaled margin value
+            return marginComparison(m_value, other, m_margin) ||
+                   marginComparison(m_value,
+                                    other,
+                                    m_epsilon *
+                                        (m_scale + std::fabs(std::isinf(m_value) ? 0 : m_value)));
+        }
+
+        void Approx::setMargin(double newMargin) {
+            CATCH_ENFORCE(newMargin >= 0,
+                          "Invalid Approx::margin: " << newMargin << '.'
+                                                     << " Approx::Margin has to be non-negative.");
+            m_margin = newMargin;
+        }
+
+        void Approx::setEpsilon(double newEpsilon) {
+            CATCH_ENFORCE(newEpsilon >= 0 && newEpsilon <= 1.0,
+                          "Invalid Approx::epsilon: " << newEpsilon << '.'
+                                                      << " Approx::epsilon has to be in [0, 1]");
+            m_epsilon = newEpsilon;
+        }
+
+    } // end namespace Detail
+
+    namespace literals {
+        Detail::Approx operator"" _a(long double val) { return Detail::Approx(val); }
+        Detail::Approx operator"" _a(unsigned long long val) { return Detail::Approx(val); }
+    } // end namespace literals
+
+    std::string StringMaker<Catch::Detail::Approx>::convert(Catch::Detail::Approx const& value) {
+        return value.toString();
+    }
+
+} // end namespace Catch
+// end catch_approx.cpp
+// start catch_assertionhandler.cpp
+
+// start catch_debugger.h
+
+namespace Catch {
+    bool isDebuggerActive();
+}
+
+#ifdef CATCH_PLATFORM_MAC
+
+#if defined(__i386__) || defined(__x86_64__)
+#define CATCH_TRAP() __asm__("int $3\n" : :) /* NOLINT */
+#elif defined(__aarch64__)
+#define CATCH_TRAP() __asm__(".inst 0xd4200000")
+#endif
+
+#elif defined(CATCH_PLATFORM_IPHONE)
+
+// use inline assembler
+#if defined(__i386__) || defined(__x86_64__)
+#define CATCH_TRAP() __asm__("int $3")
+#elif defined(__aarch64__)
+#define CATCH_TRAP() __asm__(".inst 0xd4200000")
+#elif defined(__arm__) && !defined(__thumb__)
+#define CATCH_TRAP() __asm__(".inst 0xe7f001f0")
+#elif defined(__arm__) && defined(__thumb__)
+#define CATCH_TRAP() __asm__(".inst 0xde01")
+#endif
+
+#elif defined(CATCH_PLATFORM_LINUX)
+// If we can use inline assembler, do it because this allows us to break
+// directly at the location of the failing check instead of breaking inside
+// raise() called from it, i.e. one stack frame below.
+#if defined(__GNUC__) && (defined(__i386) || defined(__x86_64))
+#define CATCH_TRAP() asm volatile("int $3") /* NOLINT */
+#else                                       // Fall back to the generic way.
+#include <signal.h>
+
+#define CATCH_TRAP() raise(SIGTRAP)
+#endif
+#elif defined(_MSC_VER)
+#define CATCH_TRAP() __debugbreak()
+#elif defined(__MINGW32__)
+extern "C" __declspec(dllimport) void __stdcall DebugBreak();
+#define CATCH_TRAP() DebugBreak()
+#endif
+
+#ifndef CATCH_BREAK_INTO_DEBUGGER
+#ifdef CATCH_TRAP
+#define CATCH_BREAK_INTO_DEBUGGER()                                                                \
+    [] {                                                                                           \
+        if (Catch::isDebuggerActive()) {                                                           \
+            CATCH_TRAP();                                                                          \
+        }                                                                                          \
+    }()
+#else
+#define CATCH_BREAK_INTO_DEBUGGER() [] {}()
+#endif
+#endif
+
+// end catch_debugger.h
+// start catch_run_context.h
+
+// start catch_fatal_condition.h
+
+#include <cassert>
+
+namespace Catch {
+
+    // Wrapper for platform-specific fatal error (signals/SEH) handlers
+    //
+    // Tries to be cooperative with other handlers, and not step over
+    // other handlers. This means that unknown structured exceptions
+    // are passed on, previous signal handlers are called, and so on.
+    //
+    // Can only be instantiated once, and assumes that once a signal
+    // is caught, the binary will end up terminating. Thus, there
+    class FatalConditionHandler {
+        bool m_started = false;
+
+        // Install/disengage implementation for specific platform.
+        // Should be if-defed to work on current platform, can assume
+        // engage-disengage 1:1 pairing.
+        void engage_platform();
+        void disengage_platform();
+
+    public:
+        // Should also have platform-specific implementations as needed
+        FatalConditionHandler();
+        ~FatalConditionHandler();
+
+        void engage() {
+            assert(!m_started && "Handler cannot be installed twice.");
+            m_started = true;
+            engage_platform();
+        }
+
+        void disengage() {
+            assert(m_started && "Handler cannot be uninstalled without being installed first");
+            m_started = false;
+            disengage_platform();
+        }
+    };
+
+    //! Simple RAII guard for (dis)engaging the FatalConditionHandler
+    class FatalConditionHandlerGuard {
+        FatalConditionHandler* m_handler;
+
+    public:
+        FatalConditionHandlerGuard(FatalConditionHandler* handler) : m_handler(handler) {
+            m_handler->engage();
+        }
+        ~FatalConditionHandlerGuard() { m_handler->disengage(); }
+    };
+
+} // end namespace Catch
+
+// end catch_fatal_condition.h
+#include <string>
+
+namespace Catch {
+
+    struct IMutableContext;
+
+    ///////////////////////////////////////////////////////////////////////////
+
+    class RunContext : public IResultCapture, public IRunner {
+
+    public:
+        RunContext(RunContext const&) = delete;
+        RunContext& operator=(RunContext const&) = delete;
+
+        explicit RunContext(IConfigPtr const& _config, IStreamingReporterPtr&& reporter);
+
+        ~RunContext() override;
+
+        void testGroupStarting(std::string const& testSpec,
+                               std::size_t groupIndex,
+                               std::size_t groupsCount);
+        void testGroupEnded(std::string const& testSpec,
+                            Totals const& totals,
+                            std::size_t groupIndex,
+                            std::size_t groupsCount);
+
+        Totals runTest(TestCase const& testCase);
+
+        IConfigPtr config() const;
+        IStreamingReporter& reporter() const;
+
+    public: // IResultCapture
+        // Assertion handlers
+        void handleExpr(AssertionInfo const& info,
+                        ITransientExpression const& expr,
+                        AssertionReaction& reaction) override;
+        void handleMessage(AssertionInfo const& info,
+                           ResultWas::OfType resultType,
+                           StringRef const& message,
+                           AssertionReaction& reaction) override;
+        void handleUnexpectedExceptionNotThrown(AssertionInfo const& info,
+                                                AssertionReaction& reaction) override;
+        void handleUnexpectedInflightException(AssertionInfo const& info,
+                                               std::string const& message,
+                                               AssertionReaction& reaction) override;
+        void handleIncomplete(AssertionInfo const& info) override;
+        void handleNonExpr(AssertionInfo const& info,
+                           ResultWas::OfType resultType,
+                           AssertionReaction& reaction) override;
+
+        bool sectionStarted(SectionInfo const& sectionInfo, Counts& assertions) override;
+
+        void sectionEnded(SectionEndInfo const& endInfo) override;
+        void sectionEndedEarly(SectionEndInfo const& endInfo) override;
+
+        auto acquireGeneratorTracker(StringRef generatorName, SourceLineInfo const& lineInfo)
+            -> IGeneratorTracker& override;
+
+#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING)
+        void benchmarkPreparing(std::string const& name) override;
+        void benchmarkStarting(BenchmarkInfo const& info) override;
+        void benchmarkEnded(BenchmarkStats<> const& stats) override;
+        void benchmarkFailed(std::string const& error) override;
+#endif // CATCH_CONFIG_ENABLE_BENCHMARKING
+
+        void pushScopedMessage(MessageInfo const& message) override;
+        void popScopedMessage(MessageInfo const& message) override;
+
+        void emplaceUnscopedMessage(MessageBuilder const& builder) override;
+
+        std::string getCurrentTestName() const override;
+
+        const AssertionResult* getLastResult() const override;
+
+        void exceptionEarlyReported() override;
+
+        void handleFatalErrorCondition(StringRef message) override;
+
+        bool lastAssertionPassed() override;
+
+        void assertionPassed() override;
+
+    public:
+        // !TBD We need to do this another way!
+        bool aborting() const final;
+
+    private:
+        void runCurrentTest(std::string& redirectedCout, std::string& redirectedCerr);
+        void invokeActiveTestCase();
+
+        void resetAssertionInfo();
+        bool testForMissingAssertions(Counts& assertions);
+
+        void assertionEnded(AssertionResult const& result);
+        void reportExpr(AssertionInfo const& info,
+                        ResultWas::OfType resultType,
+                        ITransientExpression const* expr,
+                        bool negated);
+
+        void populateReaction(AssertionReaction& reaction);
+
+    private:
+        void handleUnfinishedSections();
+
+        TestRunInfo m_runInfo;
+        IMutableContext& m_context;
+        TestCase const* m_activeTestCase = nullptr;
+        ITracker* m_testCaseTracker = nullptr;
+        Option<AssertionResult> m_lastResult;
+
+        IConfigPtr m_config;
+        Totals m_totals;
+        IStreamingReporterPtr m_reporter;
+        std::vector<MessageInfo> m_messages;
+        std::vector<ScopedMessage>
+            m_messageScopes; /* Keeps owners of so-called unscoped messages. */
+        AssertionInfo m_lastAssertionInfo;
+        std::vector<SectionEndInfo> m_unfinishedSections;
+        std::vector<ITracker*> m_activeSections;
+        TrackerContext m_trackerContext;
+        FatalConditionHandler m_fatalConditionhandler;
+        bool m_lastAssertionPassed = false;
+        bool m_shouldReportUnexpected = true;
+        bool m_includeSuccessfulResults;
+    };
+
+    void seedRng(IConfig const& config);
+    unsigned int rngSeed();
+} // end namespace Catch
+
+// end catch_run_context.h
+namespace Catch {
+
+    namespace {
+        auto operator<<(std::ostream& os, ITransientExpression const& expr) -> std::ostream& {
+            expr.streamReconstructedExpression(os);
+            return os;
+        }
+    } // namespace
+
+    LazyExpression::LazyExpression(bool isNegated) : m_isNegated(isNegated) {}
+
+    LazyExpression::LazyExpression(LazyExpression const& other) : m_isNegated(other.m_isNegated) {}
+
+    LazyExpression::operator bool() const { return m_transientExpression != nullptr; }
+
+    auto operator<<(std::ostream& os, LazyExpression const& lazyExpr) -> std::ostream& {
+        if (lazyExpr.m_isNegated)
+            os << "!";
+
+        if (lazyExpr) {
+            if (lazyExpr.m_isNegated && lazyExpr.m_transientExpression->isBinaryExpression())
+                os << "(" << *lazyExpr.m_transientExpression << ")";
+            else
+                os << *lazyExpr.m_transientExpression;
+        } else {
+            os << "{** error - unchecked empty expression requested **}";
+        }
+        return os;
+    }
+
+    AssertionHandler::AssertionHandler(StringRef const& macroName,
+                                       SourceLineInfo const& lineInfo,
+                                       StringRef capturedExpression,
+                                       ResultDisposition::Flags resultDisposition) :
+        m_assertionInfo{macroName, lineInfo, capturedExpression, resultDisposition},
+        m_resultCapture(getResultCapture()) {}
+
+    void AssertionHandler::handleExpr(ITransientExpression const& expr) {
+        m_resultCapture.handleExpr(m_assertionInfo, expr, m_reaction);
+    }
+    void AssertionHandler::handleMessage(ResultWas::OfType resultType, StringRef const& message) {
+        m_resultCapture.handleMessage(m_assertionInfo, resultType, message, m_reaction);
+    }
+
+    auto AssertionHandler::allowThrows() const -> bool {
+        return getCurrentContext().getConfig()->allowThrows();
+    }
+
+    void AssertionHandler::complete() {
+        setCompleted();
+        if (m_reaction.shouldDebugBreak) {
+
+            // If you find your debugger stopping you here then go one level up
+            // on the call-stack for the code that caused it (typically a failed
+            // assertion)
+
+            // (To go back to the test and change execution, jump over the
+            // throw, next)
+            CATCH_BREAK_INTO_DEBUGGER();
+        }
+        if (m_reaction.shouldThrow) {
+#if !defined(CATCH_CONFIG_DISABLE_EXCEPTIONS)
+            throw Catch::TestFailureException();
+#else
+            CATCH_ERROR("Test failure requires aborting test!");
+#endif
+        }
+    }
+    void AssertionHandler::setCompleted() { m_completed = true; }
+
+    void AssertionHandler::handleUnexpectedInflightException() {
+        m_resultCapture.handleUnexpectedInflightException(
+            m_assertionInfo, Catch::translateActiveException(), m_reaction);
+    }
+
+    void AssertionHandler::handleExceptionThrownAsExpected() {
+        m_resultCapture.handleNonExpr(m_assertionInfo, ResultWas::Ok, m_reaction);
+    }
+    void AssertionHandler::handleExceptionNotThrownAsExpected() {
+        m_resultCapture.handleNonExpr(m_assertionInfo, ResultWas::Ok, m_reaction);
+    }
+
+    void AssertionHandler::handleUnexpectedExceptionNotThrown() {
+        m_resultCapture.handleUnexpectedExceptionNotThrown(m_assertionInfo, m_reaction);
+    }
+
+    void AssertionHandler::handleThrowingCallSkipped() {
+        m_resultCapture.handleNonExpr(m_assertionInfo, ResultWas::Ok, m_reaction);
+    }
+
+    // This is the overload that takes a string and infers the Equals matcher
+    // from it The more general overload, that takes any string matcher, is in
+    // catch_capture_matchers.cpp
+    void handleExceptionMatchExpr(AssertionHandler& handler,
+                                  std::string const& str,
+                                  StringRef const& matcherString) {
+        handleExceptionMatchExpr(handler, Matchers::Equals(str), matcherString);
+    }
+
+} // namespace Catch
+// end catch_assertionhandler.cpp
+// start catch_assertionresult.cpp
+
+namespace Catch {
+    AssertionResultData::AssertionResultData(ResultWas::OfType _resultType,
+                                             LazyExpression const& _lazyExpression) :
+        lazyExpression(_lazyExpression), resultType(_resultType) {}
+
+    std::string AssertionResultData::reconstructExpression() const {
+
+        if (reconstructedExpression.empty()) {
+            if (lazyExpression) {
+                ReusableStringStream rss;
+                rss << lazyExpression;
+                reconstructedExpression = rss.str();
+            }
+        }
+        return reconstructedExpression;
+    }
+
+    AssertionResult::AssertionResult(AssertionInfo const& info, AssertionResultData const& data) :
+        m_info(info), m_resultData(data) {}
+
+    // Result was a success
+    bool AssertionResult::succeeded() const { return Catch::isOk(m_resultData.resultType); }
+
+    // Result was a success, or failure is suppressed
+    bool AssertionResult::isOk() const {
+        return Catch::isOk(m_resultData.resultType) ||
+               shouldSuppressFailure(m_info.resultDisposition);
+    }
+
+    ResultWas::OfType AssertionResult::getResultType() const { return m_resultData.resultType; }
+
+    bool AssertionResult::hasExpression() const { return !m_info.capturedExpression.empty(); }
+
+    bool AssertionResult::hasMessage() const { return !m_resultData.message.empty(); }
+
+    std::string AssertionResult::getExpression() const {
+        // Possibly overallocating by 3 characters should be basically free
+        std::string expr;
+        expr.reserve(m_info.capturedExpression.size() + 3);
+        if (isFalseTest(m_info.resultDisposition)) {
+            expr += "!(";
+        }
+        expr += m_info.capturedExpression;
+        if (isFalseTest(m_info.resultDisposition)) {
+            expr += ')';
+        }
+        return expr;
+    }
+
+    std::string AssertionResult::getExpressionInMacro() const {
+        std::string expr;
+        if (m_info.macroName.empty())
+            expr = static_cast<std::string>(m_info.capturedExpression);
+        else {
+            expr.reserve(m_info.macroName.size() + m_info.capturedExpression.size() + 4);
+            expr += m_info.macroName;
+            expr += "( ";
+            expr += m_info.capturedExpression;
+            expr += " )";
+        }
+        return expr;
+    }
+
+    bool AssertionResult::hasExpandedExpression() const {
+        return hasExpression() && getExpandedExpression() != getExpression();
+    }
+
+    std::string AssertionResult::getExpandedExpression() const {
+        std::string expr = m_resultData.reconstructExpression();
+        return expr.empty() ? getExpression() : expr;
+    }
+
+    std::string AssertionResult::getMessage() const { return m_resultData.message; }
+    SourceLineInfo AssertionResult::getSourceInfo() const { return m_info.lineInfo; }
+
+    StringRef AssertionResult::getTestMacroName() const { return m_info.macroName; }
+
+} // end namespace Catch
+// end catch_assertionresult.cpp
+// start catch_capture_matchers.cpp
+
+namespace Catch {
+
+    using StringMatcher = Matchers::Impl::MatcherBase<std::string>;
+
+    // This is the general overload that takes a any string matcher
+    // There is another overload, in catch_assertionhandler.h/.cpp, that only
+    // takes a string and infers the Equals matcher (so the header does not
+    // mention matchers)
+    void handleExceptionMatchExpr(AssertionHandler& handler,
+                                  StringMatcher const& matcher,
+                                  StringRef const& matcherString) {
+        std::string exceptionMessage = Catch::translateActiveException();
+        MatchExpr<std::string, StringMatcher const&> expr(exceptionMessage, matcher, matcherString);
+        handler.handleExpr(expr);
+    }
+
+} // namespace Catch
+// end catch_capture_matchers.cpp
+// start catch_commandline.cpp
+
+// start catch_commandline.h
+
+// start catch_clara.h
+
+// Use Catch's value for console width (store Clara's off to the side, if
+// present)
+#ifdef CLARA_CONFIG_CONSOLE_WIDTH
+#define CATCH_TEMP_CLARA_CONFIG_CONSOLE_WIDTH CATCH_CLARA_TEXTFLOW_CONFIG_CONSOLE_WIDTH
+#undef CATCH_CLARA_TEXTFLOW_CONFIG_CONSOLE_WIDTH
+#endif
+#define CATCH_CLARA_TEXTFLOW_CONFIG_CONSOLE_WIDTH CATCH_CONFIG_CONSOLE_WIDTH - 1
+
+#ifdef __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wweak-vtables"
+#pragma clang diagnostic ignored "-Wexit-time-destructors"
+#pragma clang diagnostic ignored "-Wshadow"
+#endif
+
+// start clara.hpp
+// Copyright 2017 Two Blue Cubes Ltd. All rights reserved.
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+// See https://github.com/philsquared/Clara for more details
+
+// Clara v1.1.5
+
+#ifndef CATCH_CLARA_CONFIG_CONSOLE_WIDTH
+#define CATCH_CLARA_CONFIG_CONSOLE_WIDTH 80
+#endif
+
+#ifndef CATCH_CLARA_TEXTFLOW_CONFIG_CONSOLE_WIDTH
+#define CATCH_CLARA_TEXTFLOW_CONFIG_CONSOLE_WIDTH CATCH_CLARA_CONFIG_CONSOLE_WIDTH
+#endif
+
+#ifndef CLARA_CONFIG_OPTIONAL_TYPE
+#ifdef __has_include
+#if __has_include(<optional>) && __cplusplus >= 201703L
+#include <optional>
+#define CLARA_CONFIG_OPTIONAL_TYPE std::optional
+#endif
+#endif
+#endif
+
+// ----------- #included from clara_textflow.hpp -----------
+
+// TextFlowCpp
+//
+// A single-header library for wrapping and laying out basic text, by Phil Nash
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+// This project is hosted at https://github.com/philsquared/textflowcpp
+
+#include <cassert>
+#include <ostream>
+#include <sstream>
+#include <vector>
+
+#ifndef CATCH_CLARA_TEXTFLOW_CONFIG_CONSOLE_WIDTH
+#define CATCH_CLARA_TEXTFLOW_CONFIG_CONSOLE_WIDTH 80
+#endif
+
+namespace Catch {
+    namespace clara {
+        namespace TextFlow {
+
+            inline auto isWhitespace(char c) -> bool {
+                static std::string chars = " \t\n\r";
+                return chars.find(c) != std::string::npos;
+            }
+            inline auto isBreakableBefore(char c) -> bool {
+                static std::string chars = "[({<|";
+                return chars.find(c) != std::string::npos;
+            }
+            inline auto isBreakableAfter(char c) -> bool {
+                static std::string chars = "])}>.,:;*+-=&/\\";
+                return chars.find(c) != std::string::npos;
+            }
+
+            class Columns;
+
+            class Column {
+                std::vector<std::string> m_strings;
+                size_t m_width = CATCH_CLARA_TEXTFLOW_CONFIG_CONSOLE_WIDTH;
+                size_t m_indent = 0;
+                size_t m_initialIndent = std::string::npos;
+
+            public:
+                class iterator {
+                    friend Column;
+
+                    Column const& m_column;
+                    size_t m_stringIndex = 0;
+                    size_t m_pos = 0;
+
+                    size_t m_len = 0;
+                    size_t m_end = 0;
+                    bool m_suffix = false;
+
+                    iterator(Column const& column, size_t stringIndex) :
+                        m_column(column), m_stringIndex(stringIndex) {}
+
+                    auto line() const -> std::string const& {
+                        return m_column.m_strings[m_stringIndex];
+                    }
+
+                    auto isBoundary(size_t at) const -> bool {
+                        assert(at > 0);
+                        assert(at <= line().size());
+
+                        return at == line().size() ||
+                               (isWhitespace(line()[at]) && !isWhitespace(line()[at - 1])) ||
+                               isBreakableBefore(line()[at]) || isBreakableAfter(line()[at - 1]);
+                    }
+
+                    void calcLength() {
+                        assert(m_stringIndex < m_column.m_strings.size());
+
+                        m_suffix = false;
+                        auto width = m_column.m_width - indent();
+                        m_end = m_pos;
+                        if (line()[m_pos] == '\n') {
+                            ++m_end;
+                        }
+                        while (m_end < line().size() && line()[m_end] != '\n')
+                            ++m_end;
+
+                        if (m_end < m_pos + width) {
+                            m_len = m_end - m_pos;
+                        } else {
+                            size_t len = width;
+                            while (len > 0 && !isBoundary(m_pos + len))
+                                --len;
+                            while (len > 0 && isWhitespace(line()[m_pos + len - 1]))
+                                --len;
+
+                            if (len > 0) {
+                                m_len = len;
+                            } else {
+                                m_suffix = true;
+                                m_len = width - 1;
+                            }
+                        }
+                    }
+
+                    auto indent() const -> size_t {
+                        auto initial = m_pos == 0 && m_stringIndex == 0 ? m_column.m_initialIndent
+                                                                        : std::string::npos;
+                        return initial == std::string::npos ? m_column.m_indent : initial;
+                    }
+
+                    auto addIndentAndSuffix(std::string const& plain) const -> std::string {
+                        return std::string(indent(), ' ') + (m_suffix ? plain + "-" : plain);
+                    }
+
+                public:
+                    using difference_type = std::ptrdiff_t;
+                    using value_type = std::string;
+                    using pointer = value_type*;
+                    using reference = value_type&;
+                    using iterator_category = std::forward_iterator_tag;
+
+                    explicit iterator(Column const& column) : m_column(column) {
+                        assert(m_column.m_width > m_column.m_indent);
+                        assert(m_column.m_initialIndent == std::string::npos ||
+                               m_column.m_width > m_column.m_initialIndent);
+                        calcLength();
+                        if (m_len == 0)
+                            m_stringIndex++; // Empty string
+                    }
+
+                    auto operator*() const -> std::string {
+                        assert(m_stringIndex < m_column.m_strings.size());
+                        assert(m_pos <= m_end);
+                        return addIndentAndSuffix(line().substr(m_pos, m_len));
+                    }
+
+                    auto operator++() -> iterator& {
+                        m_pos += m_len;
+                        if (m_pos < line().size() && line()[m_pos] == '\n')
+                            m_pos += 1;
+                        else
+                            while (m_pos < line().size() && isWhitespace(line()[m_pos]))
+                                ++m_pos;
+
+                        if (m_pos == line().size()) {
+                            m_pos = 0;
+                            ++m_stringIndex;
+                        }
+                        if (m_stringIndex < m_column.m_strings.size())
+                            calcLength();
+                        return *this;
+                    }
+                    auto operator++(int) -> iterator {
+                        iterator prev(*this);
+                        operator++();
+                        return prev;
+                    }
+
+                    auto operator==(iterator const& other) const -> bool {
+                        return m_pos == other.m_pos && m_stringIndex == other.m_stringIndex &&
+                               &m_column == &other.m_column;
+                    }
+                    auto operator!=(iterator const& other) const -> bool {
+                        return !operator==(other);
+                    }
+                };
+                using const_iterator = iterator;
+
+                explicit Column(std::string const& text) { m_strings.push_back(text); }
+
+                auto width(size_t newWidth) -> Column& {
+                    assert(newWidth > 0);
+                    m_width = newWidth;
+                    return *this;
+                }
+                auto indent(size_t newIndent) -> Column& {
+                    m_indent = newIndent;
+                    return *this;
+                }
+                auto initialIndent(size_t newIndent) -> Column& {
+                    m_initialIndent = newIndent;
+                    return *this;
+                }
+
+                auto width() const -> size_t { return m_width; }
+                auto begin() const -> iterator { return iterator(*this); }
+                auto end() const -> iterator { return {*this, m_strings.size()}; }
+
+                inline friend std::ostream& operator<<(std::ostream& os, Column const& col) {
+                    bool first = true;
+                    for (auto line : col) {
+                        if (first)
+                            first = false;
+                        else
+                            os << "\n";
+                        os << line;
+                    }
+                    return os;
+                }
+
+                auto operator+(Column const& other) -> Columns;
+
+                auto toString() const -> std::string {
+                    std::ostringstream oss;
+                    oss << *this;
+                    return oss.str();
+                }
+            };
+
+            class Spacer : public Column {
+
+            public:
+                explicit Spacer(size_t spaceWidth) : Column("") { width(spaceWidth); }
+            };
+
+            class Columns {
+                std::vector<Column> m_columns;
+
+            public:
+                class iterator {
+                    friend Columns;
+                    struct EndTag {};
+
+                    std::vector<Column> const& m_columns;
+                    std::vector<Column::iterator> m_iterators;
+                    size_t m_activeIterators;
+
+                    iterator(Columns const& columns, EndTag) :
+                        m_columns(columns.m_columns), m_activeIterators(0) {
+                        m_iterators.reserve(m_columns.size());
+
+                        for (auto const& col : m_columns)
+                            m_iterators.push_back(col.end());
+                    }
+
+                public:
+                    using difference_type = std::ptrdiff_t;
+                    using value_type = std::string;
+                    using pointer = value_type*;
+                    using reference = value_type&;
+                    using iterator_category = std::forward_iterator_tag;
+
+                    explicit iterator(Columns const& columns) :
+                        m_columns(columns.m_columns), m_activeIterators(m_columns.size()) {
+                        m_iterators.reserve(m_columns.size());
+
+                        for (auto const& col : m_columns)
+                            m_iterators.push_back(col.begin());
+                    }
+
+                    auto operator==(iterator const& other) const -> bool {
+                        return m_iterators == other.m_iterators;
+                    }
+                    auto operator!=(iterator const& other) const -> bool {
+                        return m_iterators != other.m_iterators;
+                    }
+                    auto operator*() const -> std::string {
+                        std::string row, padding;
+
+                        for (size_t i = 0; i < m_columns.size(); ++i) {
+                            auto width = m_columns[i].width();
+                            if (m_iterators[i] != m_columns[i].end()) {
+                                std::string col = *m_iterators[i];
+                                row += padding + col;
+                                if (col.size() < width)
+                                    padding = std::string(width - col.size(), ' ');
+                                else
+                                    padding = "";
+                            } else {
+                                padding += std::string(width, ' ');
+                            }
+                        }
+                        return row;
+                    }
+                    auto operator++() -> iterator& {
+                        for (size_t i = 0; i < m_columns.size(); ++i) {
+                            if (m_iterators[i] != m_columns[i].end())
+                                ++m_iterators[i];
+                        }
+                        return *this;
+                    }
+                    auto operator++(int) -> iterator {
+                        iterator prev(*this);
+                        operator++();
+                        return prev;
+                    }
+                };
+                using const_iterator = iterator;
+
+                auto begin() const -> iterator { return iterator(*this); }
+                auto end() const -> iterator { return {*this, iterator::EndTag()}; }
+
+                auto operator+=(Column const& col) -> Columns& {
+                    m_columns.push_back(col);
+                    return *this;
+                }
+                auto operator+(Column const& col) -> Columns {
+                    Columns combined = *this;
+                    combined += col;
+                    return combined;
+                }
+
+                inline friend std::ostream& operator<<(std::ostream& os, Columns const& cols) {
+
+                    bool first = true;
+                    for (auto line : cols) {
+                        if (first)
+                            first = false;
+                        else
+                            os << "\n";
+                        os << line;
+                    }
+                    return os;
+                }
+
+                auto toString() const -> std::string {
+                    std::ostringstream oss;
+                    oss << *this;
+                    return oss.str();
+                }
+            };
+
+            inline auto Column::operator+(Column const& other) -> Columns {
+                Columns cols;
+                cols += *this;
+                cols += other;
+                return cols;
+            }
+        } // namespace TextFlow
+
+    } // namespace clara
+} // namespace Catch
+
+// ----------- end of #include from clara_textflow.hpp -----------
+// ........... back in clara.hpp
+
+#include <cctype>
+#include <string>
+#include <memory>
+#include <set>
+#include <algorithm>
+
+#if !defined(CATCH_PLATFORM_WINDOWS) &&                                                            \
+    (defined(WIN32) || defined(__WIN32__) || defined(_WIN32) || defined(_MSC_VER))
+#define CATCH_PLATFORM_WINDOWS
+#endif
+
+namespace Catch {
+    namespace clara {
+        namespace detail {
+
+            // Traits for extracting arg and return type of lambdas (for single
+            // argument lambdas)
+            template <typename L>
+            struct UnaryLambdaTraits : UnaryLambdaTraits<decltype(&L::operator())> {};
+
+            template <typename ClassT, typename ReturnT, typename... Args>
+            struct UnaryLambdaTraits<ReturnT (ClassT::*)(Args...) const> {
+                static const bool isValid = false;
+            };
+
+            template <typename ClassT, typename ReturnT, typename ArgT>
+            struct UnaryLambdaTraits<ReturnT (ClassT::*)(ArgT) const> {
+                static const bool isValid = true;
+                using ArgType =
+                    typename std::remove_const<typename std::remove_reference<ArgT>::type>::type;
+                using ReturnType = ReturnT;
+            };
+
+            class TokenStream;
+
+            // Transport for raw args (copied from main args, or supplied via
+            // init list for testing)
+            class Args {
+                friend TokenStream;
+                std::string m_exeName;
+                std::vector<std::string> m_args;
+
+            public:
+                Args(int argc, char const* const* argv) :
+                    m_exeName(argv[0]), m_args(argv + 1, argv + argc) {}
+
+                Args(std::initializer_list<std::string> args) :
+                    m_exeName(*args.begin()), m_args(args.begin() + 1, args.end()) {}
+
+                auto exeName() const -> std::string { return m_exeName; }
+            };
+
+            // Wraps a token coming from a token stream. These may not directly
+            // correspond to strings as a single string may encode an option +
+            // its argument if the : or = form is used
+            enum class TokenType { Option, Argument };
+            struct Token {
+                TokenType type;
+                std::string token;
+            };
+
+            inline auto isOptPrefix(char c) -> bool {
+                return c == '-'
+#ifdef CATCH_PLATFORM_WINDOWS
+                       || c == '/'
+#endif
+                    ;
+            }
+
+            // Abstracts iterators into args as a stream of tokens, with option
+            // arguments uniformly handled
+            class TokenStream {
+                using Iterator = std::vector<std::string>::const_iterator;
+                Iterator it;
+                Iterator itEnd;
+                std::vector<Token> m_tokenBuffer;
+
+                void loadBuffer() {
+                    m_tokenBuffer.resize(0);
+
+                    // Skip any empty strings
+                    while (it != itEnd && it->empty())
+                        ++it;
+
+                    if (it != itEnd) {
+                        auto const& next = *it;
+                        if (isOptPrefix(next[0])) {
+                            auto delimiterPos = next.find_first_of(" :=");
+                            if (delimiterPos != std::string::npos) {
+                                m_tokenBuffer.push_back(
+                                    {TokenType::Option, next.substr(0, delimiterPos)});
+                                m_tokenBuffer.push_back(
+                                    {TokenType::Argument, next.substr(delimiterPos + 1)});
+                            } else {
+                                if (next[1] != '-' && next.size() > 2) {
+                                    std::string opt = "- ";
+                                    for (size_t i = 1; i < next.size(); ++i) {
+                                        opt[1] = next[i];
+                                        m_tokenBuffer.push_back({TokenType::Option, opt});
+                                    }
+                                } else {
+                                    m_tokenBuffer.push_back({TokenType::Option, next});
+                                }
+                            }
+                        } else {
+                            m_tokenBuffer.push_back({TokenType::Argument, next});
+                        }
+                    }
+                }
+
+            public:
+                explicit TokenStream(Args const& args) :
+                    TokenStream(args.m_args.begin(), args.m_args.end()) {}
+
+                TokenStream(Iterator it, Iterator itEnd) : it(it), itEnd(itEnd) { loadBuffer(); }
+
+                explicit operator bool() const { return !m_tokenBuffer.empty() || it != itEnd; }
+
+                auto count() const -> size_t { return m_tokenBuffer.size() + (itEnd - it); }
+
+                auto operator*() const -> Token {
+                    assert(!m_tokenBuffer.empty());
+                    return m_tokenBuffer.front();
+                }
+
+                auto operator->() const -> Token const* {
+                    assert(!m_tokenBuffer.empty());
+                    return &m_tokenBuffer.front();
+                }
+
+                auto operator++() -> TokenStream& {
+                    if (m_tokenBuffer.size() >= 2) {
+                        m_tokenBuffer.erase(m_tokenBuffer.begin());
+                    } else {
+                        if (it != itEnd)
+                            ++it;
+                        loadBuffer();
+                    }
+                    return *this;
+                }
+            };
+
+            class ResultBase {
+            public:
+                enum Type { Ok, LogicError, RuntimeError };
+
+            protected:
+                ResultBase(Type type) : m_type(type) {}
+                virtual ~ResultBase() = default;
+
+                virtual void enforceOk() const = 0;
+
+                Type m_type;
+            };
+
+            template <typename T> class ResultValueBase : public ResultBase {
+            public:
+                auto value() const -> T const& {
+                    enforceOk();
+                    return m_value;
+                }
+
+            protected:
+                ResultValueBase(Type type) : ResultBase(type) {}
+
+                ResultValueBase(ResultValueBase const& other) : ResultBase(other) {
+                    if (m_type == ResultBase::Ok)
+                        new (&m_value) T(other.m_value);
+                }
+
+                ResultValueBase(Type, T const& value) : ResultBase(Ok) { new (&m_value) T(value); }
+
+                auto operator=(ResultValueBase const& other) -> ResultValueBase& {
+                    if (m_type == ResultBase::Ok)
+                        m_value.~T();
+                    ResultBase::operator=(other);
+                    if (m_type == ResultBase::Ok)
+                        new (&m_value) T(other.m_value);
+                    return *this;
+                }
+
+                ~ResultValueBase() override {
+                    if (m_type == Ok)
+                        m_value.~T();
+                }
+
+                union {
+                    T m_value;
+                };
+            };
+
+            template <> class ResultValueBase<void> : public ResultBase {
+            protected:
+                using ResultBase::ResultBase;
+            };
+
+            template <typename T = void> class BasicResult : public ResultValueBase<T> {
+            public:
+                template <typename U>
+                explicit BasicResult(BasicResult<U> const& other) :
+                    ResultValueBase<T>(other.type()), m_errorMessage(other.errorMessage()) {
+                    assert(type() != ResultBase::Ok);
+                }
+
+                template <typename U> static auto ok(U const& value) -> BasicResult {
+                    return {ResultBase::Ok, value};
+                }
+                static auto ok() -> BasicResult { return {ResultBase::Ok}; }
+                static auto logicError(std::string const& message) -> BasicResult {
+                    return {ResultBase::LogicError, message};
+                }
+                static auto runtimeError(std::string const& message) -> BasicResult {
+                    return {ResultBase::RuntimeError, message};
+                }
+
+                explicit operator bool() const { return m_type == ResultBase::Ok; }
+                auto type() const -> ResultBase::Type { return m_type; }
+                auto errorMessage() const -> std::string { return m_errorMessage; }
+
+            protected:
+                void enforceOk() const override {
+
+                    // Errors shouldn't reach this point, but if they do
+                    // the actual error message will be in m_errorMessage
+                    assert(m_type != ResultBase::LogicError);
+                    assert(m_type != ResultBase::RuntimeError);
+                    if (m_type != ResultBase::Ok)
+                        std::abort();
+                }
+
+                std::string m_errorMessage; // Only populated if resultType is an error
+
+                BasicResult(ResultBase::Type type, std::string const& message) :
+                    ResultValueBase<T>(type), m_errorMessage(message) {
+                    assert(m_type != ResultBase::Ok);
+                }
+
+                using ResultValueBase<T>::ResultValueBase;
+                using ResultBase::m_type;
+            };
+
+            enum class ParseResultType { Matched, NoMatch, ShortCircuitAll, ShortCircuitSame };
+
+            class ParseState {
+            public:
+                ParseState(ParseResultType type, TokenStream const& remainingTokens) :
+                    m_type(type), m_remainingTokens(remainingTokens) {}
+
+                auto type() const -> ParseResultType { return m_type; }
+                auto remainingTokens() const -> TokenStream { return m_remainingTokens; }
+
+            private:
+                ParseResultType m_type;
+                TokenStream m_remainingTokens;
+            };
+
+            using Result = BasicResult<void>;
+            using ParserResult = BasicResult<ParseResultType>;
+            using InternalParseResult = BasicResult<ParseState>;
+
+            struct HelpColumns {
+                std::string left;
+                std::string right;
+            };
+
+            template <typename T>
+            inline auto convertInto(std::string const& source, T& target) -> ParserResult {
+                std::stringstream ss;
+                ss << source;
+                ss >> target;
+                if (ss.fail())
+                    return ParserResult::runtimeError("Unable to convert '" + source +
+                                                      "' to destination type");
+                else
+                    return ParserResult::ok(ParseResultType::Matched);
+            }
+            inline auto convertInto(std::string const& source, std::string& target)
+                -> ParserResult {
+                target = source;
+                return ParserResult::ok(ParseResultType::Matched);
+            }
+            inline auto convertInto(std::string const& source, bool& target) -> ParserResult {
+                std::string srcLC = source;
+                std::transform(srcLC.begin(), srcLC.end(), srcLC.begin(), [](unsigned char c) {
+                    return static_cast<char>(std::tolower(c));
+                });
+                if (srcLC == "y" || srcLC == "1" || srcLC == "true" || srcLC == "yes" ||
+                    srcLC == "on")
+                    target = true;
+                else if (srcLC == "n" || srcLC == "0" || srcLC == "false" || srcLC == "no" ||
+                         srcLC == "off")
+                    target = false;
+                else
+                    return ParserResult::runtimeError(
+                        "Expected a boolean value but did not recognise: '" + source + "'");
+                return ParserResult::ok(ParseResultType::Matched);
+            }
+#ifdef CLARA_CONFIG_OPTIONAL_TYPE
+            template <typename T>
+            inline auto convertInto(std::string const& source,
+                                    CLARA_CONFIG_OPTIONAL_TYPE<T>& target) -> ParserResult {
+                T temp;
+                auto result = convertInto(source, temp);
+                if (result)
+                    target = std::move(temp);
+                return result;
+            }
+#endif // CLARA_CONFIG_OPTIONAL_TYPE
+
+            struct NonCopyable {
+                NonCopyable() = default;
+                NonCopyable(NonCopyable const&) = delete;
+                NonCopyable(NonCopyable&&) = delete;
+                NonCopyable& operator=(NonCopyable const&) = delete;
+                NonCopyable& operator=(NonCopyable&&) = delete;
+            };
+
+            struct BoundRef : NonCopyable {
+                virtual ~BoundRef() = default;
+                virtual auto isContainer() const -> bool { return false; }
+                virtual auto isFlag() const -> bool { return false; }
+            };
+            struct BoundValueRefBase : BoundRef {
+                virtual auto setValue(std::string const& arg) -> ParserResult = 0;
+            };
+            struct BoundFlagRefBase : BoundRef {
+                virtual auto setFlag(bool flag) -> ParserResult = 0;
+                virtual auto isFlag() const -> bool { return true; }
+            };
+
+            template <typename T> struct BoundValueRef : BoundValueRefBase {
+                T& m_ref;
+
+                explicit BoundValueRef(T& ref) : m_ref(ref) {}
+
+                auto setValue(std::string const& arg) -> ParserResult override {
+                    return convertInto(arg, m_ref);
+                }
+            };
+
+            template <typename T> struct BoundValueRef<std::vector<T>> : BoundValueRefBase {
+                std::vector<T>& m_ref;
+
+                explicit BoundValueRef(std::vector<T>& ref) : m_ref(ref) {}
+
+                auto isContainer() const -> bool override { return true; }
+
+                auto setValue(std::string const& arg) -> ParserResult override {
+                    T temp;
+                    auto result = convertInto(arg, temp);
+                    if (result)
+                        m_ref.push_back(temp);
+                    return result;
+                }
+            };
+
+            struct BoundFlagRef : BoundFlagRefBase {
+                bool& m_ref;
+
+                explicit BoundFlagRef(bool& ref) : m_ref(ref) {}
+
+                auto setFlag(bool flag) -> ParserResult override {
+                    m_ref = flag;
+                    return ParserResult::ok(ParseResultType::Matched);
+                }
+            };
+
+            template <typename ReturnType> struct LambdaInvoker {
+                static_assert(std::is_same<ReturnType, ParserResult>::value,
+                              "Lambda must return void or clara::ParserResult");
+
+                template <typename L, typename ArgType>
+                static auto invoke(L const& lambda, ArgType const& arg) -> ParserResult {
+                    return lambda(arg);
+                }
+            };
+
+            template <> struct LambdaInvoker<void> {
+                template <typename L, typename ArgType>
+                static auto invoke(L const& lambda, ArgType const& arg) -> ParserResult {
+                    lambda(arg);
+                    return ParserResult::ok(ParseResultType::Matched);
+                }
+            };
+
+            template <typename ArgType, typename L>
+            inline auto invokeLambda(L const& lambda, std::string const& arg) -> ParserResult {
+                ArgType temp{};
+                auto result = convertInto(arg, temp);
+                return !result ? result
+                               : LambdaInvoker<typename UnaryLambdaTraits<L>::ReturnType>::invoke(
+                                     lambda, temp);
+            }
+
+            template <typename L> struct BoundLambda : BoundValueRefBase {
+                L m_lambda;
+
+                static_assert(UnaryLambdaTraits<L>::isValid,
+                              "Supplied lambda must take exactly one argument");
+                explicit BoundLambda(L const& lambda) : m_lambda(lambda) {}
+
+                auto setValue(std::string const& arg) -> ParserResult override {
+                    return invokeLambda<typename UnaryLambdaTraits<L>::ArgType>(m_lambda, arg);
+                }
+            };
+
+            template <typename L> struct BoundFlagLambda : BoundFlagRefBase {
+                L m_lambda;
+
+                static_assert(UnaryLambdaTraits<L>::isValid,
+                              "Supplied lambda must take exactly one argument");
+                static_assert(std::is_same<typename UnaryLambdaTraits<L>::ArgType, bool>::value,
+                              "flags must be boolean");
+
+                explicit BoundFlagLambda(L const& lambda) : m_lambda(lambda) {}
+
+                auto setFlag(bool flag) -> ParserResult override {
+                    return LambdaInvoker<typename UnaryLambdaTraits<L>::ReturnType>::invoke(
+                        m_lambda, flag);
+                }
+            };
+
+            enum class Optionality { Optional, Required };
+
+            struct Parser;
+
+            class ParserBase {
+            public:
+                virtual ~ParserBase() = default;
+                virtual auto validate() const -> Result { return Result::ok(); }
+                virtual auto parse(std::string const& exeName, TokenStream const& tokens) const
+                    -> InternalParseResult = 0;
+                virtual auto cardinality() const -> size_t { return 1; }
+
+                auto parse(Args const& args) const -> InternalParseResult {
+                    return parse(args.exeName(), TokenStream(args));
+                }
+            };
+
+            template <typename DerivedT> class ComposableParserImpl : public ParserBase {
+            public:
+                template <typename T> auto operator|(T const& other) const -> Parser;
+
+                template <typename T> auto operator+(T const& other) const -> Parser;
+            };
+
+            // Common code and state for Args and Opts
+            template <typename DerivedT>
+            class ParserRefImpl : public ComposableParserImpl<DerivedT> {
+            protected:
+                Optionality m_optionality = Optionality::Optional;
+                std::shared_ptr<BoundRef> m_ref;
+                std::string m_hint;
+                std::string m_description;
+
+                explicit ParserRefImpl(std::shared_ptr<BoundRef> const& ref) : m_ref(ref) {}
+
+            public:
+                template <typename T>
+                ParserRefImpl(T& ref, std::string const& hint) :
+                    m_ref(std::make_shared<BoundValueRef<T>>(ref)), m_hint(hint) {}
+
+                template <typename LambdaT>
+                ParserRefImpl(LambdaT const& ref, std::string const& hint) :
+                    m_ref(std::make_shared<BoundLambda<LambdaT>>(ref)), m_hint(hint) {}
+
+                auto operator()(std::string const& description) -> DerivedT& {
+                    m_description = description;
+                    return static_cast<DerivedT&>(*this);
+                }
+
+                auto optional() -> DerivedT& {
+                    m_optionality = Optionality::Optional;
+                    return static_cast<DerivedT&>(*this);
+                };
+
+                auto required() -> DerivedT& {
+                    m_optionality = Optionality::Required;
+                    return static_cast<DerivedT&>(*this);
+                };
+
+                auto isOptional() const -> bool { return m_optionality == Optionality::Optional; }
+
+                auto cardinality() const -> size_t override {
+                    if (m_ref->isContainer())
+                        return 0;
+                    else
+                        return 1;
+                }
+
+                auto hint() const -> std::string { return m_hint; }
+            };
+
+            class ExeName : public ComposableParserImpl<ExeName> {
+                std::shared_ptr<std::string> m_name;
+                std::shared_ptr<BoundValueRefBase> m_ref;
+
+                template <typename LambdaT>
+                static auto makeRef(LambdaT const& lambda) -> std::shared_ptr<BoundValueRefBase> {
+                    return std::make_shared<BoundLambda<LambdaT>>(lambda);
+                }
+
+            public:
+                ExeName() : m_name(std::make_shared<std::string>("<executable>")) {}
+
+                explicit ExeName(std::string& ref) : ExeName() {
+                    m_ref = std::make_shared<BoundValueRef<std::string>>(ref);
+                }
+
+                template <typename LambdaT> explicit ExeName(LambdaT const& lambda) : ExeName() {
+                    m_ref = std::make_shared<BoundLambda<LambdaT>>(lambda);
+                }
+
+                // The exe name is not parsed out of the normal tokens, but is
+                // handled specially
+                auto parse(std::string const&, TokenStream const& tokens) const
+                    -> InternalParseResult override {
+                    return InternalParseResult::ok(ParseState(ParseResultType::NoMatch, tokens));
+                }
+
+                auto name() const -> std::string { return *m_name; }
+                auto set(std::string const& newName) -> ParserResult {
+
+                    auto lastSlash = newName.find_last_of("\\/");
+                    auto filename =
+                        (lastSlash == std::string::npos) ? newName : newName.substr(lastSlash + 1);
+
+                    *m_name = filename;
+                    if (m_ref)
+                        return m_ref->setValue(filename);
+                    else
+                        return ParserResult::ok(ParseResultType::Matched);
+                }
+            };
+
+            class Arg : public ParserRefImpl<Arg> {
+            public:
+                using ParserRefImpl::ParserRefImpl;
+
+                auto parse(std::string const&, TokenStream const& tokens) const
+                    -> InternalParseResult override {
+                    auto validationResult = validate();
+                    if (!validationResult)
+                        return InternalParseResult(validationResult);
+
+                    auto remainingTokens = tokens;
+                    auto const& token = *remainingTokens;
+                    if (token.type != TokenType::Argument)
+                        return InternalParseResult::ok(
+                            ParseState(ParseResultType::NoMatch, remainingTokens));
+
+                    assert(!m_ref->isFlag());
+                    auto valueRef = static_cast<detail::BoundValueRefBase*>(m_ref.get());
+
+                    auto result = valueRef->setValue(remainingTokens->token);
+                    if (!result)
+                        return InternalParseResult(result);
+                    else
+                        return InternalParseResult::ok(
+                            ParseState(ParseResultType::Matched, ++remainingTokens));
+                }
+            };
+
+            inline auto normaliseOpt(std::string const& optName) -> std::string {
+#ifdef CATCH_PLATFORM_WINDOWS
+                if (optName[0] == '/')
+                    return "-" + optName.substr(1);
+                else
+#endif
+                    return optName;
+            }
+
+            class Opt : public ParserRefImpl<Opt> {
+            protected:
+                std::vector<std::string> m_optNames;
+
+            public:
+                template <typename LambdaT>
+                explicit Opt(LambdaT const& ref) :
+                    ParserRefImpl(std::make_shared<BoundFlagLambda<LambdaT>>(ref)) {}
+
+                explicit Opt(bool& ref) : ParserRefImpl(std::make_shared<BoundFlagRef>(ref)) {}
+
+                template <typename LambdaT>
+                Opt(LambdaT const& ref, std::string const& hint) : ParserRefImpl(ref, hint) {}
+
+                template <typename T>
+                Opt(T& ref, std::string const& hint) : ParserRefImpl(ref, hint) {}
+
+                auto operator[](std::string const& optName) -> Opt& {
+                    m_optNames.push_back(optName);
+                    return *this;
+                }
+
+                auto getHelpColumns() const -> std::vector<HelpColumns> {
+                    std::ostringstream oss;
+                    bool first = true;
+                    for (auto const& opt : m_optNames) {
+                        if (first)
+                            first = false;
+                        else
+                            oss << ", ";
+                        oss << opt;
+                    }
+                    if (!m_hint.empty())
+                        oss << " <" << m_hint << ">";
+                    return {{oss.str(), m_description}};
+                }
+
+                auto isMatch(std::string const& optToken) const -> bool {
+                    auto normalisedToken = normaliseOpt(optToken);
+                    for (auto const& name : m_optNames) {
+                        if (normaliseOpt(name) == normalisedToken)
+                            return true;
+                    }
+                    return false;
+                }
+
+                using ParserBase::parse;
+
+                auto parse(std::string const&, TokenStream const& tokens) const
+                    -> InternalParseResult override {
+                    auto validationResult = validate();
+                    if (!validationResult)
+                        return InternalParseResult(validationResult);
+
+                    auto remainingTokens = tokens;
+                    if (remainingTokens && remainingTokens->type == TokenType::Option) {
+                        auto const& token = *remainingTokens;
+                        if (isMatch(token.token)) {
+                            if (m_ref->isFlag()) {
+                                auto flagRef = static_cast<detail::BoundFlagRefBase*>(m_ref.get());
+                                auto result = flagRef->setFlag(true);
+                                if (!result)
+                                    return InternalParseResult(result);
+                                if (result.value() == ParseResultType::ShortCircuitAll)
+                                    return InternalParseResult::ok(
+                                        ParseState(result.value(), remainingTokens));
+                            } else {
+                                auto valueRef =
+                                    static_cast<detail::BoundValueRefBase*>(m_ref.get());
+                                ++remainingTokens;
+                                if (!remainingTokens)
+                                    return InternalParseResult::runtimeError(
+                                        "Expected argument following " + token.token);
+                                auto const& argToken = *remainingTokens;
+                                if (argToken.type != TokenType::Argument)
+                                    return InternalParseResult::runtimeError(
+                                        "Expected argument following " + token.token);
+                                auto result = valueRef->setValue(argToken.token);
+                                if (!result)
+                                    return InternalParseResult(result);
+                                if (result.value() == ParseResultType::ShortCircuitAll)
+                                    return InternalParseResult::ok(
+                                        ParseState(result.value(), remainingTokens));
+                            }
+                            return InternalParseResult::ok(
+                                ParseState(ParseResultType::Matched, ++remainingTokens));
+                        }
+                    }
+                    return InternalParseResult::ok(
+                        ParseState(ParseResultType::NoMatch, remainingTokens));
+                }
+
+                auto validate() const -> Result override {
+                    if (m_optNames.empty())
+                        return Result::logicError("No options supplied to Opt");
+                    for (auto const& name : m_optNames) {
+                        if (name.empty())
+                            return Result::logicError("Option name cannot be empty");
+#ifdef CATCH_PLATFORM_WINDOWS
+                        if (name[0] != '-' && name[0] != '/')
+                            return Result::logicError("Option name must begin with '-' or '/'");
+#else
+                        if (name[0] != '-')
+                            return Result::logicError("Option name must begin with '-'");
+#endif
+                    }
+                    return ParserRefImpl::validate();
+                }
+            };
+
+            struct Help : Opt {
+                Help(bool& showHelpFlag) :
+                    Opt([&](bool flag) {
+                        showHelpFlag = flag;
+                        return ParserResult::ok(ParseResultType::ShortCircuitAll);
+                    }) {
+                    static_cast<Opt&> (*this)("display usage information")["-?"]["-h"]["--help"]
+                        .optional();
+                }
+            };
+
+            struct Parser : ParserBase {
+
+                mutable ExeName m_exeName;
+                std::vector<Opt> m_options;
+                std::vector<Arg> m_args;
+
+                auto operator|=(ExeName const& exeName) -> Parser& {
+                    m_exeName = exeName;
+                    return *this;
+                }
+
+                auto operator|=(Arg const& arg) -> Parser& {
+                    m_args.push_back(arg);
+                    return *this;
+                }
+
+                auto operator|=(Opt const& opt) -> Parser& {
+                    m_options.push_back(opt);
+                    return *this;
+                }
+
+                auto operator|=(Parser const& other) -> Parser& {
+                    m_options.insert(
+                        m_options.end(), other.m_options.begin(), other.m_options.end());
+                    m_args.insert(m_args.end(), other.m_args.begin(), other.m_args.end());
+                    return *this;
+                }
+
+                template <typename T> auto operator|(T const& other) const -> Parser {
+                    return Parser(*this) |= other;
+                }
+
+                // Forward deprecated interface with '+' instead of '|'
+                template <typename T> auto operator+=(T const& other) -> Parser& {
+                    return operator|=(other);
+                }
+                template <typename T> auto operator+(T const& other) const -> Parser {
+                    return operator|(other);
+                }
+
+                auto getHelpColumns() const -> std::vector<HelpColumns> {
+                    std::vector<HelpColumns> cols;
+                    for (auto const& o : m_options) {
+                        auto childCols = o.getHelpColumns();
+                        cols.insert(cols.end(), childCols.begin(), childCols.end());
+                    }
+                    return cols;
+                }
+
+                void writeToStream(std::ostream& os) const {
+                    if (!m_exeName.name().empty()) {
+                        os << "usage:\n"
+                           << "  " << m_exeName.name() << " ";
+                        bool required = true, first = true;
+                        for (auto const& arg : m_args) {
+                            if (first)
+                                first = false;
+                            else
+                                os << " ";
+                            if (arg.isOptional() && required) {
+                                os << "[";
+                                required = false;
+                            }
+                            os << "<" << arg.hint() << ">";
+                            if (arg.cardinality() == 0)
+                                os << " ... ";
+                        }
+                        if (!required)
+                            os << "]";
+                        if (!m_options.empty())
+                            os << " options";
+                        os << "\n\nwhere options are:" << std::endl;
+                    }
+
+                    auto rows = getHelpColumns();
+                    size_t consoleWidth = CATCH_CLARA_CONFIG_CONSOLE_WIDTH;
+                    size_t optWidth = 0;
+                    for (auto const& cols : rows)
+                        optWidth = (std::max)(optWidth, cols.left.size() + 2);
+
+                    optWidth = (std::min)(optWidth, consoleWidth / 2);
+
+                    for (auto const& cols : rows) {
+                        auto row = TextFlow::Column(cols.left).width(optWidth).indent(2) +
+                                   TextFlow::Spacer(4) +
+                                   TextFlow::Column(cols.right).width(consoleWidth - 7 - optWidth);
+                        os << row << std::endl;
+                    }
+                }
+
+                friend auto operator<<(std::ostream& os, Parser const& parser) -> std::ostream& {
+                    parser.writeToStream(os);
+                    return os;
+                }
+
+                auto validate() const -> Result override {
+                    for (auto const& opt : m_options) {
+                        auto result = opt.validate();
+                        if (!result)
+                            return result;
+                    }
+                    for (auto const& arg : m_args) {
+                        auto result = arg.validate();
+                        if (!result)
+                            return result;
+                    }
+                    return Result::ok();
+                }
+
+                using ParserBase::parse;
+
+                auto parse(std::string const& exeName, TokenStream const& tokens) const
+                    -> InternalParseResult override {
+
+                    struct ParserInfo {
+                        ParserBase const* parser = nullptr;
+                        size_t count = 0;
+                    };
+                    const size_t totalParsers = m_options.size() + m_args.size();
+                    assert(totalParsers < 512);
+                    // ParserInfo parseInfos[totalParsers]; // <-- this is what
+                    // we really want to do
+                    ParserInfo parseInfos[512];
+
+                    {
+                        size_t i = 0;
+                        for (auto const& opt : m_options)
+                            parseInfos[i++].parser = &opt;
+                        for (auto const& arg : m_args)
+                            parseInfos[i++].parser = &arg;
+                    }
+
+                    m_exeName.set(exeName);
+
+                    auto result =
+                        InternalParseResult::ok(ParseState(ParseResultType::NoMatch, tokens));
+                    while (result.value().remainingTokens()) {
+                        bool tokenParsed = false;
+
+                        for (size_t i = 0; i < totalParsers; ++i) {
+                            auto& parseInfo = parseInfos[i];
+                            if (parseInfo.parser->cardinality() == 0 ||
+                                parseInfo.count < parseInfo.parser->cardinality()) {
+                                result = parseInfo.parser->parse(exeName,
+                                                                 result.value().remainingTokens());
+                                if (!result)
+                                    return result;
+                                if (result.value().type() != ParseResultType::NoMatch) {
+                                    tokenParsed = true;
+                                    ++parseInfo.count;
+                                    break;
+                                }
+                            }
+                        }
+
+                        if (result.value().type() == ParseResultType::ShortCircuitAll)
+                            return result;
+                        if (!tokenParsed)
+                            return InternalParseResult::runtimeError(
+                                "Unrecognised token: " + result.value().remainingTokens()->token);
+                    }
+                    // !TBD Check missing required options
+                    return result;
+                }
+            };
+
+            template <typename DerivedT>
+            template <typename T>
+            auto ComposableParserImpl<DerivedT>::operator|(T const& other) const -> Parser {
+                return Parser() | static_cast<DerivedT const&>(*this) | other;
+            }
+        } // namespace detail
+
+        // A Combined parser
+        using detail::Parser;
+
+        // A parser for options
+        using detail::Opt;
+
+        // A parser for arguments
+        using detail::Arg;
+
+        // Wrapper for argc, argv from main()
+        using detail::Args;
+
+        // Specifies the name of the executable
+        using detail::ExeName;
+
+        // Convenience wrapper for option parser that specifies the help option
+        using detail::Help;
+
+        // enum of result types from a parse
+        using detail::ParseResultType;
+
+        // Result type for parser operation
+        using detail::ParserResult;
+
+    } // namespace clara
+} // namespace Catch
+
+// end clara.hpp
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
+
+// Restore Clara's value for console width, if present
+#ifdef CATCH_TEMP_CLARA_CONFIG_CONSOLE_WIDTH
+#define CATCH_CLARA_TEXTFLOW_CONFIG_CONSOLE_WIDTH CATCH_TEMP_CLARA_CONFIG_CONSOLE_WIDTH
+#undef CATCH_TEMP_CLARA_CONFIG_CONSOLE_WIDTH
+#endif
+
+// end catch_clara.h
+namespace Catch {
+
+    clara::Parser makeCommandLineParser(ConfigData& config);
+
+} // end namespace Catch
+
+// end catch_commandline.h
+#include <fstream>
+#include <ctime>
+
+namespace Catch {
+
+    clara::Parser makeCommandLineParser(ConfigData& config) {
+
+        using namespace clara;
+
+        auto const setWarning = [&](std::string const& warning) {
+            auto warningSet = [&]() {
+                if (warning == "NoAssertions")
+                    return WarnAbout::NoAssertions;
+
+                if (warning == "NoTests")
+                    return WarnAbout::NoTests;
+
+                return WarnAbout::Nothing;
+            }();
+
+            if (warningSet == WarnAbout::Nothing)
+                return ParserResult::runtimeError("Unrecognised warning: '" + warning + "'");
+            config.warnings = static_cast<WarnAbout::What>(config.warnings | warningSet);
+            return ParserResult::ok(ParseResultType::Matched);
+        };
+        auto const loadTestNamesFromFile = [&](std::string const& filename) {
+            std::ifstream f(filename.c_str());
+            if (!f.is_open())
+                return ParserResult::runtimeError("Unable to load input file: '" + filename + "'");
+
+            std::string line;
+            while (std::getline(f, line)) {
+                line = trim(line);
+                if (!line.empty() && !startsWith(line, '#')) {
+                    if (!startsWith(line, '"'))
+                        line = '"' + line + '"';
+                    config.testsOrTags.push_back(line);
+                    config.testsOrTags.emplace_back(",");
+                }
+            }
+            // Remove comma in the end
+            if (!config.testsOrTags.empty())
+                config.testsOrTags.erase(config.testsOrTags.end() - 1);
+
+            return ParserResult::ok(ParseResultType::Matched);
+        };
+        auto const setTestOrder = [&](std::string const& order) {
+            if (startsWith("declared", order))
+                config.runOrder = RunTests::InDeclarationOrder;
+            else if (startsWith("lexical", order))
+                config.runOrder = RunTests::InLexicographicalOrder;
+            else if (startsWith("random", order))
+                config.runOrder = RunTests::InRandomOrder;
+            else
+                return clara::ParserResult::runtimeError("Unrecognised ordering: '" + order + "'");
+            return ParserResult::ok(ParseResultType::Matched);
+        };
+        auto const setRngSeed = [&](std::string const& seed) {
+            if (seed != "time")
+                return clara::detail::convertInto(seed, config.rngSeed);
+            config.rngSeed = static_cast<unsigned int>(std::time(nullptr));
+            return ParserResult::ok(ParseResultType::Matched);
+        };
+        auto const setColourUsage = [&](std::string const& useColour) {
+            auto mode = toLower(useColour);
+
+            if (mode == "yes")
+                config.useColour = UseColour::Yes;
+            else if (mode == "no")
+                config.useColour = UseColour::No;
+            else if (mode == "auto")
+                config.useColour = UseColour::Auto;
+            else
+                return ParserResult::runtimeError("colour mode must be one of: auto, yes or no. '" +
+                                                  useColour + "' not recognised");
+            return ParserResult::ok(ParseResultType::Matched);
+        };
+        auto const setWaitForKeypress = [&](std::string const& keypress) {
+            auto keypressLc = toLower(keypress);
+            if (keypressLc == "never")
+                config.waitForKeypress = WaitForKeypress::Never;
+            else if (keypressLc == "start")
+                config.waitForKeypress = WaitForKeypress::BeforeStart;
+            else if (keypressLc == "exit")
+                config.waitForKeypress = WaitForKeypress::BeforeExit;
+            else if (keypressLc == "both")
+                config.waitForKeypress = WaitForKeypress::BeforeStartAndExit;
+            else
+                return ParserResult::runtimeError(
+                    "keypress argument must be one of: never, start, exit or "
+                    "both. '" +
+                    keypress + "' not recognised");
+            return ParserResult::ok(ParseResultType::Matched);
+        };
+        auto const setVerbosity = [&](std::string const& verbosity) {
+            auto lcVerbosity = toLower(verbosity);
+            if (lcVerbosity == "quiet")
+                config.verbosity = Verbosity::Quiet;
+            else if (lcVerbosity == "normal")
+                config.verbosity = Verbosity::Normal;
+            else if (lcVerbosity == "high")
+                config.verbosity = Verbosity::High;
+            else
+                return ParserResult::runtimeError("Unrecognised verbosity, '" + verbosity + "'");
+            return ParserResult::ok(ParseResultType::Matched);
+        };
+        auto const setReporter = [&](std::string const& reporter) {
+            IReporterRegistry::FactoryMap const& factories =
+                getRegistryHub().getReporterRegistry().getFactories();
+
+            auto lcReporter = toLower(reporter);
+            auto result = factories.find(lcReporter);
+
+            if (factories.end() != result)
+                config.reporterName = lcReporter;
+            else
+                return ParserResult::runtimeError("Unrecognized reporter, '" + reporter +
+                                                  "'. Check available with --list-reporters");
+            return ParserResult::ok(ParseResultType::Matched);
+        };
+
+        auto cli =
+            ExeName(config.processName) | Help(config.showHelp) |
+            Opt(config.listTests)["-l"]["--list-tests"]("list all/matching test cases") |
+            Opt(config.listTags)["-t"]["--list-tags"]("list all/matching tags") |
+            Opt(config.showSuccessfulTests)["-s"]["--success"](
+                "include successful tests in output") |
+            Opt(config.shouldDebugBreak)["-b"]["--break"]("break into debugger on failure") |
+            Opt(config.noThrow)["-e"]["--nothrow"]("skip exception tests") |
+            Opt(config.showInvisibles)["-i"]["--invisibles"]("show invisibles (tabs, newlines)") |
+            Opt(config.outputFilename, "filename")["-o"]["--out"]("output filename") |
+            Opt(setReporter, "name")["-r"]["--reporter"]("reporter to use (defaults to console)") |
+            Opt(config.name, "name")["-n"]["--name"]("suite name") |
+            Opt([&](bool) { config.abortAfter = 1; })["-a"]["--abort"]("abort at first failure") |
+            Opt([&](int x) { config.abortAfter = x; },
+                "no. failures")["-x"]["--abortx"]("abort after x failures") |
+            Opt(setWarning, "warning name")["-w"]["--warn"]("enable warnings") |
+            Opt(
+                [&](bool flag) {
+                    config.showDurations = flag ? ShowDurations::Always : ShowDurations::Never;
+                },
+                "yes|no")["-d"]["--durations"]("show test durations") |
+            Opt(config.minDuration, "seconds")["-D"]["--min-duration"](
+                "show test durations for tests taking at least the given "
+                "number of seconds") |
+            Opt(loadTestNamesFromFile,
+                "filename")["-f"]["--input-file"]("load test names to run from a file") |
+            Opt(config.filenamesAsTags)["-#"]["--filenames-as-tags"](
+                "adds a tag for the filename") |
+            Opt(config.sectionsToRun, "section name")["-c"]["--section"]("specify section to run") |
+            Opt(setVerbosity, "quiet|normal|high")["-v"]["--verbosity"]("set output verbosity") |
+            Opt(config.listTestNamesOnly)["--list-test-names-only"](
+                "list all/matching test cases names only") |
+            Opt(config.listReporters)["--list-reporters"]("list all reporters") |
+            Opt(setTestOrder, "decl|lex|rand")["--order"]("test case order (defaults to decl)") |
+            Opt(setRngSeed,
+                "'time'|number")["--rng-seed"]("set a specific seed for random numbers") |
+            Opt(setColourUsage, "yes|no")["--use-colour"]("should output be colourised") |
+            Opt(config.libIdentify)["--libidentify"](
+                "report name and version according to libidentify standard") |
+            Opt(setWaitForKeypress, "never|start|exit|both")["--wait-for-keypress"](
+                "waits for a keypress before exiting") |
+            Opt(config.benchmarkSamples,
+                "samples")["--benchmark-samples"]("number of samples to collect (default: 100)") |
+            Opt(config.benchmarkResamples, "resamples")["--benchmark-resamples"](
+                "number of resamples for the bootstrap (default: 100000)") |
+            Opt(config.benchmarkConfidenceInterval,
+                "confidence interval")["--benchmark-confidence-interval"](
+                "confidence interval for the bootstrap (between 0 and 1, "
+                "default: 0.95)") |
+            Opt(config.benchmarkNoAnalysis)["--benchmark-no-analysis"](
+                "perform only measurements; do not perform any analysis") |
+            Opt(config.benchmarkWarmupTime, "benchmarkWarmupTime")["--benchmark-warmup-time"](
+                "amount of time in milliseconds spent on warming up each test "
+                "(default: 100)") |
+            Arg(config.testsOrTags, "test name|pattern|tags")("which test or tests to use");
+
+        return cli;
+    }
+
+} // end namespace Catch
+// end catch_commandline.cpp
+// start catch_common.cpp
+
+#include <cstring>
+#include <ostream>
+
+namespace Catch {
+
+    bool SourceLineInfo::operator==(SourceLineInfo const& other) const noexcept {
+        return line == other.line && (file == other.file || std::strcmp(file, other.file) == 0);
+    }
+    bool SourceLineInfo::operator<(SourceLineInfo const& other) const noexcept {
+        // We can assume that the same file will usually have the same pointer.
+        // Thus, if the pointers are the same, there is no point in calling the
+        // strcmp
+        return line < other.line ||
+               (line == other.line && file != other.file && (std::strcmp(file, other.file) < 0));
+    }
+
+    std::ostream& operator<<(std::ostream& os, SourceLineInfo const& info) {
+#ifndef __GNUG__
+        os << info.file << '(' << info.line << ')';
+#else
+        os << info.file << ':' << info.line;
+#endif
+        return os;
+    }
+
+    std::string StreamEndStop::operator+() const { return std::string(); }
+
+    NonCopyable::NonCopyable() = default;
+    NonCopyable::~NonCopyable() = default;
+
+} // namespace Catch
+// end catch_common.cpp
+// start catch_config.cpp
+
+namespace Catch {
+
+    Config::Config(ConfigData const& data) : m_data(data), m_stream(openStream()) {
+        // We need to trim filter specs to avoid trouble with superfluous
+        // whitespace (esp. important for bdd macros, as those are manually
+        // aligned with whitespace).
+
+        for (auto& elem : m_data.testsOrTags) {
+            elem = trim(elem);
+        }
+        for (auto& elem : m_data.sectionsToRun) {
+            elem = trim(elem);
+        }
+
+        TestSpecParser parser(ITagAliasRegistry::get());
+        if (!m_data.testsOrTags.empty()) {
+            m_hasTestFilters = true;
+            for (auto const& testOrTags : m_data.testsOrTags) {
+                parser.parse(testOrTags);
+            }
+        }
+        m_testSpec = parser.testSpec();
+    }
+
+    std::string const& Config::getFilename() const { return m_data.outputFilename; }
+
+    bool Config::listTests() const { return m_data.listTests; }
+    bool Config::listTestNamesOnly() const { return m_data.listTestNamesOnly; }
+    bool Config::listTags() const { return m_data.listTags; }
+    bool Config::listReporters() const { return m_data.listReporters; }
+
+    std::string Config::getProcessName() const { return m_data.processName; }
+    std::string const& Config::getReporterName() const { return m_data.reporterName; }
+
+    std::vector<std::string> const& Config::getTestsOrTags() const { return m_data.testsOrTags; }
+    std::vector<std::string> const& Config::getSectionsToRun() const {
+        return m_data.sectionsToRun;
+    }
+
+    TestSpec const& Config::testSpec() const { return m_testSpec; }
+    bool Config::hasTestFilters() const { return m_hasTestFilters; }
+
+    bool Config::showHelp() const { return m_data.showHelp; }
+
+    // IConfig interface
+    bool Config::allowThrows() const { return !m_data.noThrow; }
+    std::ostream& Config::stream() const { return m_stream->stream(); }
+    std::string Config::name() const {
+        return m_data.name.empty() ? m_data.processName : m_data.name;
+    }
+    bool Config::includeSuccessfulResults() const { return m_data.showSuccessfulTests; }
+    bool Config::warnAboutMissingAssertions() const {
+        return !!(m_data.warnings & WarnAbout::NoAssertions);
+    }
+    bool Config::warnAboutNoTests() const { return !!(m_data.warnings & WarnAbout::NoTests); }
+    ShowDurations::OrNot Config::showDurations() const { return m_data.showDurations; }
+    double Config::minDuration() const { return m_data.minDuration; }
+    RunTests::InWhatOrder Config::runOrder() const { return m_data.runOrder; }
+    unsigned int Config::rngSeed() const { return m_data.rngSeed; }
+    UseColour::YesOrNo Config::useColour() const { return m_data.useColour; }
+    bool Config::shouldDebugBreak() const { return m_data.shouldDebugBreak; }
+    int Config::abortAfter() const { return m_data.abortAfter; }
+    bool Config::showInvisibles() const { return m_data.showInvisibles; }
+    Verbosity Config::verbosity() const { return m_data.verbosity; }
+
+    bool Config::benchmarkNoAnalysis() const { return m_data.benchmarkNoAnalysis; }
+    int Config::benchmarkSamples() const { return m_data.benchmarkSamples; }
+    double Config::benchmarkConfidenceInterval() const {
+        return m_data.benchmarkConfidenceInterval;
+    }
+    unsigned int Config::benchmarkResamples() const { return m_data.benchmarkResamples; }
+    std::chrono::milliseconds Config::benchmarkWarmupTime() const {
+        return std::chrono::milliseconds(m_data.benchmarkWarmupTime);
+    }
+
+    IStream const* Config::openStream() { return Catch::makeStream(m_data.outputFilename); }
+
+} // end namespace Catch
+// end catch_config.cpp
+// start catch_console_colour.cpp
+
+#if defined(__clang__)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wexit-time-destructors"
+#endif
+
+// start catch_errno_guard.h
+
+namespace Catch {
+
+    class ErrnoGuard {
+    public:
+        ErrnoGuard();
+        ~ErrnoGuard();
+
+    private:
+        int m_oldErrno;
+    };
+
+} // namespace Catch
+
+// end catch_errno_guard.h
+// start catch_windows_h_proxy.h
+
+#if defined(CATCH_PLATFORM_WINDOWS)
+
+#if !defined(NOMINMAX) && !defined(CATCH_CONFIG_NO_NOMINMAX)
+#define CATCH_DEFINED_NOMINMAX
+#define NOMINMAX
+#endif
+#if !defined(WIN32_LEAN_AND_MEAN) && !defined(CATCH_CONFIG_NO_WIN32_LEAN_AND_MEAN)
+#define CATCH_DEFINED_WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
+
+#ifdef __AFXDLL
+#include <AfxWin.h>
+#else
+#include <windows.h>
+#endif
+
+#ifdef CATCH_DEFINED_NOMINMAX
+#undef NOMINMAX
+#endif
+#ifdef CATCH_DEFINED_WIN32_LEAN_AND_MEAN
+#undef WIN32_LEAN_AND_MEAN
+#endif
+
+#endif // defined(CATCH_PLATFORM_WINDOWS)
+
+// end catch_windows_h_proxy.h
+#include <sstream>
+
+namespace Catch {
+    namespace {
+
+        struct IColourImpl {
+            virtual ~IColourImpl() = default;
+            virtual void use(Colour::Code _colourCode) = 0;
+        };
+
+        struct NoColourImpl : IColourImpl {
+            void use(Colour::Code) override {}
+
+            static IColourImpl* instance() {
+                static NoColourImpl s_instance;
+                return &s_instance;
+            }
+        };
+
+    } // namespace
+} // namespace Catch
+
+#if !defined(CATCH_CONFIG_COLOUR_NONE) && !defined(CATCH_CONFIG_COLOUR_WINDOWS) &&                 \
+    !defined(CATCH_CONFIG_COLOUR_ANSI)
+#ifdef CATCH_PLATFORM_WINDOWS
+#define CATCH_CONFIG_COLOUR_WINDOWS
+#else
+#define CATCH_CONFIG_COLOUR_ANSI
+#endif
+#endif
+
+#if defined(CATCH_CONFIG_COLOUR_WINDOWS) /////////////////////////////////////////
+
+namespace Catch {
+    namespace {
+
+        class Win32ColourImpl : public IColourImpl {
+        public:
+            Win32ColourImpl() : stdoutHandle(GetStdHandle(STD_OUTPUT_HANDLE)) {
+                CONSOLE_SCREEN_BUFFER_INFO csbiInfo;
+                GetConsoleScreenBufferInfo(stdoutHandle, &csbiInfo);
+                originalForegroundAttributes =
+                    csbiInfo.wAttributes &
+                    ~(BACKGROUND_GREEN | BACKGROUND_RED | BACKGROUND_BLUE | BACKGROUND_INTENSITY);
+                originalBackgroundAttributes =
+                    csbiInfo.wAttributes &
+                    ~(FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_INTENSITY);
+            }
+
+            void use(Colour::Code _colourCode) override {
+                switch (_colourCode) {
+                    case Colour::None:
+                        return setTextAttribute(originalForegroundAttributes);
+                    case Colour::White:
+                        return setTextAttribute(FOREGROUND_GREEN | FOREGROUND_RED |
+                                                FOREGROUND_BLUE);
+                    case Colour::Red:
+                        return setTextAttribute(FOREGROUND_RED);
+                    case Colour::Green:
+                        return setTextAttribute(FOREGROUND_GREEN);
+                    case Colour::Blue:
+                        return setTextAttribute(FOREGROUND_BLUE);
+                    case Colour::Cyan:
+                        return setTextAttribute(FOREGROUND_BLUE | FOREGROUND_GREEN);
+                    case Colour::Yellow:
+                        return setTextAttribute(FOREGROUND_RED | FOREGROUND_GREEN);
+                    case Colour::Grey:
+                        return setTextAttribute(0);
+
+                    case Colour::LightGrey:
+                        return setTextAttribute(FOREGROUND_INTENSITY);
+                    case Colour::BrightRed:
+                        return setTextAttribute(FOREGROUND_INTENSITY | FOREGROUND_RED);
+                    case Colour::BrightGreen:
+                        return setTextAttribute(FOREGROUND_INTENSITY | FOREGROUND_GREEN);
+                    case Colour::BrightWhite:
+                        return setTextAttribute(FOREGROUND_INTENSITY | FOREGROUND_GREEN |
+                                                FOREGROUND_RED | FOREGROUND_BLUE);
+                    case Colour::BrightYellow:
+                        return setTextAttribute(FOREGROUND_INTENSITY | FOREGROUND_RED |
+                                                FOREGROUND_GREEN);
+
+                    case Colour::Bright:
+                        CATCH_INTERNAL_ERROR("not a colour");
+
+                    default:
+                        CATCH_ERROR("Unknown colour requested");
+                }
+            }
+
+        private:
+            void setTextAttribute(WORD _textAttribute) {
+                SetConsoleTextAttribute(stdoutHandle,
+                                        _textAttribute | originalBackgroundAttributes);
+            }
+            HANDLE stdoutHandle;
+            WORD originalForegroundAttributes;
+            WORD originalBackgroundAttributes;
+        };
+
+        IColourImpl* platformColourInstance() {
+            static Win32ColourImpl s_instance;
+
+            IConfigPtr config = getCurrentContext().getConfig();
+            UseColour::YesOrNo colourMode = config ? config->useColour() : UseColour::Auto;
+            if (colourMode == UseColour::Auto)
+                colourMode = UseColour::Yes;
+            return colourMode == UseColour::Yes ? &s_instance : NoColourImpl::instance();
+        }
+
+    } // namespace
+} // end namespace Catch
+
+#elif defined(CATCH_CONFIG_COLOUR_ANSI) //////////////////////////////////////
+
+#include <unistd.h>
+
+namespace Catch {
+    namespace {
+
+        // use POSIX/ ANSI console terminal codes
+        // Thanks to Adam Strzelecki for original contribution
+        // (http://github.com/nanoant)
+        // https://github.com/philsquared/Catch/pull/131
+        class PosixColourImpl : public IColourImpl {
+        public:
+            void use(Colour::Code _colourCode) override {
+                switch (_colourCode) {
+                    case Colour::None:
+                    case Colour::White:
+                        return setColour("[0m");
+                    case Colour::Red:
+                        return setColour("[0;31m");
+                    case Colour::Green:
+                        return setColour("[0;32m");
+                    case Colour::Blue:
+                        return setColour("[0;34m");
+                    case Colour::Cyan:
+                        return setColour("[0;36m");
+                    case Colour::Yellow:
+                        return setColour("[0;33m");
+                    case Colour::Grey:
+                        return setColour("[1;30m");
+
+                    case Colour::LightGrey:
+                        return setColour("[0;37m");
+                    case Colour::BrightRed:
+                        return setColour("[1;31m");
+                    case Colour::BrightGreen:
+                        return setColour("[1;32m");
+                    case Colour::BrightWhite:
+                        return setColour("[1;37m");
+                    case Colour::BrightYellow:
+                        return setColour("[1;33m");
+
+                    case Colour::Bright:
+                        CATCH_INTERNAL_ERROR("not a colour");
+                    default:
+                        CATCH_INTERNAL_ERROR("Unknown colour requested");
+                }
+            }
+            static IColourImpl* instance() {
+                static PosixColourImpl s_instance;
+                return &s_instance;
+            }
+
+        private:
+            void setColour(const char* _escapeCode) {
+                getCurrentContext().getConfig()->stream() << '\033' << _escapeCode;
+            }
+        };
+
+        bool useColourOnPlatform() {
+            return
+#if defined(CATCH_PLATFORM_MAC) || defined(CATCH_PLATFORM_IPHONE)
+                !isDebuggerActive() &&
+#endif
+#if !(defined(__DJGPP__) && defined(__STRICT_ANSI__))
+                isatty(STDOUT_FILENO)
+#else
+                false
+#endif
+                    ;
+        }
+        IColourImpl* platformColourInstance() {
+            ErrnoGuard guard;
+            IConfigPtr config = getCurrentContext().getConfig();
+            UseColour::YesOrNo colourMode = config ? config->useColour() : UseColour::Auto;
+            if (colourMode == UseColour::Auto)
+                colourMode = useColourOnPlatform() ? UseColour::Yes : UseColour::No;
+            return colourMode == UseColour::Yes ? PosixColourImpl::instance()
+                                                : NoColourImpl::instance();
+        }
+
+    } // namespace
+} // end namespace Catch
+
+#else // not Windows or ANSI ///////////////////////////////////////////////
+
+namespace Catch {
+
+    static IColourImpl* platformColourInstance() { return NoColourImpl::instance(); }
+
+} // end namespace Catch
+
+#endif // Windows/ ANSI/ None
+
+namespace Catch {
+
+    Colour::Colour(Code _colourCode) { use(_colourCode); }
+    Colour::Colour(Colour&& other) noexcept {
+        m_moved = other.m_moved;
+        other.m_moved = true;
+    }
+    Colour& Colour::operator=(Colour&& other) noexcept {
+        m_moved = other.m_moved;
+        other.m_moved = true;
+        return *this;
+    }
+
+    Colour::~Colour() {
+        if (!m_moved)
+            use(None);
+    }
+
+    void Colour::use(Code _colourCode) {
+        static IColourImpl* impl = platformColourInstance();
+        // Strictly speaking, this cannot possibly happen.
+        // However, under some conditions it does happen (see #1626),
+        // and this change is small enough that we can let practicality
+        // triumph over purity in this case.
+        if (impl != nullptr) {
+            impl->use(_colourCode);
+        }
+    }
+
+    std::ostream& operator<<(std::ostream& os, Colour const&) { return os; }
+
+} // end namespace Catch
+
+#if defined(__clang__)
+#pragma clang diagnostic pop
+#endif
+
+// end catch_console_colour.cpp
+// start catch_context.cpp
+
+namespace Catch {
+
+    class Context : public IMutableContext, NonCopyable {
+
+    public: // IContext
+        IResultCapture* getResultCapture() override { return m_resultCapture; }
+        IRunner* getRunner() override { return m_runner; }
+
+        IConfigPtr const& getConfig() const override { return m_config; }
+
+        ~Context() override;
+
+    public: // IMutableContext
+        void setResultCapture(IResultCapture* resultCapture) override {
+            m_resultCapture = resultCapture;
+        }
+        void setRunner(IRunner* runner) override { m_runner = runner; }
+        void setConfig(IConfigPtr const& config) override { m_config = config; }
+
+        friend IMutableContext& getCurrentMutableContext();
+
+    private:
+        IConfigPtr m_config;
+        IRunner* m_runner = nullptr;
+        IResultCapture* m_resultCapture = nullptr;
+    };
+
+    IMutableContext* IMutableContext::currentContext = nullptr;
+
+    void IMutableContext::createContext() { currentContext = new Context(); }
+
+    void cleanUpContext() {
+        delete IMutableContext::currentContext;
+        IMutableContext::currentContext = nullptr;
+    }
+    IContext::~IContext() = default;
+    IMutableContext::~IMutableContext() = default;
+    Context::~Context() = default;
+
+    SimplePcg32& rng() {
+        static SimplePcg32 s_rng;
+        return s_rng;
+    }
+
+} // namespace Catch
+// end catch_context.cpp
+// start catch_debug_console.cpp
+
+// start catch_debug_console.h
+
+#include <string>
+
+namespace Catch {
+    void writeToDebugConsole(std::string const& text);
+}
+
+// end catch_debug_console.h
+#if defined(CATCH_CONFIG_ANDROID_LOGWRITE)
+#include <android/log.h>
+
+namespace Catch {
+    void writeToDebugConsole(std::string const& text) {
+        __android_log_write(ANDROID_LOG_DEBUG, "Catch", text.c_str());
+    }
+} // namespace Catch
+
+#elif defined(CATCH_PLATFORM_WINDOWS)
+
+namespace Catch {
+    void writeToDebugConsole(std::string const& text) { ::OutputDebugStringA(text.c_str()); }
+} // namespace Catch
+
+#else
+
+namespace Catch {
+    void writeToDebugConsole(std::string const& text) {
+        // !TBD: Need a version for Mac/ XCode and other IDEs
+        Catch::cout() << text;
+    }
+} // namespace Catch
+
+#endif // Platform
+// end catch_debug_console.cpp
+// start catch_debugger.cpp
+
+#if defined(CATCH_PLATFORM_MAC) || defined(CATCH_PLATFORM_IPHONE)
+
+#include <cassert>
+#include <sys/types.h>
+#include <unistd.h>
+#include <cstddef>
+#include <ostream>
+
+#ifdef __apple_build_version__
+// These headers will only compile with AppleClang (XCode)
+// For other compilers (Clang, GCC, ... ) we need to exclude them
+#include <sys/sysctl.h>
+#endif
+
+namespace Catch {
+#ifdef __apple_build_version__
+    // The following function is taken directly from the following technical
+    // note: https://developer.apple.com/library/archive/qa/qa1361/_index.html
+
+    // Returns true if the current process is being debugged (either
+    // running under the debugger or has a debugger attached post facto).
+    bool isDebuggerActive() {
+        int mib[4];
+        struct kinfo_proc info;
+        std::size_t size;
+
+        // Initialize the flags so that, if sysctl fails for some bizarre
+        // reason, we get a predictable result.
+
+        info.kp_proc.p_flag = 0;
+
+        // Initialize mib, which tells sysctl the info we want, in this case
+        // we're looking for information about a specific process ID.
+
+        mib[0] = CTL_KERN;
+        mib[1] = KERN_PROC;
+        mib[2] = KERN_PROC_PID;
+        mib[3] = getpid();
+
+        // Call sysctl.
+
+        size = sizeof(info);
+        if (sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, nullptr, 0) != 0) {
+            Catch::cerr() << "\n** Call to sysctl failed - unable to determine "
+                             "if debugger is active **\n"
+                          << std::endl;
+            return false;
+        }
+
+        // We're being debugged if the P_TRACED flag is set.
+
+        return ((info.kp_proc.p_flag & P_TRACED) != 0);
+    }
+#else
+    bool isDebuggerActive() {
+        // We need to find another way to determine this for non-appleclang
+        // compilers on macOS
+        return false;
+    }
+#endif
+} // namespace Catch
+
+#elif defined(CATCH_PLATFORM_LINUX)
+#include <fstream>
+#include <string>
+
+namespace Catch {
+    // The standard POSIX way of detecting a debugger is to attempt to
+    // ptrace() the process, but this needs to be done from a child and not
+    // this process itself to still allow attaching to this process later
+    // if wanted, so is rather heavy. Under Linux we have the PID of the
+    // "debugger" (which doesn't need to be gdb, of course, it could also
+    // be strace, for example) in /proc/$PID/status, so just get it from
+    // there instead.
+    bool isDebuggerActive() {
+        // Libstdc++ has a bug, where std::ifstream sets errno to 0
+        // This way our users can properly assert over errno values
+        ErrnoGuard guard;
+        std::ifstream in("/proc/self/status");
+        for (std::string line; std::getline(in, line);) {
+            static const int PREFIX_LEN = 11;
+            if (line.compare(0, PREFIX_LEN, "TracerPid:\t") == 0) {
+                // We're traced if the PID is not 0 and no other PID starts
+                // with 0 digit, so it's enough to check for just a single
+                // character.
+                return line.length() > PREFIX_LEN && line[PREFIX_LEN] != '0';
+            }
+        }
+
+        return false;
+    }
+} // namespace Catch
+#elif defined(_MSC_VER)
+extern "C" __declspec(dllimport) int __stdcall IsDebuggerPresent();
+namespace Catch {
+    bool isDebuggerActive() { return IsDebuggerPresent() != 0; }
+} // namespace Catch
+#elif defined(__MINGW32__)
+extern "C" __declspec(dllimport) int __stdcall IsDebuggerPresent();
+namespace Catch {
+    bool isDebuggerActive() { return IsDebuggerPresent() != 0; }
+} // namespace Catch
+#else
+namespace Catch {
+    bool isDebuggerActive() { return false; }
+} // namespace Catch
+#endif // Platform
+// end catch_debugger.cpp
+// start catch_decomposer.cpp
+
+namespace Catch {
+
+    ITransientExpression::~ITransientExpression() = default;
+
+    void formatReconstructedExpression(std::ostream& os,
+                                       std::string const& lhs,
+                                       StringRef op,
+                                       std::string const& rhs) {
+        if (lhs.size() + rhs.size() < 40 && lhs.find('\n') == std::string::npos &&
+            rhs.find('\n') == std::string::npos)
+            os << lhs << " " << op << " " << rhs;
+        else
+            os << lhs << "\n" << op << "\n" << rhs;
+    }
+} // namespace Catch
+// end catch_decomposer.cpp
+// start catch_enforce.cpp
+
+#include <stdexcept>
+
+namespace Catch {
+#if defined(CATCH_CONFIG_DISABLE_EXCEPTIONS) &&                                                    \
+    !defined(CATCH_CONFIG_DISABLE_EXCEPTIONS_CUSTOM_HANDLER)
+    [[noreturn]] void throw_exception(std::exception const& e) {
+        Catch::cerr() << "Catch will terminate because it needed to throw an exception.\n"
+                      << "The message was: " << e.what() << '\n';
+        std::terminate();
+    }
+#endif
+
+    [[noreturn]] void throw_logic_error(std::string const& msg) {
+        throw_exception(std::logic_error(msg));
+    }
+
+    [[noreturn]] void throw_domain_error(std::string const& msg) {
+        throw_exception(std::domain_error(msg));
+    }
+
+    [[noreturn]] void throw_runtime_error(std::string const& msg) {
+        throw_exception(std::runtime_error(msg));
+    }
+
+} // namespace Catch
+// end catch_enforce.cpp
+// start catch_enum_values_registry.cpp
+// start catch_enum_values_registry.h
+
+#include <vector>
+#include <memory>
+
+namespace Catch {
+
+    namespace Detail {
+
+        std::unique_ptr<EnumInfo>
+        makeEnumInfo(StringRef enumName, StringRef allValueNames, std::vector<int> const& values);
+
+        class EnumValuesRegistry : public IMutableEnumValuesRegistry {
+
+            std::vector<std::unique_ptr<EnumInfo>> m_enumInfos;
+
+            EnumInfo const& registerEnum(StringRef enumName,
+                                         StringRef allEnums,
+                                         std::vector<int> const& values) override;
+        };
+
+        std::vector<StringRef> parseEnums(StringRef enums);
+
+    } // namespace Detail
+
+} // namespace Catch
+
+// end catch_enum_values_registry.h
+
+#include <map>
+#include <cassert>
+
+namespace Catch {
+
+    IMutableEnumValuesRegistry::~IMutableEnumValuesRegistry() {}
+
+    namespace Detail {
+
+        namespace {
+            // Extracts the actual name part of an enum instance
+            // In other words, it returns the Blue part of
+            // Bikeshed::Colour::Blue
+            StringRef extractInstanceName(StringRef enumInstance) {
+                // Find last occurrence of ":"
+                size_t name_start = enumInstance.size();
+                while (name_start > 0 && enumInstance[name_start - 1] != ':') {
+                    --name_start;
+                }
+                return enumInstance.substr(name_start, enumInstance.size() - name_start);
+            }
+        } // namespace
+
+        std::vector<StringRef> parseEnums(StringRef enums) {
+            auto enumValues = splitStringRef(enums, ',');
+            std::vector<StringRef> parsed;
+            parsed.reserve(enumValues.size());
+            for (auto const& enumValue : enumValues) {
+                parsed.push_back(trim(extractInstanceName(enumValue)));
+            }
+            return parsed;
+        }
+
+        EnumInfo::~EnumInfo() {}
+
+        StringRef EnumInfo::lookup(int value) const {
+            for (auto const& valueToName : m_values) {
+                if (valueToName.first == value)
+                    return valueToName.second;
+            }
+            return "{** unexpected enum value **}"_sr;
+        }
+
+        std::unique_ptr<EnumInfo>
+        makeEnumInfo(StringRef enumName, StringRef allValueNames, std::vector<int> const& values) {
+            std::unique_ptr<EnumInfo> enumInfo(new EnumInfo);
+            enumInfo->m_name = enumName;
+            enumInfo->m_values.reserve(values.size());
+
+            const auto valueNames = Catch::Detail::parseEnums(allValueNames);
+            assert(valueNames.size() == values.size());
+            std::size_t i = 0;
+            for (auto value : values)
+                enumInfo->m_values.emplace_back(value, valueNames[i++]);
+
+            return enumInfo;
+        }
+
+        EnumInfo const& EnumValuesRegistry::registerEnum(StringRef enumName,
+                                                         StringRef allValueNames,
+                                                         std::vector<int> const& values) {
+            m_enumInfos.push_back(makeEnumInfo(enumName, allValueNames, values));
+            return *m_enumInfos.back();
+        }
+
+    } // namespace Detail
+} // namespace Catch
+
+// end catch_enum_values_registry.cpp
+// start catch_errno_guard.cpp
+
+#include <cerrno>
+
+namespace Catch {
+    ErrnoGuard::ErrnoGuard() : m_oldErrno(errno) {}
+    ErrnoGuard::~ErrnoGuard() { errno = m_oldErrno; }
+} // namespace Catch
+// end catch_errno_guard.cpp
+// start catch_exception_translator_registry.cpp
+
+// start catch_exception_translator_registry.h
+
+#include <vector>
+#include <string>
+#include <memory>
+
+namespace Catch {
+
+    class ExceptionTranslatorRegistry : public IExceptionTranslatorRegistry {
+    public:
+        ~ExceptionTranslatorRegistry();
+        virtual void registerTranslator(const IExceptionTranslator* translator);
+        std::string translateActiveException() const override;
+        std::string tryTranslators() const;
+
+    private:
+        std::vector<std::unique_ptr<IExceptionTranslator const>> m_translators;
+    };
+} // namespace Catch
+
+// end catch_exception_translator_registry.h
+#ifdef __OBJC__
+#import "Foundation/Foundation.h"
+#endif
+
+namespace Catch {
+
+    ExceptionTranslatorRegistry::~ExceptionTranslatorRegistry() {}
+
+    void ExceptionTranslatorRegistry::registerTranslator(const IExceptionTranslator* translator) {
+        m_translators.push_back(std::unique_ptr<const IExceptionTranslator>(translator));
+    }
+
+#if !defined(CATCH_CONFIG_DISABLE_EXCEPTIONS)
+    std::string ExceptionTranslatorRegistry::translateActiveException() const {
+        try {
+#ifdef __OBJC__
+            // In Objective-C try objective-c exceptions first
+            @try
+            {
+                return tryTranslators();
+            } @catch (NSException* exception)
+            {
+                return Catch::Detail::stringify([exception description]);
+            }
+#else
+            // Compiling a mixed mode project with MSVC means that CLR
+            // exceptions will be caught in (...) as well. However, these
+            // do not fill-in std::current_exception and thus lead to crash
+            // when attempting rethrow.
+            // /EHa switch also causes structured exceptions to be caught
+            // here, but they fill-in current_exception properly, so
+            // at worst the output should be a little weird, instead of
+            // causing a crash.
+            if (std::current_exception() == nullptr) {
+                return "Non C++ exception. Possibly a CLR exception.";
+            }
+            return tryTranslators();
+#endif
+        } catch (TestFailureException&) {
+            std::rethrow_exception(std::current_exception());
+        } catch (std::exception& ex) {
+            return ex.what();
+        } catch (std::string& msg) {
+            return msg;
+        } catch (const char* msg) {
+            return msg;
+        } catch (...) {
+            return "Unknown exception";
+        }
+    }
+
+    std::string ExceptionTranslatorRegistry::tryTranslators() const {
+        if (m_translators.empty()) {
+            std::rethrow_exception(std::current_exception());
+        } else {
+            return m_translators[0]->translate(m_translators.begin() + 1, m_translators.end());
+        }
+    }
+
+#else // ^^ Exceptions are enabled // Exceptions are disabled vv
+    std::string ExceptionTranslatorRegistry::translateActiveException() const {
+        CATCH_INTERNAL_ERROR("Attempted to translate active exception under "
+                             "CATCH_CONFIG_DISABLE_EXCEPTIONS!");
+    }
+
+    std::string ExceptionTranslatorRegistry::tryTranslators() const {
+        CATCH_INTERNAL_ERROR("Attempted to use exception translators under "
+                             "CATCH_CONFIG_DISABLE_EXCEPTIONS!");
+    }
+#endif
+
+} // namespace Catch
+// end catch_exception_translator_registry.cpp
+// start catch_fatal_condition.cpp
+
+#include <algorithm>
+
+#if !defined(CATCH_CONFIG_WINDOWS_SEH) && !defined(CATCH_CONFIG_POSIX_SIGNALS)
+
+namespace Catch {
+
+    // If neither SEH nor signal handling is required, the handler impls
+    // do not have to do anything, and can be empty.
+    void FatalConditionHandler::engage_platform() {}
+    void FatalConditionHandler::disengage_platform() {}
+    FatalConditionHandler::FatalConditionHandler() = default;
+    FatalConditionHandler::~FatalConditionHandler() = default;
+
+} // end namespace Catch
+
+#endif // !CATCH_CONFIG_WINDOWS_SEH && !CATCH_CONFIG_POSIX_SIGNALS
+
+#if defined(CATCH_CONFIG_WINDOWS_SEH) && defined(CATCH_CONFIG_POSIX_SIGNALS)
+#error                                                                                             \
+    "Inconsistent configuration: Windows' SEH handling and POSIX signals cannot be enabled at the same time"
+#endif // CATCH_CONFIG_WINDOWS_SEH && CATCH_CONFIG_POSIX_SIGNALS
+
+#if defined(CATCH_CONFIG_WINDOWS_SEH) || defined(CATCH_CONFIG_POSIX_SIGNALS)
+
+namespace {
+    //! Signals fatal error message to the run context
+    void reportFatal(char const* const message) {
+        Catch::getCurrentContext().getResultCapture()->handleFatalErrorCondition(message);
+    }
+
+    //! Minimal size Catch2 needs for its own fatal error handling.
+    //! Picked anecdotally, so it might not be sufficient on all
+    //! platforms, and for all configurations.
+    constexpr std::size_t minStackSizeForErrors = 32 * 1024;
+} // end unnamed namespace
+
+#endif // CATCH_CONFIG_WINDOWS_SEH || CATCH_CONFIG_POSIX_SIGNALS
+
+#if defined(CATCH_CONFIG_WINDOWS_SEH)
+
+namespace Catch {
+
+    struct SignalDefs {
+        DWORD id;
+        const char* name;
+    };
+
+    // There is no 1-1 mapping between signals and windows exceptions.
+    // Windows can easily distinguish between SO and SigSegV,
+    // but SigInt, SigTerm, etc are handled differently.
+    static SignalDefs signalDefs[] = {
+        {static_cast<DWORD>(EXCEPTION_ILLEGAL_INSTRUCTION), "SIGILL - Illegal instruction signal"},
+        {static_cast<DWORD>(EXCEPTION_STACK_OVERFLOW), "SIGSEGV - Stack overflow"},
+        {static_cast<DWORD>(EXCEPTION_ACCESS_VIOLATION), "SIGSEGV - Segmentation violation signal"},
+        {static_cast<DWORD>(EXCEPTION_INT_DIVIDE_BY_ZERO), "Divide by zero error"},
+    };
+
+    static LONG CALLBACK handleVectoredException(PEXCEPTION_POINTERS ExceptionInfo) {
+        for (auto const& def : signalDefs) {
+            if (ExceptionInfo->ExceptionRecord->ExceptionCode == def.id) {
+                reportFatal(def.name);
+            }
+        }
+        // If its not an exception we care about, pass it along.
+        // This stops us from eating debugger breaks etc.
+        return EXCEPTION_CONTINUE_SEARCH;
+    }
+
+    // Since we do not support multiple instantiations, we put these
+    // into global variables and rely on cleaning them up in outlined
+    // constructors/destructors
+    static PVOID exceptionHandlerHandle = nullptr;
+
+    // For MSVC, we reserve part of the stack memory for handling
+    // memory overflow structured exception.
+    FatalConditionHandler::FatalConditionHandler() {
+        ULONG guaranteeSize = static_cast<ULONG>(minStackSizeForErrors);
+        if (!SetThreadStackGuarantee(&guaranteeSize)) {
+            // We do not want to fully error out, because needing
+            // the stack reserve should be rare enough anyway.
+            Catch::cerr() << "Failed to reserve piece of stack."
+                          << " Stack overflows will not be reported successfully.";
+        }
+    }
+
+    // We do not attempt to unset the stack guarantee, because
+    // Windows does not support lowering the stack size guarantee.
+    FatalConditionHandler::~FatalConditionHandler() = default;
+
+    void FatalConditionHandler::engage_platform() {
+        // Register as first handler in current chain
+        exceptionHandlerHandle = AddVectoredExceptionHandler(1, handleVectoredException);
+        if (!exceptionHandlerHandle) {
+            CATCH_RUNTIME_ERROR("Could not register vectored exception handler");
+        }
+    }
+
+    void FatalConditionHandler::disengage_platform() {
+        if (!RemoveVectoredExceptionHandler(exceptionHandlerHandle)) {
+            CATCH_RUNTIME_ERROR("Could not unregister vectored exception handler");
+        }
+        exceptionHandlerHandle = nullptr;
+    }
+
+} // end namespace Catch
+
+#endif // CATCH_CONFIG_WINDOWS_SEH
+
+#if defined(CATCH_CONFIG_POSIX_SIGNALS)
+
+#include <signal.h>
+
+namespace Catch {
+
+    struct SignalDefs {
+        int id;
+        const char* name;
+    };
+
+    static SignalDefs signalDefs[] = {{SIGINT, "SIGINT - Terminal interrupt signal"},
+                                      {SIGILL, "SIGILL - Illegal instruction signal"},
+                                      {SIGFPE, "SIGFPE - Floating point error signal"},
+                                      {SIGSEGV, "SIGSEGV - Segmentation violation signal"},
+                                      {SIGTERM, "SIGTERM - Termination request signal"},
+                                      {SIGABRT, "SIGABRT - Abort (abnormal termination) signal"}};
+
+// Older GCCs trigger -Wmissing-field-initializers for T foo = {}
+// which is zero initialization, but not explicit. We want to avoid
+// that.
+#if defined(__GNUC__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
+#endif
+
+    static char* altStackMem = nullptr;
+    static std::size_t altStackSize = 0;
+    static stack_t oldSigStack{};
+    static struct sigaction oldSigActions[sizeof(signalDefs) / sizeof(SignalDefs)]{};
+
+    static void restorePreviousSignalHandlers() {
+        // We set signal handlers back to the previous ones. Hopefully
+        // nobody overwrote them in the meantime, and doesn't expect
+        // their signal handlers to live past ours given that they
+        // installed them after ours..
+        for (std::size_t i = 0; i < sizeof(signalDefs) / sizeof(SignalDefs); ++i) {
+            sigaction(signalDefs[i].id, &oldSigActions[i], nullptr);
+        }
+        // Return the old stack
+        sigaltstack(&oldSigStack, nullptr);
+    }
+
+    static void handleSignal(int sig) {
+        char const* name = "<unknown signal>";
+        for (auto const& def : signalDefs) {
+            if (sig == def.id) {
+                name = def.name;
+                break;
+            }
+        }
+        // We need to restore previous signal handlers and let them do
+        // their thing, so that the users can have the debugger break
+        // when a signal is raised, and so on.
+        restorePreviousSignalHandlers();
+        reportFatal(name);
+        raise(sig);
+    }
+
+    FatalConditionHandler::FatalConditionHandler() {
+        assert(!altStackMem && "Cannot initialize POSIX signal handler when one already exists");
+        if (altStackSize == 0) {
+            altStackSize = std::max(static_cast<size_t>(SIGSTKSZ), minStackSizeForErrors);
+        }
+        altStackMem = new char[altStackSize]();
+    }
+
+    FatalConditionHandler::~FatalConditionHandler() {
+        delete[] altStackMem;
+        // We signal that another instance can be constructed by zeroing
+        // out the pointer.
+        altStackMem = nullptr;
+    }
+
+    void FatalConditionHandler::engage_platform() {
+        stack_t sigStack;
+        sigStack.ss_sp = altStackMem;
+        sigStack.ss_size = altStackSize;
+        sigStack.ss_flags = 0;
+        sigaltstack(&sigStack, &oldSigStack);
+        struct sigaction sa = {};
+
+        sa.sa_handler = handleSignal;
+        sa.sa_flags = SA_ONSTACK;
+        for (std::size_t i = 0; i < sizeof(signalDefs) / sizeof(SignalDefs); ++i) {
+            sigaction(signalDefs[i].id, &sa, &oldSigActions[i]);
+        }
+    }
+
+#if defined(__GNUC__)
+#pragma GCC diagnostic pop
+#endif
+
+    void FatalConditionHandler::disengage_platform() { restorePreviousSignalHandlers(); }
+
+} // end namespace Catch
+
+#endif // CATCH_CONFIG_POSIX_SIGNALS
+// end catch_fatal_condition.cpp
+// start catch_generators.cpp
+
+#include <limits>
+#include <set>
+
+namespace Catch {
+
+    IGeneratorTracker::~IGeneratorTracker() {}
+
+    const char* GeneratorException::what() const noexcept { return m_msg; }
+
+    namespace Generators {
+
+        GeneratorUntypedBase::~GeneratorUntypedBase() {}
+
+        auto acquireGeneratorTracker(StringRef generatorName, SourceLineInfo const& lineInfo)
+            -> IGeneratorTracker& {
+            return getResultCapture().acquireGeneratorTracker(generatorName, lineInfo);
+        }
+
+    } // namespace Generators
+} // namespace Catch
+// end catch_generators.cpp
+// start catch_interfaces_capture.cpp
+
+namespace Catch {
+    IResultCapture::~IResultCapture() = default;
+}
+// end catch_interfaces_capture.cpp
+// start catch_interfaces_config.cpp
+
+namespace Catch {
+    IConfig::~IConfig() = default;
+}
+// end catch_interfaces_config.cpp
+// start catch_interfaces_exception.cpp
+
+namespace Catch {
+    IExceptionTranslator::~IExceptionTranslator() = default;
+    IExceptionTranslatorRegistry::~IExceptionTranslatorRegistry() = default;
+} // namespace Catch
+// end catch_interfaces_exception.cpp
+// start catch_interfaces_registry_hub.cpp
+
+namespace Catch {
+    IRegistryHub::~IRegistryHub() = default;
+    IMutableRegistryHub::~IMutableRegistryHub() = default;
+} // namespace Catch
+// end catch_interfaces_registry_hub.cpp
+// start catch_interfaces_reporter.cpp
+
+// start catch_reporter_listening.h
+
+namespace Catch {
+
+    class ListeningReporter : public IStreamingReporter {
+        using Reporters = std::vector<IStreamingReporterPtr>;
+        Reporters m_listeners;
+        IStreamingReporterPtr m_reporter = nullptr;
+        ReporterPreferences m_preferences;
+
+    public:
+        ListeningReporter();
+
+        void addListener(IStreamingReporterPtr&& listener);
+        void addReporter(IStreamingReporterPtr&& reporter);
+
+    public: // IStreamingReporter
+        ReporterPreferences getPreferences() const override;
+
+        void noMatchingTestCases(std::string const& spec) override;
+
+        void reportInvalidArguments(std::string const& arg) override;
+
+        static std::set<Verbosity> getSupportedVerbosities();
+
+#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING)
+        void benchmarkPreparing(std::string const& name) override;
+        void benchmarkStarting(BenchmarkInfo const& benchmarkInfo) override;
+        void benchmarkEnded(BenchmarkStats<> const& benchmarkStats) override;
+        void benchmarkFailed(std::string const&) override;
+#endif // CATCH_CONFIG_ENABLE_BENCHMARKING
+
+        void testRunStarting(TestRunInfo const& testRunInfo) override;
+        void testGroupStarting(GroupInfo const& groupInfo) override;
+        void testCaseStarting(TestCaseInfo const& testInfo) override;
+        void sectionStarting(SectionInfo const& sectionInfo) override;
+        void assertionStarting(AssertionInfo const& assertionInfo) override;
+
+        // The return value indicates if the messages buffer should be cleared:
+        bool assertionEnded(AssertionStats const& assertionStats) override;
+        void sectionEnded(SectionStats const& sectionStats) override;
+        void testCaseEnded(TestCaseStats const& testCaseStats) override;
+        void testGroupEnded(TestGroupStats const& testGroupStats) override;
+        void testRunEnded(TestRunStats const& testRunStats) override;
+
+        void skipTest(TestCaseInfo const& testInfo) override;
+        bool isMulti() const override;
+    };
+
+} // end namespace Catch
+
+// end catch_reporter_listening.h
+namespace Catch {
+
+    ReporterConfig::ReporterConfig(IConfigPtr const& _fullConfig) :
+        m_stream(&_fullConfig->stream()), m_fullConfig(_fullConfig) {}
+
+    ReporterConfig::ReporterConfig(IConfigPtr const& _fullConfig, std::ostream& _stream) :
+        m_stream(&_stream), m_fullConfig(_fullConfig) {}
+
+    std::ostream& ReporterConfig::stream() const { return *m_stream; }
+    IConfigPtr ReporterConfig::fullConfig() const { return m_fullConfig; }
+
+    TestRunInfo::TestRunInfo(std::string const& _name) : name(_name) {}
+
+    GroupInfo::GroupInfo(std::string const& _name,
+                         std::size_t _groupIndex,
+                         std::size_t _groupsCount) :
+        name(_name), groupIndex(_groupIndex), groupsCounts(_groupsCount) {}
+
+    AssertionStats::AssertionStats(AssertionResult const& _assertionResult,
+                                   std::vector<MessageInfo> const& _infoMessages,
+                                   Totals const& _totals) :
+        assertionResult(_assertionResult), infoMessages(_infoMessages), totals(_totals) {
+        assertionResult.m_resultData.lazyExpression.m_transientExpression =
+            _assertionResult.m_resultData.lazyExpression.m_transientExpression;
+
+        if (assertionResult.hasMessage()) {
+            // Copy message into messages list.
+            // !TBD This should have been done earlier, somewhere
+            MessageBuilder builder(assertionResult.getTestMacroName(),
+                                   assertionResult.getSourceInfo(),
+                                   assertionResult.getResultType());
+            builder << assertionResult.getMessage();
+            builder.m_info.message = builder.m_stream.str();
+
+            infoMessages.push_back(builder.m_info);
+        }
+    }
+
+    AssertionStats::~AssertionStats() = default;
+
+    SectionStats::SectionStats(SectionInfo const& _sectionInfo,
+                               Counts const& _assertions,
+                               double _durationInSeconds,
+                               bool _missingAssertions) :
+        sectionInfo(_sectionInfo),
+        assertions(_assertions),
+        durationInSeconds(_durationInSeconds),
+        missingAssertions(_missingAssertions) {}
+
+    SectionStats::~SectionStats() = default;
+
+    TestCaseStats::TestCaseStats(TestCaseInfo const& _testInfo,
+                                 Totals const& _totals,
+                                 std::string const& _stdOut,
+                                 std::string const& _stdErr,
+                                 bool _aborting) :
+        testInfo(_testInfo),
+        totals(_totals),
+        stdOut(_stdOut),
+        stdErr(_stdErr),
+        aborting(_aborting) {}
+
+    TestCaseStats::~TestCaseStats() = default;
+
+    TestGroupStats::TestGroupStats(GroupInfo const& _groupInfo,
+                                   Totals const& _totals,
+                                   bool _aborting) :
+        groupInfo(_groupInfo), totals(_totals), aborting(_aborting) {}
+
+    TestGroupStats::TestGroupStats(GroupInfo const& _groupInfo) :
+        groupInfo(_groupInfo), aborting(false) {}
+
+    TestGroupStats::~TestGroupStats() = default;
+
+    TestRunStats::TestRunStats(TestRunInfo const& _runInfo, Totals const& _totals, bool _aborting) :
+        runInfo(_runInfo), totals(_totals), aborting(_aborting) {}
+
+    TestRunStats::~TestRunStats() = default;
+
+    void IStreamingReporter::fatalErrorEncountered(StringRef) {}
+    bool IStreamingReporter::isMulti() const { return false; }
+
+    IReporterFactory::~IReporterFactory() = default;
+    IReporterRegistry::~IReporterRegistry() = default;
+
+} // end namespace Catch
+// end catch_interfaces_reporter.cpp
+// start catch_interfaces_runner.cpp
+
+namespace Catch {
+    IRunner::~IRunner() = default;
+}
+// end catch_interfaces_runner.cpp
+// start catch_interfaces_testcase.cpp
+
+namespace Catch {
+    ITestInvoker::~ITestInvoker() = default;
+    ITestCaseRegistry::~ITestCaseRegistry() = default;
+} // namespace Catch
+// end catch_interfaces_testcase.cpp
+// start catch_leak_detector.cpp
+
+#ifdef CATCH_CONFIG_WINDOWS_CRTDBG
+#include <crtdbg.h>
+
+namespace Catch {
+
+    LeakDetector::LeakDetector() {
+        int flag = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG);
+        flag |= _CRTDBG_LEAK_CHECK_DF;
+        flag |= _CRTDBG_ALLOC_MEM_DF;
+        _CrtSetDbgFlag(flag);
+        _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE | _CRTDBG_MODE_DEBUG);
+        _CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR);
+        // Change this to leaking allocation's number to break there
+        _CrtSetBreakAlloc(-1);
+    }
+} // namespace Catch
+
+#else
+
+Catch::LeakDetector::LeakDetector() {}
+
+#endif
+
+Catch::LeakDetector::~LeakDetector() { Catch::cleanUp(); }
+// end catch_leak_detector.cpp
+// start catch_list.cpp
+
+// start catch_list.h
+
+#include <set>
+
+namespace Catch {
+
+    std::size_t listTests(Config const& config);
+
+    std::size_t listTestsNamesOnly(Config const& config);
+
+    struct TagInfo {
+        void add(std::string const& spelling);
+        std::string all() const;
+
+        std::set<std::string> spellings;
+        std::size_t count = 0;
+    };
+
+    std::size_t listTags(Config const& config);
+
+    std::size_t listReporters();
+
+    Option<std::size_t> list(std::shared_ptr<Config> const& config);
+
+} // end namespace Catch
+
+// end catch_list.h
+// start catch_text.h
+
+namespace Catch {
+    using namespace clara::TextFlow;
+}
+
+// end catch_text.h
+#include <limits>
+#include <algorithm>
+#include <iomanip>
+
+namespace Catch {
+
+    std::size_t listTests(Config const& config) {
+        TestSpec const& testSpec = config.testSpec();
+        if (config.hasTestFilters())
+            Catch::cout() << "Matching test cases:\n";
+        else {
+            Catch::cout() << "All available test cases:\n";
+        }
+
+        auto matchedTestCases = filterTests(getAllTestCasesSorted(config), testSpec, config);
+        for (auto const& testCaseInfo : matchedTestCases) {
+            Colour::Code colour = testCaseInfo.isHidden() ? Colour::SecondaryText : Colour::None;
+            Colour colourGuard(colour);
+
+            Catch::cout() << Column(testCaseInfo.name).initialIndent(2).indent(4) << "\n";
+            if (config.verbosity() >= Verbosity::High) {
+                Catch::cout() << Column(Catch::Detail::stringify(testCaseInfo.lineInfo)).indent(4)
+                              << std::endl;
+                std::string description = testCaseInfo.description;
+                if (description.empty())
+                    description = "(NO DESCRIPTION)";
+                Catch::cout() << Column(description).indent(4) << std::endl;
+            }
+            if (!testCaseInfo.tags.empty())
+                Catch::cout() << Column(testCaseInfo.tagsAsString()).indent(6) << "\n";
+        }
+
+        if (!config.hasTestFilters())
+            Catch::cout() << pluralise(matchedTestCases.size(), "test case") << '\n' << std::endl;
+        else
+            Catch::cout() << pluralise(matchedTestCases.size(), "matching test case") << '\n'
+                          << std::endl;
+        return matchedTestCases.size();
+    }
+
+    std::size_t listTestsNamesOnly(Config const& config) {
+        TestSpec const& testSpec = config.testSpec();
+        std::size_t matchedTests = 0;
+        std::vector<TestCase> matchedTestCases =
+            filterTests(getAllTestCasesSorted(config), testSpec, config);
+        for (auto const& testCaseInfo : matchedTestCases) {
+            matchedTests++;
+            if (startsWith(testCaseInfo.name, '#'))
+                Catch::cout() << '"' << testCaseInfo.name << '"';
+            else
+                Catch::cout() << testCaseInfo.name;
+            if (config.verbosity() >= Verbosity::High)
+                Catch::cout() << "\t@" << testCaseInfo.lineInfo;
+            Catch::cout() << std::endl;
+        }
+        return matchedTests;
+    }
+
+    void TagInfo::add(std::string const& spelling) {
+        ++count;
+        spellings.insert(spelling);
+    }
+
+    std::string TagInfo::all() const {
+        size_t size = 0;
+        for (auto const& spelling : spellings) {
+            // Add 2 for the brackes
+            size += spelling.size() + 2;
+        }
+
+        std::string out;
+        out.reserve(size);
+        for (auto const& spelling : spellings) {
+            out += '[';
+            out += spelling;
+            out += ']';
+        }
+        return out;
+    }
+
+    std::size_t listTags(Config const& config) {
+        TestSpec const& testSpec = config.testSpec();
+        if (config.hasTestFilters())
+            Catch::cout() << "Tags for matching test cases:\n";
+        else {
+            Catch::cout() << "All available tags:\n";
+        }
+
+        std::map<std::string, TagInfo> tagCounts;
+
+        std::vector<TestCase> matchedTestCases =
+            filterTests(getAllTestCasesSorted(config), testSpec, config);
+        for (auto const& testCase : matchedTestCases) {
+            for (auto const& tagName : testCase.getTestCaseInfo().tags) {
+                std::string lcaseTagName = toLower(tagName);
+                auto countIt = tagCounts.find(lcaseTagName);
+                if (countIt == tagCounts.end())
+                    countIt = tagCounts.insert(std::make_pair(lcaseTagName, TagInfo())).first;
+                countIt->second.add(tagName);
+            }
+        }
+
+        for (auto const& tagCount : tagCounts) {
+            ReusableStringStream rss;
+            rss << "  " << std::setw(2) << tagCount.second.count << "  ";
+            auto str = rss.str();
+            auto wrapper = Column(tagCount.second.all())
+                               .initialIndent(0)
+                               .indent(str.size())
+                               .width(CATCH_CONFIG_CONSOLE_WIDTH - 10);
+            Catch::cout() << str << wrapper << '\n';
+        }
+        Catch::cout() << pluralise(tagCounts.size(), "tag") << '\n' << std::endl;
+        return tagCounts.size();
+    }
+
+    std::size_t listReporters() {
+        Catch::cout() << "Available reporters:\n";
+        IReporterRegistry::FactoryMap const& factories =
+            getRegistryHub().getReporterRegistry().getFactories();
+        std::size_t maxNameLen = 0;
+        for (auto const& factoryKvp : factories)
+            maxNameLen = (std::max)(maxNameLen, factoryKvp.first.size());
+
+        for (auto const& factoryKvp : factories) {
+            Catch::cout() << Column(factoryKvp.first + ":").indent(2).width(5 + maxNameLen) +
+                                 Column(factoryKvp.second->getDescription())
+                                     .initialIndent(0)
+                                     .indent(2)
+                                     .width(CATCH_CONFIG_CONSOLE_WIDTH - maxNameLen - 8)
+                          << "\n";
+        }
+        Catch::cout() << std::endl;
+        return factories.size();
+    }
+
+    Option<std::size_t> list(std::shared_ptr<Config> const& config) {
+        Option<std::size_t> listedCount;
+        getCurrentMutableContext().setConfig(config);
+        if (config->listTests())
+            listedCount = listedCount.valueOr(0) + listTests(*config);
+        if (config->listTestNamesOnly())
+            listedCount = listedCount.valueOr(0) + listTestsNamesOnly(*config);
+        if (config->listTags())
+            listedCount = listedCount.valueOr(0) + listTags(*config);
+        if (config->listReporters())
+            listedCount = listedCount.valueOr(0) + listReporters();
+        return listedCount;
+    }
+
+} // end namespace Catch
+// end catch_list.cpp
+// start catch_matchers.cpp
+
+namespace Catch {
+    namespace Matchers {
+        namespace Impl {
+
+            std::string MatcherUntypedBase::toString() const {
+                if (m_cachedToString.empty())
+                    m_cachedToString = describe();
+                return m_cachedToString;
+            }
+
+            MatcherUntypedBase::~MatcherUntypedBase() = default;
+
+        } // namespace Impl
+    }     // namespace Matchers
+
+    using namespace Matchers;
+    using Matchers::Impl::MatcherBase;
+
+} // namespace Catch
+// end catch_matchers.cpp
+// start catch_matchers_exception.cpp
+
+namespace Catch {
+    namespace Matchers {
+        namespace Exception {
+
+            bool ExceptionMessageMatcher::match(std::exception const& ex) const {
+                return ex.what() == m_message;
+            }
+
+            std::string ExceptionMessageMatcher::describe() const {
+                return "exception message matches \"" + m_message + "\"";
+            }
+
+        } // namespace Exception
+        Exception::ExceptionMessageMatcher Message(std::string const& message) {
+            return Exception::ExceptionMessageMatcher(message);
+        }
+
+        // namespace Exception
+    } // namespace Matchers
+} // namespace Catch
+// end catch_matchers_exception.cpp
+// start catch_matchers_floating.cpp
+
+// start catch_polyfills.hpp
+
+namespace Catch {
+    bool isnan(float f);
+    bool isnan(double d);
+} // namespace Catch
+
+// end catch_polyfills.hpp
+// start catch_to_string.hpp
+
+#include <string>
+
+namespace Catch {
+    template <typename T> std::string to_string(T const& t) {
+#if defined(CATCH_CONFIG_CPP11_TO_STRING)
+        return std::to_string(t);
+#else
+        ReusableStringStream rss;
+        rss << t;
+        return rss.str();
+#endif
+    }
+} // end namespace Catch
+
+// end catch_to_string.hpp
+#include <algorithm>
+#include <cmath>
+#include <cstdlib>
+#include <cstdint>
+#include <cstring>
+#include <sstream>
+#include <type_traits>
+#include <iomanip>
+#include <limits>
+
+namespace Catch {
+    namespace {
+
+        int32_t convert(float f) {
+            static_assert(sizeof(float) == sizeof(int32_t),
+                          "Important ULP matcher assumption violated");
+            int32_t i;
+            std::memcpy(&i, &f, sizeof(f));
+            return i;
+        }
+
+        int64_t convert(double d) {
+            static_assert(sizeof(double) == sizeof(int64_t),
+                          "Important ULP matcher assumption violated");
+            int64_t i;
+            std::memcpy(&i, &d, sizeof(d));
+            return i;
+        }
+
+        template <typename FP> bool almostEqualUlps(FP lhs, FP rhs, uint64_t maxUlpDiff) {
+            // Comparison with NaN should always be false.
+            // This way we can rule it out before getting into the ugly details
+            if (Catch::isnan(lhs) || Catch::isnan(rhs)) {
+                return false;
+            }
+
+            auto lc = convert(lhs);
+            auto rc = convert(rhs);
+
+            if ((lc < 0) != (rc < 0)) {
+                // Potentially we can have +0 and -0
+                return lhs == rhs;
+            }
+
+            // static cast as a workaround for IBM XLC
+            auto ulpDiff = std::abs(static_cast<FP>(lc - rc));
+            return static_cast<uint64_t>(ulpDiff) <= maxUlpDiff;
+        }
+
+#if defined(CATCH_CONFIG_GLOBAL_NEXTAFTER)
+
+        float nextafter(float x, float y) { return ::nextafterf(x, y); }
+
+        double nextafter(double x, double y) { return ::nextafter(x, y); }
+
+#endif // ^^^ CATCH_CONFIG_GLOBAL_NEXTAFTER ^^^
+
+        template <typename FP> FP step(FP start, FP direction, uint64_t steps) {
+            for (uint64_t i = 0; i < steps; ++i) {
+#if defined(CATCH_CONFIG_GLOBAL_NEXTAFTER)
+                start = Catch::nextafter(start, direction);
+#else
+                start = std::nextafter(start, direction);
+#endif
+            }
+            return start;
+        }
+
+        // Performs equivalent check of std::fabs(lhs - rhs) <= margin
+        // But without the subtraction to allow for INFINITY in comparison
+        bool marginComparison(double lhs, double rhs, double margin) {
+            return (lhs + margin >= rhs) && (rhs + margin >= lhs);
+        }
+
+        template <typename FloatingPoint> void write(std::ostream& out, FloatingPoint num) {
+            out << std::scientific
+                << std::setprecision(std::numeric_limits<FloatingPoint>::max_digits10 - 1) << num;
+        }
+
+    } // end anonymous namespace
+
+    namespace Matchers {
+        namespace Floating {
+
+            enum class FloatingPointKind : uint8_t { Float, Double };
+
+            WithinAbsMatcher::WithinAbsMatcher(double target, double margin) :
+                m_target{target}, m_margin{margin} {
+                CATCH_ENFORCE(margin >= 0,
+                              "Invalid margin: " << margin << '.'
+                                                 << " Margin has to be non-negative.");
+            }
+
+            // Performs equivalent check of std::fabs(lhs - rhs) <= margin
+            // But without the subtraction to allow for INFINITY in comparison
+            bool WithinAbsMatcher::match(double const& matchee) const {
+                return (matchee + m_margin >= m_target) && (m_target + m_margin >= matchee);
+            }
+
+            std::string WithinAbsMatcher::describe() const {
+                return "is within " + ::Catch::Detail::stringify(m_margin) + " of " +
+                       ::Catch::Detail::stringify(m_target);
+            }
+
+            WithinUlpsMatcher::WithinUlpsMatcher(double target,
+                                                 uint64_t ulps,
+                                                 FloatingPointKind baseType) :
+                m_target{target}, m_ulps{ulps}, m_type{baseType} {
+                CATCH_ENFORCE(m_type == FloatingPointKind::Double ||
+                                  m_ulps < (std::numeric_limits<uint32_t>::max)(),
+                              "Provided ULP is impossibly large for a float comparison.");
+            }
+
+#if defined(__clang__)
+#pragma clang diagnostic push
+// Clang <3.5 reports on the default branch in the switch below
+#pragma clang diagnostic ignored "-Wunreachable-code"
+#endif
+
+            bool WithinUlpsMatcher::match(double const& matchee) const {
+                switch (m_type) {
+                    case FloatingPointKind::Float:
+                        return almostEqualUlps<float>(
+                            static_cast<float>(matchee), static_cast<float>(m_target), m_ulps);
+                    case FloatingPointKind::Double:
+                        return almostEqualUlps<double>(matchee, m_target, m_ulps);
+                    default:
+                        CATCH_INTERNAL_ERROR("Unknown FloatingPointKind value");
+                }
+            }
+
+#if defined(__clang__)
+#pragma clang diagnostic pop
+#endif
+
+            std::string WithinUlpsMatcher::describe() const {
+                std::stringstream ret;
+
+                ret << "is within " << m_ulps << " ULPs of ";
+
+                if (m_type == FloatingPointKind::Float) {
+                    write(ret, static_cast<float>(m_target));
+                    ret << 'f';
+                } else {
+                    write(ret, m_target);
+                }
+
+                ret << " ([";
+                if (m_type == FloatingPointKind::Double) {
+                    write(ret, step(m_target, static_cast<double>(-INFINITY), m_ulps));
+                    ret << ", ";
+                    write(ret, step(m_target, static_cast<double>(INFINITY), m_ulps));
+                } else {
+                    // We have to cast INFINITY to float because of MinGW, see
+                    // #1782
+                    write(
+                        ret,
+                        step(static_cast<float>(m_target), static_cast<float>(-INFINITY), m_ulps));
+                    ret << ", ";
+                    write(ret,
+                          step(static_cast<float>(m_target), static_cast<float>(INFINITY), m_ulps));
+                }
+                ret << "])";
+
+                return ret.str();
+            }
+
+            WithinRelMatcher::WithinRelMatcher(double target, double epsilon) :
+                m_target(target), m_epsilon(epsilon) {
+                CATCH_ENFORCE(m_epsilon >= 0.,
+                              "Relative comparison with epsilon <  0 does not "
+                              "make sense.");
+                CATCH_ENFORCE(m_epsilon < 1.,
+                              "Relative comparison with epsilon >= 1 does not "
+                              "make sense.");
+            }
+
+            bool WithinRelMatcher::match(double const& matchee) const {
+                const auto relMargin =
+                    m_epsilon * (std::max)(std::fabs(matchee), std::fabs(m_target));
+                return marginComparison(matchee, m_target, std::isinf(relMargin) ? 0 : relMargin);
+            }
+
+            std::string WithinRelMatcher::describe() const {
+                Catch::ReusableStringStream sstr;
+                sstr << "and " << m_target << " are within " << m_epsilon * 100.
+                     << "% of each other";
+                return sstr.str();
+            }
+
+        } // namespace Floating
+
+        Floating::WithinUlpsMatcher WithinULP(double target, uint64_t maxUlpDiff) {
+            return Floating::WithinUlpsMatcher(
+                target, maxUlpDiff, Floating::FloatingPointKind::Double);
+        }
+
+        Floating::WithinUlpsMatcher WithinULP(float target, uint64_t maxUlpDiff) {
+            return Floating::WithinUlpsMatcher(
+                target, maxUlpDiff, Floating::FloatingPointKind::Float);
+        }
+
+        Floating::WithinAbsMatcher WithinAbs(double target, double margin) {
+            return Floating::WithinAbsMatcher(target, margin);
+        }
+
+        Floating::WithinRelMatcher WithinRel(double target, double eps) {
+            return Floating::WithinRelMatcher(target, eps);
+        }
+
+        Floating::WithinRelMatcher WithinRel(double target) {
+            return Floating::WithinRelMatcher(target, std::numeric_limits<double>::epsilon() * 100);
+        }
+
+        Floating::WithinRelMatcher WithinRel(float target, float eps) {
+            return Floating::WithinRelMatcher(target, eps);
+        }
+
+        Floating::WithinRelMatcher WithinRel(float target) {
+            return Floating::WithinRelMatcher(target, std::numeric_limits<float>::epsilon() * 100);
+        }
+
+    } // namespace Matchers
+} // namespace Catch
+// end catch_matchers_floating.cpp
+// start catch_matchers_generic.cpp
+
+std::string Catch::Matchers::Generic::Detail::finalizeDescription(const std::string& desc) {
+    if (desc.empty()) {
+        return "matches undescribed predicate";
+    } else {
+        return "matches predicate: \"" + desc + '"';
+    }
+}
+// end catch_matchers_generic.cpp
+// start catch_matchers_string.cpp
+
+#include <regex>
+
+namespace Catch {
+    namespace Matchers {
+
+        namespace StdString {
+
+            CasedString::CasedString(std::string const& str,
+                                     CaseSensitive::Choice caseSensitivity) :
+                m_caseSensitivity(caseSensitivity), m_str(adjustString(str)) {}
+            std::string CasedString::adjustString(std::string const& str) const {
+                return m_caseSensitivity == CaseSensitive::No ? toLower(str) : str;
+            }
+            std::string CasedString::caseSensitivitySuffix() const {
+                return m_caseSensitivity == CaseSensitive::No ? " (case insensitive)"
+                                                              : std::string();
+            }
+
+            StringMatcherBase::StringMatcherBase(std::string const& operation,
+                                                 CasedString const& comparator) :
+                m_comparator(comparator), m_operation(operation) {}
+
+            std::string StringMatcherBase::describe() const {
+                std::string description;
+                description.reserve(5 + m_operation.size() + m_comparator.m_str.size() +
+                                    m_comparator.caseSensitivitySuffix().size());
+                description += m_operation;
+                description += ": \"";
+                description += m_comparator.m_str;
+                description += "\"";
+                description += m_comparator.caseSensitivitySuffix();
+                return description;
+            }
+
+            EqualsMatcher::EqualsMatcher(CasedString const& comparator) :
+                StringMatcherBase("equals", comparator) {}
+
+            bool EqualsMatcher::match(std::string const& source) const {
+                return m_comparator.adjustString(source) == m_comparator.m_str;
+            }
+
+            ContainsMatcher::ContainsMatcher(CasedString const& comparator) :
+                StringMatcherBase("contains", comparator) {}
+
+            bool ContainsMatcher::match(std::string const& source) const {
+                return contains(m_comparator.adjustString(source), m_comparator.m_str);
+            }
+
+            StartsWithMatcher::StartsWithMatcher(CasedString const& comparator) :
+                StringMatcherBase("starts with", comparator) {}
+
+            bool StartsWithMatcher::match(std::string const& source) const {
+                return startsWith(m_comparator.adjustString(source), m_comparator.m_str);
+            }
+
+            EndsWithMatcher::EndsWithMatcher(CasedString const& comparator) :
+                StringMatcherBase("ends with", comparator) {}
+
+            bool EndsWithMatcher::match(std::string const& source) const {
+                return endsWith(m_comparator.adjustString(source), m_comparator.m_str);
+            }
+
+            RegexMatcher::RegexMatcher(std::string regex, CaseSensitive::Choice caseSensitivity) :
+                m_regex(std::move(regex)), m_caseSensitivity(caseSensitivity) {}
+
+            bool RegexMatcher::match(std::string const& matchee) const {
+                auto flags = std::regex::ECMAScript; // ECMAScript is the default syntax
+                                                     // option anyway
+                if (m_caseSensitivity == CaseSensitive::Choice::No) {
+                    flags |= std::regex::icase;
+                }
+                auto reg = std::regex(m_regex, flags);
+                return std::regex_match(matchee, reg);
+            }
+
+            std::string RegexMatcher::describe() const {
+                return "matches " + ::Catch::Detail::stringify(m_regex) +
+                       ((m_caseSensitivity == CaseSensitive::Choice::Yes) ? " case sensitively"
+                                                                          : " case insensitively");
+            }
+
+        } // namespace StdString
+
+        StdString::EqualsMatcher Equals(std::string const& str,
+                                        CaseSensitive::Choice caseSensitivity) {
+            return StdString::EqualsMatcher(StdString::CasedString(str, caseSensitivity));
+        }
+        StdString::ContainsMatcher Contains(std::string const& str,
+                                            CaseSensitive::Choice caseSensitivity) {
+            return StdString::ContainsMatcher(StdString::CasedString(str, caseSensitivity));
+        }
+        StdString::EndsWithMatcher EndsWith(std::string const& str,
+                                            CaseSensitive::Choice caseSensitivity) {
+            return StdString::EndsWithMatcher(StdString::CasedString(str, caseSensitivity));
+        }
+        StdString::StartsWithMatcher StartsWith(std::string const& str,
+                                                CaseSensitive::Choice caseSensitivity) {
+            return StdString::StartsWithMatcher(StdString::CasedString(str, caseSensitivity));
+        }
+
+        StdString::RegexMatcher Matches(std::string const& regex,
+                                        CaseSensitive::Choice caseSensitivity) {
+            return StdString::RegexMatcher(regex, caseSensitivity);
+        }
+
+    } // namespace Matchers
+} // namespace Catch
+// end catch_matchers_string.cpp
+// start catch_message.cpp
+
+// start catch_uncaught_exceptions.h
+
+namespace Catch {
+    bool uncaught_exceptions();
+} // end namespace Catch
+
+// end catch_uncaught_exceptions.h
+#include <cassert>
+#include <stack>
+
+namespace Catch {
+
+    MessageInfo::MessageInfo(StringRef const& _macroName,
+                             SourceLineInfo const& _lineInfo,
+                             ResultWas::OfType _type) :
+        macroName(_macroName), lineInfo(_lineInfo), type(_type), sequence(++globalCount) {}
+
+    bool MessageInfo::operator==(MessageInfo const& other) const {
+        return sequence == other.sequence;
+    }
+
+    bool MessageInfo::operator<(MessageInfo const& other) const {
+        return sequence < other.sequence;
+    }
+
+    // This may need protecting if threading support is added
+    unsigned int MessageInfo::globalCount = 0;
+
+    ////////////////////////////////////////////////////////////////////////////
+
+    Catch::MessageBuilder::MessageBuilder(StringRef const& macroName,
+                                          SourceLineInfo const& lineInfo,
+                                          ResultWas::OfType type) :
+        m_info(macroName, lineInfo, type) {}
+
+    ////////////////////////////////////////////////////////////////////////////
+
+    ScopedMessage::ScopedMessage(MessageBuilder const& builder) :
+        m_info(builder.m_info), m_moved() {
+        m_info.message = builder.m_stream.str();
+        getResultCapture().pushScopedMessage(m_info);
+    }
+
+    ScopedMessage::ScopedMessage(ScopedMessage&& old) : m_info(old.m_info), m_moved() {
+        old.m_moved = true;
+    }
+
+    ScopedMessage::~ScopedMessage() {
+        if (!uncaught_exceptions() && !m_moved) {
+            getResultCapture().popScopedMessage(m_info);
+        }
+    }
+
+    Capturer::Capturer(StringRef macroName,
+                       SourceLineInfo const& lineInfo,
+                       ResultWas::OfType resultType,
+                       StringRef names) {
+        auto trimmed = [&](size_t start, size_t end) {
+            while (names[start] == ',' || isspace(static_cast<unsigned char>(names[start]))) {
+                ++start;
+            }
+            while (names[end] == ',' || isspace(static_cast<unsigned char>(names[end]))) {
+                --end;
+            }
+            return names.substr(start, end - start + 1);
+        };
+        auto skipq = [&](size_t start, char quote) {
+            for (auto i = start + 1; i < names.size(); ++i) {
+                if (names[i] == quote)
+                    return i;
+                if (names[i] == '\\')
+                    ++i;
+            }
+            CATCH_INTERNAL_ERROR("CAPTURE parsing encountered unmatched quote");
+        };
+
+        size_t start = 0;
+        std::stack<char> openings;
+        for (size_t pos = 0; pos < names.size(); ++pos) {
+            char c = names[pos];
+            switch (c) {
+                case '[':
+                case '{':
+                case '(':
+                    // It is basically impossible to disambiguate between
+                    // comparison and start of template args in this context
+                    //            case '<':
+                    openings.push(c);
+                    break;
+                case ']':
+                case '}':
+                case ')':
+                    //           case '>':
+                    openings.pop();
+                    break;
+                case '"':
+                case '\'':
+                    pos = skipq(pos, c);
+                    break;
+                case ',':
+                    if (start != pos && openings.empty()) {
+                        m_messages.emplace_back(macroName, lineInfo, resultType);
+                        m_messages.back().message = static_cast<std::string>(trimmed(start, pos));
+                        m_messages.back().message += " := ";
+                        start = pos;
+                    }
+            }
+        }
+        assert(openings.empty() && "Mismatched openings");
+        m_messages.emplace_back(macroName, lineInfo, resultType);
+        m_messages.back().message = static_cast<std::string>(trimmed(start, names.size() - 1));
+        m_messages.back().message += " := ";
+    }
+    Capturer::~Capturer() {
+        if (!uncaught_exceptions()) {
+            assert(m_captured == m_messages.size());
+            for (size_t i = 0; i < m_captured; ++i)
+                m_resultCapture.popScopedMessage(m_messages[i]);
+        }
+    }
+
+    void Capturer::captureValue(size_t index, std::string const& value) {
+        assert(index < m_messages.size());
+        m_messages[index].message += value;
+        m_resultCapture.pushScopedMessage(m_messages[index]);
+        m_captured++;
+    }
+
+} // end namespace Catch
+// end catch_message.cpp
+// start catch_output_redirect.cpp
+
+// start catch_output_redirect.h
+#ifndef TWOBLUECUBES_CATCH_OUTPUT_REDIRECT_H
+#define TWOBLUECUBES_CATCH_OUTPUT_REDIRECT_H
+
+#include <cstdio>
+#include <iosfwd>
+#include <string>
+
+namespace Catch {
+
+    class RedirectedStream {
+        std::ostream& m_originalStream;
+        std::ostream& m_redirectionStream;
+        std::streambuf* m_prevBuf;
+
+    public:
+        RedirectedStream(std::ostream& originalStream, std::ostream& redirectionStream);
+        ~RedirectedStream();
+    };
+
+    class RedirectedStdOut {
+        ReusableStringStream m_rss;
+        RedirectedStream m_cout;
+
+    public:
+        RedirectedStdOut();
+        auto str() const -> std::string;
+    };
+
+    // StdErr has two constituent streams in C++, std::cerr and std::clog
+    // This means that we need to redirect 2 streams into 1 to keep proper
+    // order of writes
+    class RedirectedStdErr {
+        ReusableStringStream m_rss;
+        RedirectedStream m_cerr;
+        RedirectedStream m_clog;
+
+    public:
+        RedirectedStdErr();
+        auto str() const -> std::string;
+    };
+
+    class RedirectedStreams {
+    public:
+        RedirectedStreams(RedirectedStreams const&) = delete;
+        RedirectedStreams& operator=(RedirectedStreams const&) = delete;
+        RedirectedStreams(RedirectedStreams&&) = delete;
+        RedirectedStreams& operator=(RedirectedStreams&&) = delete;
+
+        RedirectedStreams(std::string& redirectedCout, std::string& redirectedCerr);
+        ~RedirectedStreams();
+
+    private:
+        std::string& m_redirectedCout;
+        std::string& m_redirectedCerr;
+        RedirectedStdOut m_redirectedStdOut;
+        RedirectedStdErr m_redirectedStdErr;
+    };
+
+#if defined(CATCH_CONFIG_NEW_CAPTURE)
+
+    // Windows's implementation of std::tmpfile is terrible (it tries
+    // to create a file inside system folder, thus requiring elevated
+    // privileges for the binary), so we have to use tmpnam(_s) and
+    // create the file ourselves there.
+    class TempFile {
+    public:
+        TempFile(TempFile const&) = delete;
+        TempFile& operator=(TempFile const&) = delete;
+        TempFile(TempFile&&) = delete;
+        TempFile& operator=(TempFile&&) = delete;
+
+        TempFile();
+        ~TempFile();
+
+        std::FILE* getFile();
+        std::string getContents();
+
+    private:
+        std::FILE* m_file = nullptr;
+#if defined(_MSC_VER)
+        char m_buffer[L_tmpnam] = {0};
+#endif
+    };
+
+    class OutputRedirect {
+    public:
+        OutputRedirect(OutputRedirect const&) = delete;
+        OutputRedirect& operator=(OutputRedirect const&) = delete;
+        OutputRedirect(OutputRedirect&&) = delete;
+        OutputRedirect& operator=(OutputRedirect&&) = delete;
+
+        OutputRedirect(std::string& stdout_dest, std::string& stderr_dest);
+        ~OutputRedirect();
+
+    private:
+        int m_originalStdout = -1;
+        int m_originalStderr = -1;
+        TempFile m_stdoutFile;
+        TempFile m_stderrFile;
+        std::string& m_stdoutDest;
+        std::string& m_stderrDest;
+    };
+
+#endif
+
+} // end namespace Catch
+
+#endif // TWOBLUECUBES_CATCH_OUTPUT_REDIRECT_H
+// end catch_output_redirect.h
+#include <cstdio>
+#include <cstring>
+#include <fstream>
+#include <sstream>
+#include <stdexcept>
+
+#if defined(CATCH_CONFIG_NEW_CAPTURE)
+#if defined(_MSC_VER)
+#include <io.h> //_dup and _dup2
+#define dup _dup
+#define dup2 _dup2
+#define fileno _fileno
+#else
+#include <unistd.h> // dup and dup2
+#endif
+#endif
+
+namespace Catch {
+
+    RedirectedStream::RedirectedStream(std::ostream& originalStream,
+                                       std::ostream& redirectionStream) :
+        m_originalStream(originalStream),
+        m_redirectionStream(redirectionStream),
+        m_prevBuf(m_originalStream.rdbuf()) {
+        m_originalStream.rdbuf(m_redirectionStream.rdbuf());
+    }
+
+    RedirectedStream::~RedirectedStream() { m_originalStream.rdbuf(m_prevBuf); }
+
+    RedirectedStdOut::RedirectedStdOut() : m_cout(Catch::cout(), m_rss.get()) {}
+    auto RedirectedStdOut::str() const -> std::string { return m_rss.str(); }
+
+    RedirectedStdErr::RedirectedStdErr() :
+        m_cerr(Catch::cerr(), m_rss.get()), m_clog(Catch::clog(), m_rss.get()) {}
+    auto RedirectedStdErr::str() const -> std::string { return m_rss.str(); }
+
+    RedirectedStreams::RedirectedStreams(std::string& redirectedCout, std::string& redirectedCerr) :
+        m_redirectedCout(redirectedCout), m_redirectedCerr(redirectedCerr) {}
+
+    RedirectedStreams::~RedirectedStreams() {
+        m_redirectedCout += m_redirectedStdOut.str();
+        m_redirectedCerr += m_redirectedStdErr.str();
+    }
+
+#if defined(CATCH_CONFIG_NEW_CAPTURE)
+
+#if defined(_MSC_VER)
+    TempFile::TempFile() {
+        if (tmpnam_s(m_buffer)) {
+            CATCH_RUNTIME_ERROR("Could not get a temp filename");
+        }
+        if (fopen_s(&m_file, m_buffer, "w+")) {
+            char buffer[100];
+            if (strerror_s(buffer, errno)) {
+                CATCH_RUNTIME_ERROR("Could not translate errno to a string");
+            }
+            CATCH_RUNTIME_ERROR("Could not open the temp file: '" << m_buffer
+                                                                  << "' because: " << buffer);
+        }
+    }
+#else
+    TempFile::TempFile() {
+        m_file = std::tmpfile();
+        if (!m_file) {
+            CATCH_RUNTIME_ERROR("Could not create a temp file.");
+        }
+    }
+
+#endif
+
+    TempFile::~TempFile() {
+        // TBD: What to do about errors here?
+        std::fclose(m_file);
+        // We manually create the file on Windows only, on Linux
+        // it will be autodeleted
+#if defined(_MSC_VER)
+        std::remove(m_buffer);
+#endif
+    }
+
+    FILE* TempFile::getFile() { return m_file; }
+
+    std::string TempFile::getContents() {
+        std::stringstream sstr;
+        char buffer[100] = {};
+        std::rewind(m_file);
+        while (std::fgets(buffer, sizeof(buffer), m_file)) {
+            sstr << buffer;
+        }
+        return sstr.str();
+    }
+
+    OutputRedirect::OutputRedirect(std::string& stdout_dest, std::string& stderr_dest) :
+        m_originalStdout(dup(1)),
+        m_originalStderr(dup(2)),
+        m_stdoutDest(stdout_dest),
+        m_stderrDest(stderr_dest) {
+        dup2(fileno(m_stdoutFile.getFile()), 1);
+        dup2(fileno(m_stderrFile.getFile()), 2);
+    }
+
+    OutputRedirect::~OutputRedirect() {
+        Catch::cout() << std::flush;
+        fflush(stdout);
+        // Since we support overriding these streams, we flush cerr
+        // even though std::cerr is unbuffered
+        Catch::cerr() << std::flush;
+        Catch::clog() << std::flush;
+        fflush(stderr);
+
+        dup2(m_originalStdout, 1);
+        dup2(m_originalStderr, 2);
+
+        m_stdoutDest += m_stdoutFile.getContents();
+        m_stderrDest += m_stderrFile.getContents();
+    }
+
+#endif // CATCH_CONFIG_NEW_CAPTURE
+
+} // namespace Catch
+
+#if defined(CATCH_CONFIG_NEW_CAPTURE)
+#if defined(_MSC_VER)
+#undef dup
+#undef dup2
+#undef fileno
+#endif
+#endif
+// end catch_output_redirect.cpp
+// start catch_polyfills.cpp
+
+#include <cmath>
+
+namespace Catch {
+
+#if !defined(CATCH_CONFIG_POLYFILL_ISNAN)
+    bool isnan(float f) { return std::isnan(f); }
+    bool isnan(double d) { return std::isnan(d); }
+#else
+    // For now we only use this for embarcadero
+    bool isnan(float f) { return std::_isnan(f); }
+    bool isnan(double d) { return std::_isnan(d); }
+#endif
+
+} // end namespace Catch
+// end catch_polyfills.cpp
+// start catch_random_number_generator.cpp
+
+namespace Catch {
+
+    namespace {
+
+#if defined(_MSC_VER)
+#pragma warning(push)
+#pragma warning(disable : 4146) // we negate uint32 during the rotate
+#endif
+        // Safe rotr implementation thanks to John Regehr
+        uint32_t rotate_right(uint32_t val, uint32_t count) {
+            const uint32_t mask = 31;
+            count &= mask;
+            return (val >> count) | (val << (-count & mask));
+        }
+
+#if defined(_MSC_VER)
+#pragma warning(pop)
+#endif
+
+    } // namespace
+
+    SimplePcg32::SimplePcg32(result_type seed_) { seed(seed_); }
+
+    void SimplePcg32::seed(result_type seed_) {
+        m_state = 0;
+        (*this)();
+        m_state += seed_;
+        (*this)();
+    }
+
+    void SimplePcg32::discard(uint64_t skip) {
+        // We could implement this to run in O(log n) steps, but this
+        // should suffice for our use case.
+        for (uint64_t s = 0; s < skip; ++s) {
+            static_cast<void>((*this)());
+        }
+    }
+
+    SimplePcg32::result_type SimplePcg32::operator()() {
+        // prepare the output value
+        const uint32_t xorshifted = static_cast<uint32_t>(((m_state >> 18u) ^ m_state) >> 27u);
+        const auto output = rotate_right(xorshifted, m_state >> 59u);
+
+        // advance state
+        m_state = m_state * 6364136223846793005ULL + s_inc;
+
+        return output;
+    }
+
+    bool operator==(SimplePcg32 const& lhs, SimplePcg32 const& rhs) {
+        return lhs.m_state == rhs.m_state;
+    }
+
+    bool operator!=(SimplePcg32 const& lhs, SimplePcg32 const& rhs) {
+        return lhs.m_state != rhs.m_state;
+    }
+} // namespace Catch
+// end catch_random_number_generator.cpp
+// start catch_registry_hub.cpp
+
+// start catch_test_case_registry_impl.h
+
+#include <vector>
+#include <set>
+#include <algorithm>
+#include <ios>
+
+namespace Catch {
+
+    class TestCase;
+    struct IConfig;
+
+    std::vector<TestCase> sortTests(IConfig const& config,
+                                    std::vector<TestCase> const& unsortedTestCases);
+
+    bool isThrowSafe(TestCase const& testCase, IConfig const& config);
+    bool matchTest(TestCase const& testCase, TestSpec const& testSpec, IConfig const& config);
+
+    void enforceNoDuplicateTestCases(std::vector<TestCase> const& functions);
+
+    std::vector<TestCase> filterTests(std::vector<TestCase> const& testCases,
+                                      TestSpec const& testSpec,
+                                      IConfig const& config);
+    std::vector<TestCase> const& getAllTestCasesSorted(IConfig const& config);
+
+    class TestRegistry : public ITestCaseRegistry {
+    public:
+        virtual ~TestRegistry() = default;
+
+        virtual void registerTest(TestCase const& testCase);
+
+        std::vector<TestCase> const& getAllTests() const override;
+        std::vector<TestCase> const& getAllTestsSorted(IConfig const& config) const override;
+
+    private:
+        std::vector<TestCase> m_functions;
+        mutable RunTests::InWhatOrder m_currentSortOrder = RunTests::InDeclarationOrder;
+        mutable std::vector<TestCase> m_sortedFunctions;
+        std::size_t m_unnamedCount = 0;
+        std::ios_base::Init m_ostreamInit; // Forces cout/ cerr to be initialised
+    };
+
+    ///////////////////////////////////////////////////////////////////////////
+
+    class TestInvokerAsFunction : public ITestInvoker {
+        void (*m_testAsFunction)();
+
+    public:
+        TestInvokerAsFunction(void (*testAsFunction)()) noexcept;
+
+        void invoke() const override;
+    };
+
+    std::string extractClassName(StringRef const& classOrQualifiedMethodName);
+
+    ///////////////////////////////////////////////////////////////////////////
+
+} // end namespace Catch
+
+// end catch_test_case_registry_impl.h
+// start catch_reporter_registry.h
+
+#include <map>
+
+namespace Catch {
+
+    class ReporterRegistry : public IReporterRegistry {
+
+    public:
+        ~ReporterRegistry() override;
+
+        IStreamingReporterPtr create(std::string const& name,
+                                     IConfigPtr const& config) const override;
+
+        void registerReporter(std::string const& name, IReporterFactoryPtr const& factory);
+        void registerListener(IReporterFactoryPtr const& factory);
+
+        FactoryMap const& getFactories() const override;
+        Listeners const& getListeners() const override;
+
+    private:
+        FactoryMap m_factories;
+        Listeners m_listeners;
+    };
+} // namespace Catch
+
+// end catch_reporter_registry.h
+// start catch_tag_alias_registry.h
+
+// start catch_tag_alias.h
+
+#include <string>
+
+namespace Catch {
+
+    struct TagAlias {
+        TagAlias(std::string const& _tag, SourceLineInfo _lineInfo);
+
+        std::string tag;
+        SourceLineInfo lineInfo;
+    };
+
+} // end namespace Catch
+
+// end catch_tag_alias.h
+#include <map>
+
+namespace Catch {
+
+    class TagAliasRegistry : public ITagAliasRegistry {
+    public:
+        ~TagAliasRegistry() override;
+        TagAlias const* find(std::string const& alias) const override;
+        std::string expandAliases(std::string const& unexpandedTestSpec) const override;
+        void add(std::string const& alias, std::string const& tag, SourceLineInfo const& lineInfo);
+
+    private:
+        std::map<std::string, TagAlias> m_registry;
+    };
+
+} // end namespace Catch
+
+// end catch_tag_alias_registry.h
+// start catch_startup_exception_registry.h
+
+#include <vector>
+#include <exception>
+
+namespace Catch {
+
+    class StartupExceptionRegistry {
+#if !defined(CATCH_CONFIG_DISABLE_EXCEPTIONS)
+    public:
+        void add(std::exception_ptr const& exception) noexcept;
+        std::vector<std::exception_ptr> const& getExceptions() const noexcept;
+
+    private:
+        std::vector<std::exception_ptr> m_exceptions;
+#endif
+    };
+
+} // end namespace Catch
+
+// end catch_startup_exception_registry.h
+// start catch_singletons.hpp
+
+namespace Catch {
+
+    struct ISingleton {
+        virtual ~ISingleton();
+    };
+
+    void addSingleton(ISingleton* singleton);
+    void cleanupSingletons();
+
+    template <typename SingletonImplT,
+              typename InterfaceT = SingletonImplT,
+              typename MutableInterfaceT = InterfaceT>
+    class Singleton : SingletonImplT, public ISingleton {
+
+        static auto getInternal() -> Singleton* {
+            static Singleton* s_instance = nullptr;
+            if (!s_instance) {
+                s_instance = new Singleton;
+                addSingleton(s_instance);
+            }
+            return s_instance;
+        }
+
+    public:
+        static auto get() -> InterfaceT const& { return *getInternal(); }
+        static auto getMutable() -> MutableInterfaceT& { return *getInternal(); }
+    };
+
+} // namespace Catch
+
+// end catch_singletons.hpp
+namespace Catch {
+
+    namespace {
+
+        class RegistryHub : public IRegistryHub, public IMutableRegistryHub, private NonCopyable {
+
+        public: // IRegistryHub
+            RegistryHub() = default;
+            IReporterRegistry const& getReporterRegistry() const override {
+                return m_reporterRegistry;
+            }
+            ITestCaseRegistry const& getTestCaseRegistry() const override {
+                return m_testCaseRegistry;
+            }
+            IExceptionTranslatorRegistry const& getExceptionTranslatorRegistry() const override {
+                return m_exceptionTranslatorRegistry;
+            }
+            ITagAliasRegistry const& getTagAliasRegistry() const override {
+                return m_tagAliasRegistry;
+            }
+            StartupExceptionRegistry const& getStartupExceptionRegistry() const override {
+                return m_exceptionRegistry;
+            }
+
+        public: // IMutableRegistryHub
+            void registerReporter(std::string const& name,
+                                  IReporterFactoryPtr const& factory) override {
+                m_reporterRegistry.registerReporter(name, factory);
+            }
+            void registerListener(IReporterFactoryPtr const& factory) override {
+                m_reporterRegistry.registerListener(factory);
+            }
+            void registerTest(TestCase const& testInfo) override {
+                m_testCaseRegistry.registerTest(testInfo);
+            }
+            void registerTranslator(const IExceptionTranslator* translator) override {
+                m_exceptionTranslatorRegistry.registerTranslator(translator);
+            }
+            void registerTagAlias(std::string const& alias,
+                                  std::string const& tag,
+                                  SourceLineInfo const& lineInfo) override {
+                m_tagAliasRegistry.add(alias, tag, lineInfo);
+            }
+            void registerStartupException() noexcept override {
+#if !defined(CATCH_CONFIG_DISABLE_EXCEPTIONS)
+                m_exceptionRegistry.add(std::current_exception());
+#else
+                CATCH_INTERNAL_ERROR("Attempted to register active exception "
+                                     "under CATCH_CONFIG_DISABLE_EXCEPTIONS!");
+#endif
+            }
+            IMutableEnumValuesRegistry& getMutableEnumValuesRegistry() override {
+                return m_enumValuesRegistry;
+            }
+
+        private:
+            TestRegistry m_testCaseRegistry;
+            ReporterRegistry m_reporterRegistry;
+            ExceptionTranslatorRegistry m_exceptionTranslatorRegistry;
+            TagAliasRegistry m_tagAliasRegistry;
+            StartupExceptionRegistry m_exceptionRegistry;
+            Detail::EnumValuesRegistry m_enumValuesRegistry;
+        };
+    } // namespace
+
+    using RegistryHubSingleton = Singleton<RegistryHub, IRegistryHub, IMutableRegistryHub>;
+
+    IRegistryHub const& getRegistryHub() { return RegistryHubSingleton::get(); }
+    IMutableRegistryHub& getMutableRegistryHub() { return RegistryHubSingleton::getMutable(); }
+    void cleanUp() {
+        cleanupSingletons();
+        cleanUpContext();
+    }
+    std::string translateActiveException() {
+        return getRegistryHub().getExceptionTranslatorRegistry().translateActiveException();
+    }
+
+} // end namespace Catch
+// end catch_registry_hub.cpp
+// start catch_reporter_registry.cpp
+
+namespace Catch {
+
+    ReporterRegistry::~ReporterRegistry() = default;
+
+    IStreamingReporterPtr ReporterRegistry::create(std::string const& name,
+                                                   IConfigPtr const& config) const {
+        auto it = m_factories.find(name);
+        if (it == m_factories.end())
+            return nullptr;
+        return it->second->create(ReporterConfig(config));
+    }
+
+    void ReporterRegistry::registerReporter(std::string const& name,
+                                            IReporterFactoryPtr const& factory) {
+        m_factories.emplace(name, factory);
+    }
+    void ReporterRegistry::registerListener(IReporterFactoryPtr const& factory) {
+        m_listeners.push_back(factory);
+    }
+
+    IReporterRegistry::FactoryMap const& ReporterRegistry::getFactories() const {
+        return m_factories;
+    }
+    IReporterRegistry::Listeners const& ReporterRegistry::getListeners() const {
+        return m_listeners;
+    }
+
+} // namespace Catch
+// end catch_reporter_registry.cpp
+// start catch_result_type.cpp
+
+namespace Catch {
+
+    bool isOk(ResultWas::OfType resultType) { return (resultType & ResultWas::FailureBit) == 0; }
+    bool isJustInfo(int flags) { return flags == ResultWas::Info; }
+
+    ResultDisposition::Flags operator|(ResultDisposition::Flags lhs, ResultDisposition::Flags rhs) {
+        return static_cast<ResultDisposition::Flags>(static_cast<int>(lhs) | static_cast<int>(rhs));
+    }
+
+    bool shouldContinueOnFailure(int flags) {
+        return (flags & ResultDisposition::ContinueOnFailure) != 0;
+    }
+    bool shouldSuppressFailure(int flags) { return (flags & ResultDisposition::SuppressFail) != 0; }
+
+} // end namespace Catch
+// end catch_result_type.cpp
+// start catch_run_context.cpp
+
+#include <cassert>
+#include <algorithm>
+#include <sstream>
+
+namespace Catch {
+
+    namespace Generators {
+        struct GeneratorTracker : TestCaseTracking::TrackerBase, IGeneratorTracker {
+            GeneratorBasePtr m_generator;
+
+            GeneratorTracker(TestCaseTracking::NameAndLocation const& nameAndLocation,
+                             TrackerContext& ctx,
+                             ITracker* parent) :
+                TrackerBase(nameAndLocation, ctx, parent) {}
+            ~GeneratorTracker();
+
+            static GeneratorTracker&
+            acquire(TrackerContext& ctx, TestCaseTracking::NameAndLocation const& nameAndLocation) {
+                std::shared_ptr<GeneratorTracker> tracker;
+
+                ITracker& currentTracker = ctx.currentTracker();
+                // Under specific circumstances, the generator we want
+                // to acquire is also the current tracker. If this is
+                // the case, we have to avoid looking through current
+                // tracker's children, and instead return the current
+                // tracker.
+                // A case where this check is important is e.g.
+                //     for (int i = 0; i < 5; ++i) {
+                //         int n = GENERATE(1, 2);
+                //     }
+                //
+                // without it, the code above creates 5 nested generators.
+                if (currentTracker.nameAndLocation() == nameAndLocation) {
+                    auto thisTracker = currentTracker.parent().findChild(nameAndLocation);
+                    assert(thisTracker);
+                    assert(thisTracker->isGeneratorTracker());
+                    tracker = std::static_pointer_cast<GeneratorTracker>(thisTracker);
+                } else if (TestCaseTracking::ITrackerPtr childTracker =
+                               currentTracker.findChild(nameAndLocation)) {
+                    assert(childTracker);
+                    assert(childTracker->isGeneratorTracker());
+                    tracker = std::static_pointer_cast<GeneratorTracker>(childTracker);
+                } else {
+                    tracker =
+                        std::make_shared<GeneratorTracker>(nameAndLocation, ctx, &currentTracker);
+                    currentTracker.addChild(tracker);
+                }
+
+                if (!tracker->isComplete()) {
+                    tracker->open();
+                }
+
+                return *tracker;
+            }
+
+            // TrackerBase interface
+            bool isGeneratorTracker() const override { return true; }
+            auto hasGenerator() const -> bool override { return !!m_generator; }
+            void close() override {
+                TrackerBase::close();
+                // If a generator has a child (it is followed by a section)
+                // and none of its children have started, then we must wait
+                // until later to start consuming its values.
+                // This catches cases where `GENERATE` is placed between two
+                // `SECTION`s.
+                // **The check for m_children.empty cannot be removed**.
+                // doing so would break `GENERATE` _not_ followed by `SECTION`s.
+                const bool should_wait_for_child = [&]() {
+                    // No children -> nobody to wait for
+                    if (m_children.empty()) {
+                        return false;
+                    }
+                    // If at least one child started executing, don't wait
+                    if (std::find_if(m_children.begin(),
+                                     m_children.end(),
+                                     [](TestCaseTracking::ITrackerPtr tracker) {
+                                         return tracker->hasStarted();
+                                     }) != m_children.end())
+                    {
+                        return false;
+                    }
+
+                    // No children have started. We need to check if they _can_
+                    // start, and thus we should wait for them, or they cannot
+                    // start (due to filters), and we shouldn't wait for them
+                    auto* parent = m_parent;
+                    // This is safe: there is always at least one section
+                    // tracker in a test case tracking tree
+                    while (!parent->isSectionTracker()) {
+                        parent = &(parent->parent());
+                    }
+                    assert(parent && "Missing root (test case) level section");
+
+                    auto const& parentSection = static_cast<SectionTracker&>(*parent);
+                    auto const& filters = parentSection.getFilters();
+                    // No filters -> no restrictions on running sections
+                    if (filters.empty()) {
+                        return true;
+                    }
+
+                    for (auto const& child : m_children) {
+                        if (child->isSectionTracker() &&
+                            std::find(filters.begin(),
+                                      filters.end(),
+                                      static_cast<SectionTracker&>(*child).trimmedName()) !=
+                                filters.end())
+                        {
+                            return true;
+                        }
+                    }
+                    return false;
+                }();
+
+                // This check is a bit tricky, because m_generator->next()
+                // has a side-effect, where it consumes generator's current
+                // value, but we do not want to invoke the side-effect if
+                // this generator is still waiting for any child to start.
+                if (should_wait_for_child ||
+                    (m_runState == CompletedSuccessfully && m_generator->next())) {
+                    m_children.clear();
+                    m_runState = Executing;
+                }
+            }
+
+            // IGeneratorTracker interface
+            auto getGenerator() const -> GeneratorBasePtr const& override { return m_generator; }
+            void setGenerator(GeneratorBasePtr&& generator) override {
+                m_generator = std::move(generator);
+            }
+        };
+        GeneratorTracker::~GeneratorTracker() {}
+    } // namespace Generators
+
+    RunContext::RunContext(IConfigPtr const& _config, IStreamingReporterPtr&& reporter) :
+        m_runInfo(_config->name()),
+        m_context(getCurrentMutableContext()),
+        m_config(_config),
+        m_reporter(std::move(reporter)),
+        m_lastAssertionInfo{
+            StringRef(), SourceLineInfo("", 0), StringRef(), ResultDisposition::Normal},
+        m_includeSuccessfulResults(m_config->includeSuccessfulResults() ||
+                                   m_reporter->getPreferences().shouldReportAllAssertions) {
+        m_context.setRunner(this);
+        m_context.setConfig(m_config);
+        m_context.setResultCapture(this);
+        m_reporter->testRunStarting(m_runInfo);
+    }
+
+    RunContext::~RunContext() {
+        m_reporter->testRunEnded(TestRunStats(m_runInfo, m_totals, aborting()));
+    }
+
+    void RunContext::testGroupStarting(std::string const& testSpec,
+                                       std::size_t groupIndex,
+                                       std::size_t groupsCount) {
+        m_reporter->testGroupStarting(GroupInfo(testSpec, groupIndex, groupsCount));
+    }
+
+    void RunContext::testGroupEnded(std::string const& testSpec,
+                                    Totals const& totals,
+                                    std::size_t groupIndex,
+                                    std::size_t groupsCount) {
+        m_reporter->testGroupEnded(
+            TestGroupStats(GroupInfo(testSpec, groupIndex, groupsCount), totals, aborting()));
+    }
+
+    Totals RunContext::runTest(TestCase const& testCase) {
+        Totals prevTotals = m_totals;
+
+        std::string redirectedCout;
+        std::string redirectedCerr;
+
+        auto const& testInfo = testCase.getTestCaseInfo();
+
+        m_reporter->testCaseStarting(testInfo);
+
+        m_activeTestCase = &testCase;
+
+        ITracker& rootTracker = m_trackerContext.startRun();
+        assert(rootTracker.isSectionTracker());
+        static_cast<SectionTracker&>(rootTracker).addInitialFilters(m_config->getSectionsToRun());
+        do {
+            m_trackerContext.startCycle();
+            m_testCaseTracker = &SectionTracker::acquire(
+                m_trackerContext,
+                TestCaseTracking::NameAndLocation(testInfo.name, testInfo.lineInfo));
+            runCurrentTest(redirectedCout, redirectedCerr);
+        } while (!m_testCaseTracker->isSuccessfullyCompleted() && !aborting());
+
+        Totals deltaTotals = m_totals.delta(prevTotals);
+        if (testInfo.expectedToFail() && deltaTotals.testCases.passed > 0) {
+            deltaTotals.assertions.failed++;
+            deltaTotals.testCases.passed--;
+            deltaTotals.testCases.failed++;
+        }
+        m_totals.testCases += deltaTotals.testCases;
+        m_reporter->testCaseEnded(
+            TestCaseStats(testInfo, deltaTotals, redirectedCout, redirectedCerr, aborting()));
+
+        m_activeTestCase = nullptr;
+        m_testCaseTracker = nullptr;
+
+        return deltaTotals;
+    }
+
+    IConfigPtr RunContext::config() const { return m_config; }
+
+    IStreamingReporter& RunContext::reporter() const { return *m_reporter; }
+
+    void RunContext::assertionEnded(AssertionResult const& result) {
+        if (result.getResultType() == ResultWas::Ok) {
+            m_totals.assertions.passed++;
+            m_lastAssertionPassed = true;
+        } else if (!result.isOk()) {
+            m_lastAssertionPassed = false;
+            if (m_activeTestCase->getTestCaseInfo().okToFail())
+                m_totals.assertions.failedButOk++;
+            else
+                m_totals.assertions.failed++;
+        } else {
+            m_lastAssertionPassed = true;
+        }
+
+        // We have no use for the return value (whether messages should be
+        // cleared), because messages were made scoped and should be let to
+        // clear themselves out.
+        static_cast<void>(m_reporter->assertionEnded(AssertionStats(result, m_messages, m_totals)));
+
+        if (result.getResultType() != ResultWas::Warning)
+            m_messageScopes.clear();
+
+        // Reset working state
+        resetAssertionInfo();
+        m_lastResult = result;
+    }
+    void RunContext::resetAssertionInfo() {
+        m_lastAssertionInfo.macroName = StringRef();
+        m_lastAssertionInfo.capturedExpression = "{Unknown expression after the reported line}"_sr;
+    }
+
+    bool RunContext::sectionStarted(SectionInfo const& sectionInfo, Counts& assertions) {
+        ITracker& sectionTracker = SectionTracker::acquire(
+            m_trackerContext,
+            TestCaseTracking::NameAndLocation(sectionInfo.name, sectionInfo.lineInfo));
+        if (!sectionTracker.isOpen())
+            return false;
+        m_activeSections.push_back(&sectionTracker);
+
+        m_lastAssertionInfo.lineInfo = sectionInfo.lineInfo;
+
+        m_reporter->sectionStarting(sectionInfo);
+
+        assertions = m_totals.assertions;
+
+        return true;
+    }
+    auto RunContext::acquireGeneratorTracker(StringRef generatorName,
+                                             SourceLineInfo const& lineInfo) -> IGeneratorTracker& {
+        using namespace Generators;
+        GeneratorTracker& tracker = GeneratorTracker::acquire(
+            m_trackerContext,
+            TestCaseTracking::NameAndLocation(static_cast<std::string>(generatorName), lineInfo));
+        m_lastAssertionInfo.lineInfo = lineInfo;
+        return tracker;
+    }
+
+    bool RunContext::testForMissingAssertions(Counts& assertions) {
+        if (assertions.total() != 0)
+            return false;
+        if (!m_config->warnAboutMissingAssertions())
+            return false;
+        if (m_trackerContext.currentTracker().hasChildren())
+            return false;
+        m_totals.assertions.failed++;
+        assertions.failed++;
+        return true;
+    }
+
+    void RunContext::sectionEnded(SectionEndInfo const& endInfo) {
+        Counts assertions = m_totals.assertions - endInfo.prevAssertions;
+        bool missingAssertions = testForMissingAssertions(assertions);
+
+        if (!m_activeSections.empty()) {
+            m_activeSections.back()->close();
+            m_activeSections.pop_back();
+        }
+
+        m_reporter->sectionEnded(SectionStats(
+            endInfo.sectionInfo, assertions, endInfo.durationInSeconds, missingAssertions));
+        m_messages.clear();
+        m_messageScopes.clear();
+    }
+
+    void RunContext::sectionEndedEarly(SectionEndInfo const& endInfo) {
+        if (m_unfinishedSections.empty())
+            m_activeSections.back()->fail();
+        else
+            m_activeSections.back()->close();
+        m_activeSections.pop_back();
+
+        m_unfinishedSections.push_back(endInfo);
+    }
+
+#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING)
+    void RunContext::benchmarkPreparing(std::string const& name) {
+        m_reporter->benchmarkPreparing(name);
+    }
+    void RunContext::benchmarkStarting(BenchmarkInfo const& info) {
+        m_reporter->benchmarkStarting(info);
+    }
+    void RunContext::benchmarkEnded(BenchmarkStats<> const& stats) {
+        m_reporter->benchmarkEnded(stats);
+    }
+    void RunContext::benchmarkFailed(std::string const& error) {
+        m_reporter->benchmarkFailed(error);
+    }
+#endif // CATCH_CONFIG_ENABLE_BENCHMARKING
+
+    void RunContext::pushScopedMessage(MessageInfo const& message) {
+        m_messages.push_back(message);
+    }
+
+    void RunContext::popScopedMessage(MessageInfo const& message) {
+        m_messages.erase(std::remove(m_messages.begin(), m_messages.end(), message),
+                         m_messages.end());
+    }
+
+    void RunContext::emplaceUnscopedMessage(MessageBuilder const& builder) {
+        m_messageScopes.emplace_back(builder);
+    }
+
+    std::string RunContext::getCurrentTestName() const {
+        return m_activeTestCase ? m_activeTestCase->getTestCaseInfo().name : std::string();
+    }
+
+    const AssertionResult* RunContext::getLastResult() const { return &(*m_lastResult); }
+
+    void RunContext::exceptionEarlyReported() { m_shouldReportUnexpected = false; }
+
+    void RunContext::handleFatalErrorCondition(StringRef message) {
+        // First notify reporter that bad things happened
+        m_reporter->fatalErrorEncountered(message);
+
+        // Don't rebuild the result -- the stringification itself can cause more
+        // fatal errors Instead, fake a result data.
+        AssertionResultData tempResult(ResultWas::FatalErrorCondition, {false});
+        tempResult.message = static_cast<std::string>(message);
+        AssertionResult result(m_lastAssertionInfo, tempResult);
+
+        assertionEnded(result);
+
+        handleUnfinishedSections();
+
+        // Recreate section for test case (as we will lose the one that was in
+        // scope)
+        auto const& testCaseInfo = m_activeTestCase->getTestCaseInfo();
+        SectionInfo testCaseSection(testCaseInfo.lineInfo, testCaseInfo.name);
+
+        Counts assertions;
+        assertions.failed = 1;
+        SectionStats testCaseSectionStats(testCaseSection, assertions, 0, false);
+        m_reporter->sectionEnded(testCaseSectionStats);
+
+        auto const& testInfo = m_activeTestCase->getTestCaseInfo();
+
+        Totals deltaTotals;
+        deltaTotals.testCases.failed = 1;
+        deltaTotals.assertions.failed = 1;
+        m_reporter->testCaseEnded(
+            TestCaseStats(testInfo, deltaTotals, std::string(), std::string(), false));
+        m_totals.testCases.failed++;
+        testGroupEnded(std::string(), m_totals, 1, 1);
+        m_reporter->testRunEnded(TestRunStats(m_runInfo, m_totals, false));
+    }
+
+    bool RunContext::lastAssertionPassed() { return m_lastAssertionPassed; }
+
+    void RunContext::assertionPassed() {
+        m_lastAssertionPassed = true;
+        ++m_totals.assertions.passed;
+        resetAssertionInfo();
+        m_messageScopes.clear();
+    }
+
+    bool RunContext::aborting() const {
+        return m_totals.assertions.failed >= static_cast<std::size_t>(m_config->abortAfter());
+    }
+
+    void RunContext::runCurrentTest(std::string& redirectedCout, std::string& redirectedCerr) {
+        auto const& testCaseInfo = m_activeTestCase->getTestCaseInfo();
+        SectionInfo testCaseSection(testCaseInfo.lineInfo, testCaseInfo.name);
+        m_reporter->sectionStarting(testCaseSection);
+        Counts prevAssertions = m_totals.assertions;
+        double duration = 0;
+        m_shouldReportUnexpected = true;
+        m_lastAssertionInfo = {
+            "TEST_CASE"_sr, testCaseInfo.lineInfo, StringRef(), ResultDisposition::Normal};
+
+        seedRng(*m_config);
+
+        Timer timer;
+        CATCH_TRY {
+            if (m_reporter->getPreferences().shouldRedirectStdOut) {
+#if !defined(CATCH_CONFIG_EXPERIMENTAL_REDIRECT)
+                RedirectedStreams redirectedStreams(redirectedCout, redirectedCerr);
+
+                timer.start();
+                invokeActiveTestCase();
+#else
+                OutputRedirect r(redirectedCout, redirectedCerr);
+                timer.start();
+                invokeActiveTestCase();
+#endif
+            } else {
+                timer.start();
+                invokeActiveTestCase();
+            }
+            duration = timer.getElapsedSeconds();
+        }
+        CATCH_CATCH_ANON(TestFailureException&) {
+            // This just means the test was aborted due to failure
+        }
+        CATCH_CATCH_ALL {
+            // Under CATCH_CONFIG_FAST_COMPILE, unexpected exceptions under
+            // REQUIRE assertions are reported without translation at the point
+            // of origin.
+            if (m_shouldReportUnexpected) {
+                AssertionReaction dummyReaction;
+                handleUnexpectedInflightException(
+                    m_lastAssertionInfo, translateActiveException(), dummyReaction);
+            }
+        }
+        Counts assertions = m_totals.assertions - prevAssertions;
+        bool missingAssertions = testForMissingAssertions(assertions);
+
+        m_testCaseTracker->close();
+        handleUnfinishedSections();
+        m_messages.clear();
+        m_messageScopes.clear();
+
+        SectionStats testCaseSectionStats(testCaseSection, assertions, duration, missingAssertions);
+        m_reporter->sectionEnded(testCaseSectionStats);
+    }
+
+    void RunContext::invokeActiveTestCase() {
+        FatalConditionHandlerGuard _(&m_fatalConditionhandler);
+        m_activeTestCase->invoke();
+    }
+
+    void RunContext::handleUnfinishedSections() {
+        // If sections ended prematurely due to an exception we stored their
+        // infos here so we can tear them down outside the unwind process.
+        for (auto it = m_unfinishedSections.rbegin(), itEnd = m_unfinishedSections.rend();
+             it != itEnd;
+             ++it)
+            sectionEnded(*it);
+        m_unfinishedSections.clear();
+    }
+
+    void RunContext::handleExpr(AssertionInfo const& info,
+                                ITransientExpression const& expr,
+                                AssertionReaction& reaction) {
+        m_reporter->assertionStarting(info);
+
+        bool negated = isFalseTest(info.resultDisposition);
+        bool result = expr.getResult() != negated;
+
+        if (result) {
+            if (!m_includeSuccessfulResults) {
+                assertionPassed();
+            } else {
+                reportExpr(info, ResultWas::Ok, &expr, negated);
+            }
+        } else {
+            reportExpr(info, ResultWas::ExpressionFailed, &expr, negated);
+            populateReaction(reaction);
+        }
+    }
+    void RunContext::reportExpr(AssertionInfo const& info,
+                                ResultWas::OfType resultType,
+                                ITransientExpression const* expr,
+                                bool negated) {
+
+        m_lastAssertionInfo = info;
+        AssertionResultData data(resultType, LazyExpression(negated));
+
+        AssertionResult assertionResult{info, data};
+        assertionResult.m_resultData.lazyExpression.m_transientExpression = expr;
+
+        assertionEnded(assertionResult);
+    }
+
+    void RunContext::handleMessage(AssertionInfo const& info,
+                                   ResultWas::OfType resultType,
+                                   StringRef const& message,
+                                   AssertionReaction& reaction) {
+        m_reporter->assertionStarting(info);
+
+        m_lastAssertionInfo = info;
+
+        AssertionResultData data(resultType, LazyExpression(false));
+        data.message = static_cast<std::string>(message);
+        AssertionResult assertionResult{m_lastAssertionInfo, data};
+        assertionEnded(assertionResult);
+        if (!assertionResult.isOk())
+            populateReaction(reaction);
+    }
+    void RunContext::handleUnexpectedExceptionNotThrown(AssertionInfo const& info,
+                                                        AssertionReaction& reaction) {
+        handleNonExpr(info, Catch::ResultWas::DidntThrowException, reaction);
+    }
+
+    void RunContext::handleUnexpectedInflightException(AssertionInfo const& info,
+                                                       std::string const& message,
+                                                       AssertionReaction& reaction) {
+        m_lastAssertionInfo = info;
+
+        AssertionResultData data(ResultWas::ThrewException, LazyExpression(false));
+        data.message = message;
+        AssertionResult assertionResult{info, data};
+        assertionEnded(assertionResult);
+        populateReaction(reaction);
+    }
+
+    void RunContext::populateReaction(AssertionReaction& reaction) {
+        reaction.shouldDebugBreak = m_config->shouldDebugBreak();
+        reaction.shouldThrow =
+            aborting() || (m_lastAssertionInfo.resultDisposition & ResultDisposition::Normal);
+    }
+
+    void RunContext::handleIncomplete(AssertionInfo const& info) {
+        m_lastAssertionInfo = info;
+
+        AssertionResultData data(ResultWas::ThrewException, LazyExpression(false));
+        data.message = "Exception translation was disabled by CATCH_CONFIG_FAST_COMPILE";
+        AssertionResult assertionResult{info, data};
+        assertionEnded(assertionResult);
+    }
+    void RunContext::handleNonExpr(AssertionInfo const& info,
+                                   ResultWas::OfType resultType,
+                                   AssertionReaction& reaction) {
+        m_lastAssertionInfo = info;
+
+        AssertionResultData data(resultType, LazyExpression(false));
+        AssertionResult assertionResult{info, data};
+        assertionEnded(assertionResult);
+
+        if (!assertionResult.isOk())
+            populateReaction(reaction);
+    }
+
+    IResultCapture& getResultCapture() {
+        if (auto* capture = getCurrentContext().getResultCapture())
+            return *capture;
+        else
+            CATCH_INTERNAL_ERROR("No result capture instance");
+    }
+
+    void seedRng(IConfig const& config) {
+        if (config.rngSeed() != 0) {
+            std::srand(config.rngSeed());
+            rng().seed(config.rngSeed());
+        }
+    }
+
+    unsigned int rngSeed() { return getCurrentContext().getConfig()->rngSeed(); }
+
+} // namespace Catch
+// end catch_run_context.cpp
+// start catch_section.cpp
+
+namespace Catch {
+
+    Section::Section(SectionInfo const& info) :
+        m_info(info), m_sectionIncluded(getResultCapture().sectionStarted(m_info, m_assertions)) {
+        m_timer.start();
+    }
+
+    Section::~Section() {
+        if (m_sectionIncluded) {
+            SectionEndInfo endInfo{m_info, m_assertions, m_timer.getElapsedSeconds()};
+            if (uncaught_exceptions())
+                getResultCapture().sectionEndedEarly(endInfo);
+            else
+                getResultCapture().sectionEnded(endInfo);
+        }
+    }
+
+    // This indicates whether the section should be executed or not
+    Section::operator bool() const { return m_sectionIncluded; }
+
+} // end namespace Catch
+// end catch_section.cpp
+// start catch_section_info.cpp
+
+namespace Catch {
+
+    SectionInfo::SectionInfo(SourceLineInfo const& _lineInfo, std::string const& _name) :
+        name(_name), lineInfo(_lineInfo) {}
+
+} // end namespace Catch
+// end catch_section_info.cpp
+// start catch_session.cpp
+
+// start catch_session.h
+
+#include <memory>
+
+namespace Catch {
+
+    class Session : NonCopyable {
+    public:
+        Session();
+        ~Session() override;
+
+        void showHelp() const;
+        void libIdentify();
+
+        int applyCommandLine(int argc, char const* const* argv);
+#if defined(CATCH_CONFIG_WCHAR) && defined(_WIN32) && defined(UNICODE)
+        int applyCommandLine(int argc, wchar_t const* const* argv);
+#endif
+
+        void useConfigData(ConfigData const& configData);
+
+        template <typename CharT> int run(int argc, CharT const* const argv[]) {
+            if (m_startupExceptions)
+                return 1;
+            int returnCode = applyCommandLine(argc, argv);
+            if (returnCode == 0)
+                returnCode = run();
+            return returnCode;
+        }
+
+        int run();
+
+        clara::Parser const& cli() const;
+        void cli(clara::Parser const& newParser);
+        ConfigData& configData();
+        Config& config();
+
+    private:
+        int runInternal();
+
+        clara::Parser m_cli;
+        ConfigData m_configData;
+        std::shared_ptr<Config> m_config;
+        bool m_startupExceptions = false;
+    };
+
+} // end namespace Catch
+
+// end catch_session.h
+// start catch_version.h
+
+#include <iosfwd>
+
+namespace Catch {
+
+    // Versioning information
+    struct Version {
+        Version(Version const&) = delete;
+        Version& operator=(Version const&) = delete;
+        Version(unsigned int _majorVersion,
+                unsigned int _minorVersion,
+                unsigned int _patchNumber,
+                char const* const _branchName,
+                unsigned int _buildNumber);
+
+        unsigned int const majorVersion;
+        unsigned int const minorVersion;
+        unsigned int const patchNumber;
+
+        // buildNumber is only used if branchName is not null
+        char const* const branchName;
+        unsigned int const buildNumber;
+
+        friend std::ostream& operator<<(std::ostream& os, Version const& version);
+    };
+
+    Version const& libraryVersion();
+} // namespace Catch
+
+// end catch_version.h
+#include <cstdlib>
+#include <iomanip>
+#include <set>
+#include <iterator>
+
+namespace Catch {
+
+    namespace {
+        const int MaxExitCode = 255;
+
+        IStreamingReporterPtr createReporter(std::string const& reporterName,
+                                             IConfigPtr const& config) {
+            auto reporter =
+                Catch::getRegistryHub().getReporterRegistry().create(reporterName, config);
+            CATCH_ENFORCE(reporter, "No reporter registered with name: '" << reporterName << "'");
+
+            return reporter;
+        }
+
+        IStreamingReporterPtr makeReporter(std::shared_ptr<Config> const& config) {
+            if (Catch::getRegistryHub().getReporterRegistry().getListeners().empty()) {
+                return createReporter(config->getReporterName(), config);
+            }
+
+            // On older platforms, returning std::unique_ptr<ListeningReporter>
+            // when the return type is std::unique_ptr<IStreamingReporter>
+            // doesn't compile without a std::move call. However, this causes
+            // a warning on newer platforms. Thus, we have to work around
+            // it a bit and downcast the pointer manually.
+            auto ret = std::unique_ptr<IStreamingReporter>(new ListeningReporter);
+            auto& multi = static_cast<ListeningReporter&>(*ret);
+            auto const& listeners = Catch::getRegistryHub().getReporterRegistry().getListeners();
+            for (auto const& listener : listeners) {
+                multi.addListener(listener->create(Catch::ReporterConfig(config)));
+            }
+            multi.addReporter(createReporter(config->getReporterName(), config));
+            return ret;
+        }
+
+        class TestGroup {
+        public:
+            explicit TestGroup(std::shared_ptr<Config> const& config) :
+                m_config{config}, m_context{config, makeReporter(config)} {
+                auto const& allTestCases = getAllTestCasesSorted(*m_config);
+                m_matches = m_config->testSpec().matchesByFilter(allTestCases, *m_config);
+                auto const& invalidArgs = m_config->testSpec().getInvalidArgs();
+
+                if (m_matches.empty() && invalidArgs.empty()) {
+                    for (auto const& test : allTestCases)
+                        if (!test.isHidden())
+                            m_tests.emplace(&test);
+                } else {
+                    for (auto const& match : m_matches)
+                        m_tests.insert(match.tests.begin(), match.tests.end());
+                }
+            }
+
+            Totals execute() {
+                auto const& invalidArgs = m_config->testSpec().getInvalidArgs();
+                Totals totals;
+                m_context.testGroupStarting(m_config->name(), 1, 1);
+                for (auto const& testCase : m_tests) {
+                    if (!m_context.aborting())
+                        totals += m_context.runTest(*testCase);
+                    else
+                        m_context.reporter().skipTest(*testCase);
+                }
+
+                for (auto const& match : m_matches) {
+                    if (match.tests.empty()) {
+                        m_context.reporter().noMatchingTestCases(match.name);
+                        totals.error = -1;
+                    }
+                }
+
+                if (!invalidArgs.empty()) {
+                    for (auto const& invalidArg : invalidArgs)
+                        m_context.reporter().reportInvalidArguments(invalidArg);
+                }
+
+                m_context.testGroupEnded(m_config->name(), totals, 1, 1);
+                return totals;
+            }
+
+        private:
+            using Tests = std::set<TestCase const*>;
+
+            std::shared_ptr<Config> m_config;
+            RunContext m_context;
+            Tests m_tests;
+            TestSpec::Matches m_matches;
+        };
+
+        void applyFilenamesAsTags(Catch::IConfig const& config) {
+            auto& tests = const_cast<std::vector<TestCase>&>(getAllTestCasesSorted(config));
+            for (auto& testCase : tests) {
+                auto tags = testCase.tags;
+
+                std::string filename = testCase.lineInfo.file;
+                auto lastSlash = filename.find_last_of("\\/");
+                if (lastSlash != std::string::npos) {
+                    filename.erase(0, lastSlash);
+                    filename[0] = '#';
+                }
+
+                auto lastDot = filename.find_last_of('.');
+                if (lastDot != std::string::npos) {
+                    filename.erase(lastDot);
+                }
+
+                tags.push_back(std::move(filename));
+                setTags(testCase, tags);
+            }
+        }
+
+    } // namespace
+
+    Session::Session() {
+        static bool alreadyInstantiated = false;
+        if (alreadyInstantiated) {
+            CATCH_TRY {
+                CATCH_INTERNAL_ERROR("Only one instance of Catch::Session can ever be used");
+            }
+            CATCH_CATCH_ALL { getMutableRegistryHub().registerStartupException(); }
+        }
+
+        // There cannot be exceptions at startup in no-exception mode.
+#if !defined(CATCH_CONFIG_DISABLE_EXCEPTIONS)
+        const auto& exceptions = getRegistryHub().getStartupExceptionRegistry().getExceptions();
+        if (!exceptions.empty()) {
+            config();
+            getCurrentMutableContext().setConfig(m_config);
+
+            m_startupExceptions = true;
+            Colour colourGuard(Colour::Red);
+            Catch::cerr() << "Errors occurred during startup!" << '\n';
+            // iterate over all exceptions and notify user
+            for (const auto& ex_ptr : exceptions) {
+                try {
+                    std::rethrow_exception(ex_ptr);
+                } catch (std::exception const& ex) {
+                    Catch::cerr() << Column(ex.what()).indent(2) << '\n';
+                }
+            }
+        }
+#endif
+
+        alreadyInstantiated = true;
+        m_cli = makeCommandLineParser(m_configData);
+    }
+    Session::~Session() { Catch::cleanUp(); }
+
+    void Session::showHelp() const {
+        Catch::cout() << "\nCatch v" << libraryVersion() << "\n"
+                      << m_cli << std::endl
+                      << "For more detailed usage please see the project docs\n"
+                      << std::endl;
+    }
+    void Session::libIdentify() {
+        Catch::cout() << std::left << std::setw(16) << "description: "
+                      << "A Catch2 test executable\n"
+                      << std::left << std::setw(16) << "category: "
+                      << "testframework\n"
+                      << std::left << std::setw(16) << "framework: "
+                      << "Catch Test\n"
+                      << std::left << std::setw(16) << "version: " << libraryVersion() << std::endl;
+    }
+
+    int Session::applyCommandLine(int argc, char const* const* argv) {
+        if (m_startupExceptions)
+            return 1;
+
+        auto result = m_cli.parse(clara::Args(argc, argv));
+        if (!result) {
+            config();
+            getCurrentMutableContext().setConfig(m_config);
+            Catch::cerr() << Colour(Colour::Red) << "\nError(s) in input:\n"
+                          << Column(result.errorMessage()).indent(2) << "\n\n";
+            Catch::cerr() << "Run with -? for usage\n" << std::endl;
+            return MaxExitCode;
+        }
+
+        if (m_configData.showHelp)
+            showHelp();
+        if (m_configData.libIdentify)
+            libIdentify();
+        m_config.reset();
+        return 0;
+    }
+
+#if defined(CATCH_CONFIG_WCHAR) && defined(_WIN32) && defined(UNICODE)
+    int Session::applyCommandLine(int argc, wchar_t const* const* argv) {
+
+        char** utf8Argv = new char*[argc];
+
+        for (int i = 0; i < argc; ++i) {
+            int bufSize =
+                WideCharToMultiByte(CP_UTF8, 0, argv[i], -1, nullptr, 0, nullptr, nullptr);
+
+            utf8Argv[i] = new char[bufSize];
+
+            WideCharToMultiByte(CP_UTF8, 0, argv[i], -1, utf8Argv[i], bufSize, nullptr, nullptr);
+        }
+
+        int returnCode = applyCommandLine(argc, utf8Argv);
+
+        for (int i = 0; i < argc; ++i)
+            delete[] utf8Argv[i];
+
+        delete[] utf8Argv;
+
+        return returnCode;
+    }
+#endif
+
+    void Session::useConfigData(ConfigData const& configData) {
+        m_configData = configData;
+        m_config.reset();
+    }
+
+    int Session::run() {
+        if ((m_configData.waitForKeypress & WaitForKeypress::BeforeStart) != 0) {
+            Catch::cout() << "...waiting for enter/ return before starting" << std::endl;
+            static_cast<void>(std::getchar());
+        }
+        int exitCode = runInternal();
+        if ((m_configData.waitForKeypress & WaitForKeypress::BeforeExit) != 0) {
+            Catch::cout() << "...waiting for enter/ return before exiting, with code: " << exitCode
+                          << std::endl;
+            static_cast<void>(std::getchar());
+        }
+        return exitCode;
+    }
+
+    clara::Parser const& Session::cli() const { return m_cli; }
+    void Session::cli(clara::Parser const& newParser) { m_cli = newParser; }
+    ConfigData& Session::configData() { return m_configData; }
+    Config& Session::config() {
+        if (!m_config)
+            m_config = std::make_shared<Config>(m_configData);
+        return *m_config;
+    }
+
+    int Session::runInternal() {
+        if (m_startupExceptions)
+            return 1;
+
+        if (m_configData.showHelp || m_configData.libIdentify) {
+            return 0;
+        }
+
+        CATCH_TRY {
+            config(); // Force config to be constructed
+
+            seedRng(*m_config);
+
+            if (m_configData.filenamesAsTags)
+                applyFilenamesAsTags(*m_config);
+
+            // Handle list request
+            if (Option<std::size_t> listed = list(m_config))
+                return static_cast<int>(*listed);
+
+            TestGroup tests{m_config};
+            auto const totals = tests.execute();
+
+            if (m_config->warnAboutNoTests() && totals.error == -1)
+                return 2;
+
+            // Note that on unices only the lower 8 bits are usually used,
+            // clamping the return value to 255 prevents false negative when
+            // some multiple of 256 tests has failed
+            return (std::min)(MaxExitCode,
+                              (std::max)(totals.error, static_cast<int>(totals.assertions.failed)));
+        }
+#if !defined(CATCH_CONFIG_DISABLE_EXCEPTIONS)
+        catch (std::exception& ex) {
+            Catch::cerr() << ex.what() << std::endl;
+            return MaxExitCode;
+        }
+#endif
+    }
+
+} // end namespace Catch
+// end catch_session.cpp
+// start catch_singletons.cpp
+
+#include <vector>
+
+namespace Catch {
+
+    namespace {
+        static auto getSingletons() -> std::vector<ISingleton*>*& {
+            static std::vector<ISingleton*>* g_singletons = nullptr;
+            if (!g_singletons)
+                g_singletons = new std::vector<ISingleton*>();
+            return g_singletons;
+        }
+    } // namespace
+
+    ISingleton::~ISingleton() {}
+
+    void addSingleton(ISingleton* singleton) { getSingletons()->push_back(singleton); }
+    void cleanupSingletons() {
+        auto& singletons = getSingletons();
+        for (auto singleton : *singletons)
+            delete singleton;
+        delete singletons;
+        singletons = nullptr;
+    }
+
+} // namespace Catch
+// end catch_singletons.cpp
+// start catch_startup_exception_registry.cpp
+
+#if !defined(CATCH_CONFIG_DISABLE_EXCEPTIONS)
+namespace Catch {
+    void StartupExceptionRegistry::add(std::exception_ptr const& exception) noexcept {
+        CATCH_TRY { m_exceptions.push_back(exception); }
+        CATCH_CATCH_ALL {
+            // If we run out of memory during start-up there's really not a lot
+            // more we can do about it
+            std::terminate();
+        }
+    }
+
+    std::vector<std::exception_ptr> const&
+    StartupExceptionRegistry::getExceptions() const noexcept {
+        return m_exceptions;
+    }
+
+} // end namespace Catch
+#endif
+// end catch_startup_exception_registry.cpp
+// start catch_stream.cpp
+
+#include <cstdio>
+#include <iostream>
+#include <fstream>
+#include <sstream>
+#include <vector>
+#include <memory>
+
+namespace Catch {
+
+    Catch::IStream::~IStream() = default;
+
+    namespace Detail {
+        namespace {
+            template <typename WriterF, std::size_t bufferSize = 256>
+            class StreamBufImpl : public std::streambuf {
+                char data[bufferSize];
+                WriterF m_writer;
+
+            public:
+                StreamBufImpl() { setp(data, data + sizeof(data)); }
+
+                ~StreamBufImpl() noexcept { StreamBufImpl::sync(); }
+
+            private:
+                int overflow(int c) override {
+                    sync();
+
+                    if (c != EOF) {
+                        if (pbase() == epptr())
+                            m_writer(std::string(1, static_cast<char>(c)));
+                        else
+                            sputc(static_cast<char>(c));
+                    }
+                    return 0;
+                }
+
+                int sync() override {
+                    if (pbase() != pptr()) {
+                        m_writer(std::string(
+                            pbase(), static_cast<std::string::size_type>(pptr() - pbase())));
+                        setp(pbase(), epptr());
+                    }
+                    return 0;
+                }
+            };
+
+            ///////////////////////////////////////////////////////////////////////////
+
+            struct OutputDebugWriter {
+
+                void operator()(std::string const& str) { writeToDebugConsole(str); }
+            };
+
+            ///////////////////////////////////////////////////////////////////////////
+
+            class FileStream : public IStream {
+                mutable std::ofstream m_ofs;
+
+            public:
+                FileStream(StringRef filename) {
+                    m_ofs.open(filename.c_str());
+                    CATCH_ENFORCE(!m_ofs.fail(), "Unable to open file: '" << filename << "'");
+                }
+                ~FileStream() override = default;
+
+            public: // IStream
+                std::ostream& stream() const override { return m_ofs; }
+            };
+
+            ///////////////////////////////////////////////////////////////////////////
+
+            class CoutStream : public IStream {
+                mutable std::ostream m_os;
+
+            public:
+                // Store the streambuf from cout up-front because
+                // cout may get redirected when running tests
+                CoutStream() : m_os(Catch::cout().rdbuf()) {}
+                ~CoutStream() override = default;
+
+            public: // IStream
+                std::ostream& stream() const override { return m_os; }
+            };
+
+            ///////////////////////////////////////////////////////////////////////////
+
+            class DebugOutStream : public IStream {
+                std::unique_ptr<StreamBufImpl<OutputDebugWriter>> m_streamBuf;
+                mutable std::ostream m_os;
+
+            public:
+                DebugOutStream() :
+                    m_streamBuf(new StreamBufImpl<OutputDebugWriter>()), m_os(m_streamBuf.get()) {}
+
+                ~DebugOutStream() override = default;
+
+            public: // IStream
+                std::ostream& stream() const override { return m_os; }
+            };
+
+        } // namespace
+    }     // namespace Detail
+
+    ///////////////////////////////////////////////////////////////////////////
+
+    auto makeStream(StringRef const& filename) -> IStream const* {
+        if (filename.empty())
+            return new Detail::CoutStream();
+        else if (filename[0] == '%') {
+            if (filename == "%debug")
+                return new Detail::DebugOutStream();
+            else
+                CATCH_ERROR("Unrecognised stream: '" << filename << "'");
+        } else
+            return new Detail::FileStream(filename);
+    }
+
+    // This class encapsulates the idea of a pool of ostringstreams that can be
+    // reused.
+    struct StringStreams {
+        std::vector<std::unique_ptr<std::ostringstream>> m_streams;
+        std::vector<std::size_t> m_unused;
+        std::ostringstream m_referenceStream; // Used for copy state/ flags from
+
+        auto add() -> std::size_t {
+            if (m_unused.empty()) {
+                m_streams.push_back(std::unique_ptr<std::ostringstream>(new std::ostringstream));
+                return m_streams.size() - 1;
+            } else {
+                auto index = m_unused.back();
+                m_unused.pop_back();
+                return index;
+            }
+        }
+
+        void release(std::size_t index) {
+            m_streams[index]->copyfmt(m_referenceStream); // Restore initial flags and other state
+            m_unused.push_back(index);
+        }
+    };
+
+    ReusableStringStream::ReusableStringStream() :
+        m_index(Singleton<StringStreams>::getMutable().add()),
+        m_oss(Singleton<StringStreams>::getMutable().m_streams[m_index].get()) {}
+
+    ReusableStringStream::~ReusableStringStream() {
+        static_cast<std::ostringstream*>(m_oss)->str("");
+        m_oss->clear();
+        Singleton<StringStreams>::getMutable().release(m_index);
+    }
+
+    auto ReusableStringStream::str() const -> std::string {
+        return static_cast<std::ostringstream*>(m_oss)->str();
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+
+#ifndef CATCH_CONFIG_NOSTDOUT // If you #define this you must implement these
+                              // functions
+    std::ostream& cout() { return std::cout; }
+    std::ostream& cerr() { return std::cerr; }
+    std::ostream& clog() { return std::clog; }
+#endif
+} // namespace Catch
+// end catch_stream.cpp
+// start catch_string_manip.cpp
+
+#include <algorithm>
+#include <ostream>
+#include <cstring>
+#include <cctype>
+#include <vector>
+
+namespace Catch {
+
+    namespace {
+        char toLowerCh(char c) {
+            return static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
+        }
+    } // namespace
+
+    bool startsWith(std::string const& s, std::string const& prefix) {
+        return s.size() >= prefix.size() && std::equal(prefix.begin(), prefix.end(), s.begin());
+    }
+    bool startsWith(std::string const& s, char prefix) { return !s.empty() && s[0] == prefix; }
+    bool endsWith(std::string const& s, std::string const& suffix) {
+        return s.size() >= suffix.size() && std::equal(suffix.rbegin(), suffix.rend(), s.rbegin());
+    }
+    bool endsWith(std::string const& s, char suffix) {
+        return !s.empty() && s[s.size() - 1] == suffix;
+    }
+    bool contains(std::string const& s, std::string const& infix) {
+        return s.find(infix) != std::string::npos;
+    }
+    void toLowerInPlace(std::string& s) {
+        std::transform(s.begin(), s.end(), s.begin(), toLowerCh);
+    }
+    std::string toLower(std::string const& s) {
+        std::string lc = s;
+        toLowerInPlace(lc);
+        return lc;
+    }
+    std::string trim(std::string const& str) {
+        static char const* whitespaceChars = "\n\r\t ";
+        std::string::size_type start = str.find_first_not_of(whitespaceChars);
+        std::string::size_type end = str.find_last_not_of(whitespaceChars);
+
+        return start != std::string::npos ? str.substr(start, 1 + end - start) : std::string();
+    }
+
+    StringRef trim(StringRef ref) {
+        const auto is_ws = [](char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; };
+        size_t real_begin = 0;
+        while (real_begin < ref.size() && is_ws(ref[real_begin])) {
+            ++real_begin;
+        }
+        size_t real_end = ref.size();
+        while (real_end > real_begin && is_ws(ref[real_end - 1])) {
+            --real_end;
+        }
+
+        return ref.substr(real_begin, real_end - real_begin);
+    }
+
+    bool
+    replaceInPlace(std::string& str, std::string const& replaceThis, std::string const& withThis) {
+        bool replaced = false;
+        std::size_t i = str.find(replaceThis);
+        while (i != std::string::npos) {
+            replaced = true;
+            str = str.substr(0, i) + withThis + str.substr(i + replaceThis.size());
+            if (i < str.size() - withThis.size())
+                i = str.find(replaceThis, i + withThis.size());
+            else
+                i = std::string::npos;
+        }
+        return replaced;
+    }
+
+    std::vector<StringRef> splitStringRef(StringRef str, char delimiter) {
+        std::vector<StringRef> subStrings;
+        std::size_t start = 0;
+        for (std::size_t pos = 0; pos < str.size(); ++pos) {
+            if (str[pos] == delimiter) {
+                if (pos - start > 1)
+                    subStrings.push_back(str.substr(start, pos - start));
+                start = pos + 1;
+            }
+        }
+        if (start < str.size())
+            subStrings.push_back(str.substr(start, str.size() - start));
+        return subStrings;
+    }
+
+    pluralise::pluralise(std::size_t count, std::string const& label) :
+        m_count(count), m_label(label) {}
+
+    std::ostream& operator<<(std::ostream& os, pluralise const& pluraliser) {
+        os << pluraliser.m_count << ' ' << pluraliser.m_label;
+        if (pluraliser.m_count != 1)
+            os << 's';
+        return os;
+    }
+
+} // namespace Catch
+// end catch_string_manip.cpp
+// start catch_stringref.cpp
+
+#include <algorithm>
+#include <ostream>
+#include <cstring>
+#include <cstdint>
+
+namespace Catch {
+    StringRef::StringRef(char const* rawChars) noexcept :
+        StringRef(rawChars, static_cast<StringRef::size_type>(std::strlen(rawChars))) {}
+
+    auto StringRef::c_str() const -> char const* {
+        CATCH_ENFORCE(isNullTerminated(),
+                      "Called StringRef::c_str() on a non-null-terminated instance");
+        return m_start;
+    }
+    auto StringRef::data() const noexcept -> char const* { return m_start; }
+
+    auto StringRef::substr(size_type start, size_type size) const noexcept -> StringRef {
+        if (start < m_size) {
+            return StringRef(m_start + start, (std::min)(m_size - start, size));
+        } else {
+            return StringRef();
+        }
+    }
+    auto StringRef::operator==(StringRef const& other) const noexcept -> bool {
+        return m_size == other.m_size && (std::memcmp(m_start, other.m_start, m_size) == 0);
+    }
+
+    auto operator<<(std::ostream& os, StringRef const& str) -> std::ostream& {
+        return os.write(str.data(), str.size());
+    }
+
+    auto operator+=(std::string& lhs, StringRef const& rhs) -> std::string& {
+        lhs.append(rhs.data(), rhs.size());
+        return lhs;
+    }
+
+} // namespace Catch
+// end catch_stringref.cpp
+// start catch_tag_alias.cpp
+
+namespace Catch {
+    TagAlias::TagAlias(std::string const& _tag, SourceLineInfo _lineInfo) :
+        tag(_tag), lineInfo(_lineInfo) {}
+} // namespace Catch
+// end catch_tag_alias.cpp
+// start catch_tag_alias_autoregistrar.cpp
+
+namespace Catch {
+
+    RegistrarForTagAliases::RegistrarForTagAliases(char const* alias,
+                                                   char const* tag,
+                                                   SourceLineInfo const& lineInfo) {
+        CATCH_TRY { getMutableRegistryHub().registerTagAlias(alias, tag, lineInfo); }
+        CATCH_CATCH_ALL {
+            // Do not throw when constructing global objects, instead register
+            // the exception to be processed later
+            getMutableRegistryHub().registerStartupException();
+        }
+    }
+
+} // namespace Catch
+// end catch_tag_alias_autoregistrar.cpp
+// start catch_tag_alias_registry.cpp
+
+#include <sstream>
+
+namespace Catch {
+
+    TagAliasRegistry::~TagAliasRegistry() {}
+
+    TagAlias const* TagAliasRegistry::find(std::string const& alias) const {
+        auto it = m_registry.find(alias);
+        if (it != m_registry.end())
+            return &(it->second);
+        else
+            return nullptr;
+    }
+
+    std::string TagAliasRegistry::expandAliases(std::string const& unexpandedTestSpec) const {
+        std::string expandedTestSpec = unexpandedTestSpec;
+        for (auto const& registryKvp : m_registry) {
+            std::size_t pos = expandedTestSpec.find(registryKvp.first);
+            if (pos != std::string::npos) {
+                expandedTestSpec = expandedTestSpec.substr(0, pos) + registryKvp.second.tag +
+                                   expandedTestSpec.substr(pos + registryKvp.first.size());
+            }
+        }
+        return expandedTestSpec;
+    }
+
+    void TagAliasRegistry::add(std::string const& alias,
+                               std::string const& tag,
+                               SourceLineInfo const& lineInfo) {
+        CATCH_ENFORCE(startsWith(alias, "[@") && endsWith(alias, ']'),
+                      "error: tag alias, '" << alias << "' is not of the form [@alias name].\n"
+                                            << lineInfo);
+
+        CATCH_ENFORCE(m_registry.insert(std::make_pair(alias, TagAlias(tag, lineInfo))).second,
+                      "error: tag alias, '" << alias << "' already registered.\n"
+                                            << "\tFirst seen at: " << find(alias)->lineInfo << "\n"
+                                            << "\tRedefined at: " << lineInfo);
+    }
+
+    ITagAliasRegistry::~ITagAliasRegistry() {}
+
+    ITagAliasRegistry const& ITagAliasRegistry::get() {
+        return getRegistryHub().getTagAliasRegistry();
+    }
+
+} // end namespace Catch
+// end catch_tag_alias_registry.cpp
+// start catch_test_case_info.cpp
+
+#include <cctype>
+#include <exception>
+#include <algorithm>
+#include <sstream>
+
+namespace Catch {
+
+    namespace {
+        TestCaseInfo::SpecialProperties parseSpecialTag(std::string const& tag) {
+            if (startsWith(tag, '.') || tag == "!hide")
+                return TestCaseInfo::IsHidden;
+            else if (tag == "!throws")
+                return TestCaseInfo::Throws;
+            else if (tag == "!shouldfail")
+                return TestCaseInfo::ShouldFail;
+            else if (tag == "!mayfail")
+                return TestCaseInfo::MayFail;
+            else if (tag == "!nonportable")
+                return TestCaseInfo::NonPortable;
+            else if (tag == "!benchmark")
+                return static_cast<TestCaseInfo::SpecialProperties>(TestCaseInfo::Benchmark |
+                                                                    TestCaseInfo::IsHidden);
+            else
+                return TestCaseInfo::None;
+        }
+        bool isReservedTag(std::string const& tag) {
+            return parseSpecialTag(tag) == TestCaseInfo::None && tag.size() > 0 &&
+                   !std::isalnum(static_cast<unsigned char>(tag[0]));
+        }
+        void enforceNotReservedTag(std::string const& tag, SourceLineInfo const& _lineInfo) {
+            CATCH_ENFORCE(!isReservedTag(tag),
+                          "Tag name: [" << tag << "] is not allowed.\n"
+                                        << "Tag names starting with non alphanumeric "
+                                           "characters are reserved\n"
+                                        << _lineInfo);
+        }
+    } // namespace
+
+    TestCase makeTestCase(ITestInvoker* _testCase,
+                          std::string const& _className,
+                          NameAndTags const& nameAndTags,
+                          SourceLineInfo const& _lineInfo) {
+        bool isHidden = false;
+
+        // Parse out tags
+        std::vector<std::string> tags;
+        std::string desc, tag;
+        bool inTag = false;
+        for (char c : nameAndTags.tags) {
+            if (!inTag) {
+                if (c == '[')
+                    inTag = true;
+                else
+                    desc += c;
+            } else {
+                if (c == ']') {
+                    TestCaseInfo::SpecialProperties prop = parseSpecialTag(tag);
+                    if ((prop & TestCaseInfo::IsHidden) != 0)
+                        isHidden = true;
+                    else if (prop == TestCaseInfo::None)
+                        enforceNotReservedTag(tag, _lineInfo);
+
+                    // Merged hide tags like `[.approvals]` should be added as
+                    // `[.][approvals]`. The `[.]` is added at later point, so
+                    // we only strip the prefix
+                    if (startsWith(tag, '.') && tag.size() > 1) {
+                        tag.erase(0, 1);
+                    }
+                    tags.push_back(tag);
+                    tag.clear();
+                    inTag = false;
+                } else
+                    tag += c;
+            }
+        }
+        if (isHidden) {
+            // Add all "hidden" tags to make them behave identically
+            tags.insert(tags.end(), {".", "!hide"});
+        }
+
+        TestCaseInfo info(
+            static_cast<std::string>(nameAndTags.name), _className, desc, tags, _lineInfo);
+        return TestCase(_testCase, std::move(info));
+    }
+
+    void setTags(TestCaseInfo& testCaseInfo, std::vector<std::string> tags) {
+        std::sort(begin(tags), end(tags));
+        tags.erase(std::unique(begin(tags), end(tags)), end(tags));
+        testCaseInfo.lcaseTags.clear();
+
+        for (auto const& tag : tags) {
+            std::string lcaseTag = toLower(tag);
+            testCaseInfo.properties = static_cast<TestCaseInfo::SpecialProperties>(
+                testCaseInfo.properties | parseSpecialTag(lcaseTag));
+            testCaseInfo.lcaseTags.push_back(lcaseTag);
+        }
+        testCaseInfo.tags = std::move(tags);
+    }
+
+    TestCaseInfo::TestCaseInfo(std::string const& _name,
+                               std::string const& _className,
+                               std::string const& _description,
+                               std::vector<std::string> const& _tags,
+                               SourceLineInfo const& _lineInfo) :
+        name(_name),
+        className(_className),
+        description(_description),
+        lineInfo(_lineInfo),
+        properties(None) {
+        setTags(*this, _tags);
+    }
+
+    bool TestCaseInfo::isHidden() const { return (properties & IsHidden) != 0; }
+    bool TestCaseInfo::throws() const { return (properties & Throws) != 0; }
+    bool TestCaseInfo::okToFail() const { return (properties & (ShouldFail | MayFail)) != 0; }
+    bool TestCaseInfo::expectedToFail() const { return (properties & (ShouldFail)) != 0; }
+
+    std::string TestCaseInfo::tagsAsString() const {
+        std::string ret;
+        // '[' and ']' per tag
+        std::size_t full_size = 2 * tags.size();
+        for (const auto& tag : tags) {
+            full_size += tag.size();
+        }
+        ret.reserve(full_size);
+        for (const auto& tag : tags) {
+            ret.push_back('[');
+            ret.append(tag);
+            ret.push_back(']');
+        }
+
+        return ret;
+    }
+
+    TestCase::TestCase(ITestInvoker* testCase, TestCaseInfo&& info) :
+        TestCaseInfo(std::move(info)), test(testCase) {}
+
+    TestCase TestCase::withName(std::string const& _newName) const {
+        TestCase other(*this);
+        other.name = _newName;
+        return other;
+    }
+
+    void TestCase::invoke() const { test->invoke(); }
+
+    bool TestCase::operator==(TestCase const& other) const {
+        return test.get() == other.test.get() && name == other.name && className == other.className;
+    }
+
+    bool TestCase::operator<(TestCase const& other) const { return name < other.name; }
+
+    TestCaseInfo const& TestCase::getTestCaseInfo() const { return *this; }
+
+} // end namespace Catch
+// end catch_test_case_info.cpp
+// start catch_test_case_registry_impl.cpp
+
+#include <algorithm>
+#include <sstream>
+
+namespace Catch {
+
+    namespace {
+        struct TestHasher {
+            using hash_t = uint64_t;
+
+            explicit TestHasher(hash_t hashSuffix) : m_hashSuffix{hashSuffix} {}
+
+            uint32_t operator()(TestCase const& t) const {
+                // FNV-1a hash with multiplication fold.
+                const hash_t prime = 1099511628211u;
+                hash_t hash = 14695981039346656037u;
+                for (const char c : t.name) {
+                    hash ^= c;
+                    hash *= prime;
+                }
+                hash ^= m_hashSuffix;
+                hash *= prime;
+                const uint32_t low{static_cast<uint32_t>(hash)};
+                const uint32_t high{static_cast<uint32_t>(hash >> 32)};
+                return low * high;
+            }
+
+        private:
+            hash_t m_hashSuffix;
+        };
+    } // end unnamed namespace
+
+    std::vector<TestCase> sortTests(IConfig const& config,
+                                    std::vector<TestCase> const& unsortedTestCases) {
+        switch (config.runOrder()) {
+            case RunTests::InDeclarationOrder:
+                // already in declaration order
+                break;
+
+            case RunTests::InLexicographicalOrder: {
+                std::vector<TestCase> sorted = unsortedTestCases;
+                std::sort(sorted.begin(), sorted.end());
+                return sorted;
+            }
+
+            case RunTests::InRandomOrder: {
+                seedRng(config);
+                TestHasher h{config.rngSeed()};
+
+                using hashedTest = std::pair<TestHasher::hash_t, TestCase const*>;
+                std::vector<hashedTest> indexed_tests;
+                indexed_tests.reserve(unsortedTestCases.size());
+
+                for (auto const& testCase : unsortedTestCases) {
+                    indexed_tests.emplace_back(h(testCase), &testCase);
+                }
+
+                std::sort(indexed_tests.begin(),
+                          indexed_tests.end(),
+                          [](hashedTest const& lhs, hashedTest const& rhs) {
+                              if (lhs.first == rhs.first) {
+                                  return lhs.second->name < rhs.second->name;
+                              }
+                              return lhs.first < rhs.first;
+                          });
+
+                std::vector<TestCase> sorted;
+                sorted.reserve(indexed_tests.size());
+
+                for (auto const& hashed : indexed_tests) {
+                    sorted.emplace_back(*hashed.second);
+                }
+
+                return sorted;
+            }
+        }
+        return unsortedTestCases;
+    }
+
+    bool isThrowSafe(TestCase const& testCase, IConfig const& config) {
+        return !testCase.throws() || config.allowThrows();
+    }
+
+    bool matchTest(TestCase const& testCase, TestSpec const& testSpec, IConfig const& config) {
+        return testSpec.matches(testCase) && isThrowSafe(testCase, config);
+    }
+
+    void enforceNoDuplicateTestCases(std::vector<TestCase> const& functions) {
+        std::set<TestCase> seenFunctions;
+        for (auto const& function : functions) {
+            auto prev = seenFunctions.insert(function);
+            CATCH_ENFORCE(prev.second,
+                          "error: TEST_CASE( \"" << function.name << "\" ) already defined.\n"
+                                                 << "\tFirst seen at "
+                                                 << prev.first->getTestCaseInfo().lineInfo << "\n"
+                                                 << "\tRedefined at "
+                                                 << function.getTestCaseInfo().lineInfo);
+        }
+    }
+
+    std::vector<TestCase> filterTests(std::vector<TestCase> const& testCases,
+                                      TestSpec const& testSpec,
+                                      IConfig const& config) {
+        std::vector<TestCase> filtered;
+        filtered.reserve(testCases.size());
+        for (auto const& testCase : testCases) {
+            if ((!testSpec.hasFilters() && !testCase.isHidden()) ||
+                (testSpec.hasFilters() && matchTest(testCase, testSpec, config)))
+            {
+                filtered.push_back(testCase);
+            }
+        }
+        return filtered;
+    }
+    std::vector<TestCase> const& getAllTestCasesSorted(IConfig const& config) {
+        return getRegistryHub().getTestCaseRegistry().getAllTestsSorted(config);
+    }
+
+    void TestRegistry::registerTest(TestCase const& testCase) {
+        std::string name = testCase.getTestCaseInfo().name;
+        if (name.empty()) {
+            ReusableStringStream rss;
+            rss << "Anonymous test case " << ++m_unnamedCount;
+            return registerTest(testCase.withName(rss.str()));
+        }
+        m_functions.push_back(testCase);
+    }
+
+    std::vector<TestCase> const& TestRegistry::getAllTests() const { return m_functions; }
+    std::vector<TestCase> const& TestRegistry::getAllTestsSorted(IConfig const& config) const {
+        if (m_sortedFunctions.empty())
+            enforceNoDuplicateTestCases(m_functions);
+
+        if (m_currentSortOrder != config.runOrder() || m_sortedFunctions.empty()) {
+            m_sortedFunctions = sortTests(config, m_functions);
+            m_currentSortOrder = config.runOrder();
+        }
+        return m_sortedFunctions;
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    TestInvokerAsFunction::TestInvokerAsFunction(void (*testAsFunction)()) noexcept :
+        m_testAsFunction(testAsFunction) {}
+
+    void TestInvokerAsFunction::invoke() const { m_testAsFunction(); }
+
+    std::string extractClassName(StringRef const& classOrQualifiedMethodName) {
+        std::string className(classOrQualifiedMethodName);
+        if (startsWith(className, '&')) {
+            std::size_t lastColons = className.rfind("::");
+            std::size_t penultimateColons = className.rfind("::", lastColons - 1);
+            if (penultimateColons == std::string::npos)
+                penultimateColons = 1;
+            className = className.substr(penultimateColons, lastColons - penultimateColons);
+        }
+        return className;
+    }
+
+} // end namespace Catch
+// end catch_test_case_registry_impl.cpp
+// start catch_test_case_tracker.cpp
+
+#include <algorithm>
+#include <cassert>
+#include <stdexcept>
+#include <memory>
+#include <sstream>
+
+#if defined(__clang__)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wexit-time-destructors"
+#endif
+
+namespace Catch {
+    namespace TestCaseTracking {
+
+        NameAndLocation::NameAndLocation(std::string const& _name,
+                                         SourceLineInfo const& _location) :
+            name(_name), location(_location) {}
+
+        ITracker::~ITracker() = default;
+
+        ITracker& TrackerContext::startRun() {
+            m_rootTracker = std::make_shared<SectionTracker>(
+                NameAndLocation("{root}", CATCH_INTERNAL_LINEINFO), *this, nullptr);
+            m_currentTracker = nullptr;
+            m_runState = Executing;
+            return *m_rootTracker;
+        }
+
+        void TrackerContext::endRun() {
+            m_rootTracker.reset();
+            m_currentTracker = nullptr;
+            m_runState = NotStarted;
+        }
+
+        void TrackerContext::startCycle() {
+            m_currentTracker = m_rootTracker.get();
+            m_runState = Executing;
+        }
+        void TrackerContext::completeCycle() { m_runState = CompletedCycle; }
+
+        bool TrackerContext::completedCycle() const { return m_runState == CompletedCycle; }
+        ITracker& TrackerContext::currentTracker() { return *m_currentTracker; }
+        void TrackerContext::setCurrentTracker(ITracker* tracker) { m_currentTracker = tracker; }
+
+        TrackerBase::TrackerBase(NameAndLocation const& nameAndLocation,
+                                 TrackerContext& ctx,
+                                 ITracker* parent) :
+            ITracker(nameAndLocation), m_ctx(ctx), m_parent(parent) {}
+
+        bool TrackerBase::isComplete() const {
+            return m_runState == CompletedSuccessfully || m_runState == Failed;
+        }
+        bool TrackerBase::isSuccessfullyCompleted() const {
+            return m_runState == CompletedSuccessfully;
+        }
+        bool TrackerBase::isOpen() const { return m_runState != NotStarted && !isComplete(); }
+        bool TrackerBase::hasChildren() const { return !m_children.empty(); }
+
+        void TrackerBase::addChild(ITrackerPtr const& child) { m_children.push_back(child); }
+
+        ITrackerPtr TrackerBase::findChild(NameAndLocation const& nameAndLocation) {
+            auto it = std::find_if(
+                m_children.begin(),
+                m_children.end(),
+                [&nameAndLocation](ITrackerPtr const& tracker) {
+                    return tracker->nameAndLocation().location == nameAndLocation.location &&
+                           tracker->nameAndLocation().name == nameAndLocation.name;
+                });
+            return (it != m_children.end()) ? *it : nullptr;
+        }
+        ITracker& TrackerBase::parent() {
+            assert(m_parent); // Should always be non-null except for root
+            return *m_parent;
+        }
+
+        void TrackerBase::openChild() {
+            if (m_runState != ExecutingChildren) {
+                m_runState = ExecutingChildren;
+                if (m_parent)
+                    m_parent->openChild();
+            }
+        }
+
+        bool TrackerBase::isSectionTracker() const { return false; }
+        bool TrackerBase::isGeneratorTracker() const { return false; }
+
+        void TrackerBase::open() {
+            m_runState = Executing;
+            moveToThis();
+            if (m_parent)
+                m_parent->openChild();
+        }
+
+        void TrackerBase::close() {
+
+            // Close any still open children (e.g. generators)
+            while (&m_ctx.currentTracker() != this)
+                m_ctx.currentTracker().close();
+
+            switch (m_runState) {
+                case NeedsAnotherRun:
+                    break;
+
+                case Executing:
+                    m_runState = CompletedSuccessfully;
+                    break;
+                case ExecutingChildren:
+                    if (std::all_of(m_children.begin(), m_children.end(), [](ITrackerPtr const& t) {
+                            return t->isComplete();
+                        }))
+                        m_runState = CompletedSuccessfully;
+                    break;
+
+                case NotStarted:
+                case CompletedSuccessfully:
+                case Failed:
+                    CATCH_INTERNAL_ERROR("Illogical state: " << m_runState);
+
+                default:
+                    CATCH_INTERNAL_ERROR("Unknown state: " << m_runState);
+            }
+            moveToParent();
+            m_ctx.completeCycle();
+        }
+        void TrackerBase::fail() {
+            m_runState = Failed;
+            if (m_parent)
+                m_parent->markAsNeedingAnotherRun();
+            moveToParent();
+            m_ctx.completeCycle();
+        }
+        void TrackerBase::markAsNeedingAnotherRun() { m_runState = NeedsAnotherRun; }
+
+        void TrackerBase::moveToParent() {
+            assert(m_parent);
+            m_ctx.setCurrentTracker(m_parent);
+        }
+        void TrackerBase::moveToThis() { m_ctx.setCurrentTracker(this); }
+
+        SectionTracker::SectionTracker(NameAndLocation const& nameAndLocation,
+                                       TrackerContext& ctx,
+                                       ITracker* parent) :
+            TrackerBase(nameAndLocation, ctx, parent), m_trimmed_name(trim(nameAndLocation.name)) {
+            if (parent) {
+                while (!parent->isSectionTracker())
+                    parent = &parent->parent();
+
+                SectionTracker& parentSection = static_cast<SectionTracker&>(*parent);
+                addNextFilters(parentSection.m_filters);
+            }
+        }
+
+        bool SectionTracker::isComplete() const {
+            bool complete = true;
+
+            if (m_filters.empty() || m_filters[0] == "" ||
+                std::find(m_filters.begin(), m_filters.end(), m_trimmed_name) != m_filters.end())
+            {
+                complete = TrackerBase::isComplete();
+            }
+            return complete;
+        }
+
+        bool SectionTracker::isSectionTracker() const { return true; }
+
+        SectionTracker& SectionTracker::acquire(TrackerContext& ctx,
+                                                NameAndLocation const& nameAndLocation) {
+            std::shared_ptr<SectionTracker> section;
+
+            ITracker& currentTracker = ctx.currentTracker();
+            if (ITrackerPtr childTracker = currentTracker.findChild(nameAndLocation)) {
+                assert(childTracker);
+                assert(childTracker->isSectionTracker());
+                section = std::static_pointer_cast<SectionTracker>(childTracker);
+            } else {
+                section = std::make_shared<SectionTracker>(nameAndLocation, ctx, &currentTracker);
+                currentTracker.addChild(section);
+            }
+            if (!ctx.completedCycle())
+                section->tryOpen();
+            return *section;
+        }
+
+        void SectionTracker::tryOpen() {
+            if (!isComplete())
+                open();
+        }
+
+        void SectionTracker::addInitialFilters(std::vector<std::string> const& filters) {
+            if (!filters.empty()) {
+                m_filters.reserve(m_filters.size() + filters.size() + 2);
+                m_filters.emplace_back(""); // Root - should never be consulted
+                m_filters.emplace_back(""); // Test Case - not a section filter
+                m_filters.insert(m_filters.end(), filters.begin(), filters.end());
+            }
+        }
+        void SectionTracker::addNextFilters(std::vector<std::string> const& filters) {
+            if (filters.size() > 1)
+                m_filters.insert(m_filters.end(), filters.begin() + 1, filters.end());
+        }
+
+        std::vector<std::string> const& SectionTracker::getFilters() const { return m_filters; }
+
+        std::string const& SectionTracker::trimmedName() const { return m_trimmed_name; }
+
+    } // namespace TestCaseTracking
+
+    using TestCaseTracking::ITracker;
+    using TestCaseTracking::SectionTracker;
+    using TestCaseTracking::TrackerContext;
+
+} // namespace Catch
+
+#if defined(__clang__)
+#pragma clang diagnostic pop
+#endif
+// end catch_test_case_tracker.cpp
+// start catch_test_registry.cpp
+
+namespace Catch {
+
+    auto makeTestInvoker(void (*testAsFunction)()) noexcept -> ITestInvoker* {
+        return new (std::nothrow) TestInvokerAsFunction(testAsFunction);
+    }
+
+    NameAndTags::NameAndTags(StringRef const& name_, StringRef const& tags_) noexcept :
+        name(name_), tags(tags_) {}
+
+    AutoReg::AutoReg(ITestInvoker* invoker,
+                     SourceLineInfo const& lineInfo,
+                     StringRef const& classOrMethod,
+                     NameAndTags const& nameAndTags) noexcept {
+        CATCH_TRY {
+            getMutableRegistryHub().registerTest(
+                makeTestCase(invoker, extractClassName(classOrMethod), nameAndTags, lineInfo));
+        }
+        CATCH_CATCH_ALL {
+            // Do not throw when constructing global objects, instead register
+            // the exception to be processed later
+            getMutableRegistryHub().registerStartupException();
+        }
+    }
+
+    AutoReg::~AutoReg() = default;
+} // namespace Catch
+// end catch_test_registry.cpp
+// start catch_test_spec.cpp
+
+#include <algorithm>
+#include <string>
+#include <vector>
+#include <memory>
+
+namespace Catch {
+
+    TestSpec::Pattern::Pattern(std::string const& name) : m_name(name) {}
+
+    TestSpec::Pattern::~Pattern() = default;
+
+    std::string const& TestSpec::Pattern::name() const { return m_name; }
+
+    TestSpec::NamePattern::NamePattern(std::string const& name, std::string const& filterString) :
+        Pattern(filterString), m_wildcardPattern(toLower(name), CaseSensitive::No) {}
+
+    bool TestSpec::NamePattern::matches(TestCaseInfo const& testCase) const {
+        return m_wildcardPattern.matches(testCase.name);
+    }
+
+    TestSpec::TagPattern::TagPattern(std::string const& tag, std::string const& filterString) :
+        Pattern(filterString), m_tag(toLower(tag)) {}
+
+    bool TestSpec::TagPattern::matches(TestCaseInfo const& testCase) const {
+        return std::find(begin(testCase.lcaseTags), end(testCase.lcaseTags), m_tag) !=
+               end(testCase.lcaseTags);
+    }
+
+    TestSpec::ExcludedPattern::ExcludedPattern(PatternPtr const& underlyingPattern) :
+        Pattern(underlyingPattern->name()), m_underlyingPattern(underlyingPattern) {}
+
+    bool TestSpec::ExcludedPattern::matches(TestCaseInfo const& testCase) const {
+        return !m_underlyingPattern->matches(testCase);
+    }
+
+    bool TestSpec::Filter::matches(TestCaseInfo const& testCase) const {
+        return std::all_of(m_patterns.begin(), m_patterns.end(), [&](PatternPtr const& p) {
+            return p->matches(testCase);
+        });
+    }
+
+    std::string TestSpec::Filter::name() const {
+        std::string name;
+        for (auto const& p : m_patterns)
+            name += p->name();
+        return name;
+    }
+
+    bool TestSpec::hasFilters() const { return !m_filters.empty(); }
+
+    bool TestSpec::matches(TestCaseInfo const& testCase) const {
+        return std::any_of(m_filters.begin(), m_filters.end(), [&](Filter const& f) {
+            return f.matches(testCase);
+        });
+    }
+
+    TestSpec::Matches TestSpec::matchesByFilter(std::vector<TestCase> const& testCases,
+                                                IConfig const& config) const {
+        Matches matches(m_filters.size());
+        std::transform(
+            m_filters.begin(), m_filters.end(), matches.begin(), [&](Filter const& filter) {
+                std::vector<TestCase const*> currentMatches;
+                for (auto const& test : testCases)
+                    if (isThrowSafe(test, config) && filter.matches(test))
+                        currentMatches.emplace_back(&test);
+                return FilterMatch{filter.name(), currentMatches};
+            });
+        return matches;
+    }
+
+    const TestSpec::vectorStrings& TestSpec::getInvalidArgs() const { return (m_invalidArgs); }
+
+} // namespace Catch
+// end catch_test_spec.cpp
+// start catch_test_spec_parser.cpp
+
+namespace Catch {
+
+    TestSpecParser::TestSpecParser(ITagAliasRegistry const& tagAliases) :
+        m_tagAliases(&tagAliases) {}
+
+    TestSpecParser& TestSpecParser::parse(std::string const& arg) {
+        m_mode = None;
+        m_exclusion = false;
+        m_arg = m_tagAliases->expandAliases(arg);
+        m_escapeChars.clear();
+        m_substring.reserve(m_arg.size());
+        m_patternName.reserve(m_arg.size());
+        m_realPatternPos = 0;
+
+        for (m_pos = 0; m_pos < m_arg.size(); ++m_pos)
+            // if visitChar fails
+            if (!visitChar(m_arg[m_pos])) {
+                m_testSpec.m_invalidArgs.push_back(arg);
+                break;
+            }
+        endMode();
+        return *this;
+    }
+    TestSpec TestSpecParser::testSpec() {
+        addFilter();
+        return m_testSpec;
+    }
+    bool TestSpecParser::visitChar(char c) {
+        if ((m_mode != EscapedName) && (c == '\\')) {
+            escape();
+            addCharToPattern(c);
+            return true;
+        } else if ((m_mode != EscapedName) && (c == ',')) {
+            return separate();
+        }
+
+        switch (m_mode) {
+            case None:
+                if (processNoneChar(c))
+                    return true;
+                break;
+            case Name:
+                processNameChar(c);
+                break;
+            case EscapedName:
+                endMode();
+                addCharToPattern(c);
+                return true;
+            default:
+            case Tag:
+            case QuotedName:
+                if (processOtherChar(c))
+                    return true;
+                break;
+        }
+
+        m_substring += c;
+        if (!isControlChar(c)) {
+            m_patternName += c;
+            m_realPatternPos++;
+        }
+        return true;
+    }
+    // Two of the processing methods return true to signal the caller to return
+    // without adding the given character to the current pattern strings
+    bool TestSpecParser::processNoneChar(char c) {
+        switch (c) {
+            case ' ':
+                return true;
+            case '~':
+                m_exclusion = true;
+                return false;
+            case '[':
+                startNewMode(Tag);
+                return false;
+            case '"':
+                startNewMode(QuotedName);
+                return false;
+            default:
+                startNewMode(Name);
+                return false;
+        }
+    }
+    void TestSpecParser::processNameChar(char c) {
+        if (c == '[') {
+            if (m_substring == "exclude:")
+                m_exclusion = true;
+            else
+                endMode();
+            startNewMode(Tag);
+        }
+    }
+    bool TestSpecParser::processOtherChar(char c) {
+        if (!isControlChar(c))
+            return false;
+        m_substring += c;
+        endMode();
+        return true;
+    }
+    void TestSpecParser::startNewMode(Mode mode) { m_mode = mode; }
+    void TestSpecParser::endMode() {
+        switch (m_mode) {
+            case Name:
+            case QuotedName:
+                return addNamePattern();
+            case Tag:
+                return addTagPattern();
+            case EscapedName:
+                revertBackToLastMode();
+                return;
+            case None:
+            default:
+                return startNewMode(None);
+        }
+    }
+    void TestSpecParser::escape() {
+        saveLastMode();
+        m_mode = EscapedName;
+        m_escapeChars.push_back(m_realPatternPos);
+    }
+    bool TestSpecParser::isControlChar(char c) const {
+        switch (m_mode) {
+            default:
+                return false;
+            case None:
+                return c == '~';
+            case Name:
+                return c == '[';
+            case EscapedName:
+                return true;
+            case QuotedName:
+                return c == '"';
+            case Tag:
+                return c == '[' || c == ']';
+        }
+    }
+
+    void TestSpecParser::addFilter() {
+        if (!m_currentFilter.m_patterns.empty()) {
+            m_testSpec.m_filters.push_back(m_currentFilter);
+            m_currentFilter = TestSpec::Filter();
+        }
+    }
+
+    void TestSpecParser::saveLastMode() { lastMode = m_mode; }
+
+    void TestSpecParser::revertBackToLastMode() { m_mode = lastMode; }
+
+    bool TestSpecParser::separate() {
+        if ((m_mode == QuotedName) || (m_mode == Tag)) {
+            // invalid argument, signal failure to previous scope.
+            m_mode = None;
+            m_pos = m_arg.size();
+            m_substring.clear();
+            m_patternName.clear();
+            m_realPatternPos = 0;
+            return false;
+        }
+        endMode();
+        addFilter();
+        return true; // success
+    }
+
+    std::string TestSpecParser::preprocessPattern() {
+        std::string token = m_patternName;
+        for (std::size_t i = 0; i < m_escapeChars.size(); ++i)
+            token = token.substr(0, m_escapeChars[i] - i) + token.substr(m_escapeChars[i] - i + 1);
+        m_escapeChars.clear();
+        if (startsWith(token, "exclude:")) {
+            m_exclusion = true;
+            token = token.substr(8);
+        }
+
+        m_patternName.clear();
+        m_realPatternPos = 0;
+
+        return token;
+    }
+
+    void TestSpecParser::addNamePattern() {
+        auto token = preprocessPattern();
+
+        if (!token.empty()) {
+            TestSpec::PatternPtr pattern =
+                std::make_shared<TestSpec::NamePattern>(token, m_substring);
+            if (m_exclusion)
+                pattern = std::make_shared<TestSpec::ExcludedPattern>(pattern);
+            m_currentFilter.m_patterns.push_back(pattern);
+        }
+        m_substring.clear();
+        m_exclusion = false;
+        m_mode = None;
+    }
+
+    void TestSpecParser::addTagPattern() {
+        auto token = preprocessPattern();
+
+        if (!token.empty()) {
+            // If the tag pattern is the "hide and tag" shorthand (e.g. [.foo])
+            // we have to create a separate hide tag and shorten the real one
+            if (token.size() > 1 && token[0] == '.') {
+                token.erase(token.begin());
+                TestSpec::PatternPtr pattern =
+                    std::make_shared<TestSpec::TagPattern>(".", m_substring);
+                if (m_exclusion) {
+                    pattern = std::make_shared<TestSpec::ExcludedPattern>(pattern);
+                }
+                m_currentFilter.m_patterns.push_back(pattern);
+            }
+
+            TestSpec::PatternPtr pattern =
+                std::make_shared<TestSpec::TagPattern>(token, m_substring);
+
+            if (m_exclusion) {
+                pattern = std::make_shared<TestSpec::ExcludedPattern>(pattern);
+            }
+            m_currentFilter.m_patterns.push_back(pattern);
+        }
+        m_substring.clear();
+        m_exclusion = false;
+        m_mode = None;
+    }
+
+    TestSpec parseTestSpec(std::string const& arg) {
+        return TestSpecParser(ITagAliasRegistry::get()).parse(arg).testSpec();
+    }
+
+} // namespace Catch
+// end catch_test_spec_parser.cpp
+// start catch_timer.cpp
+
+#include <chrono>
+
+static const uint64_t nanosecondsInSecond = 1000000000;
+
+namespace Catch {
+
+    auto getCurrentNanosecondsSinceEpoch() -> uint64_t {
+        return std::chrono::duration_cast<std::chrono::nanoseconds>(
+                   std::chrono::high_resolution_clock::now().time_since_epoch())
+            .count();
+    }
+
+    namespace {
+        auto estimateClockResolution() -> uint64_t {
+            uint64_t sum = 0;
+            static const uint64_t iterations = 1000000;
+
+            auto startTime = getCurrentNanosecondsSinceEpoch();
+
+            for (std::size_t i = 0; i < iterations; ++i) {
+
+                uint64_t ticks;
+                uint64_t baseTicks = getCurrentNanosecondsSinceEpoch();
+                do {
+                    ticks = getCurrentNanosecondsSinceEpoch();
+                } while (ticks == baseTicks);
+
+                auto delta = ticks - baseTicks;
+                sum += delta;
+
+                // If we have been calibrating for over 3 seconds -- the clock
+                // is terrible and we should move on.
+                // TBD: How to signal that the measured resolution is probably
+                // wrong?
+                if (ticks > startTime + 3 * nanosecondsInSecond) {
+                    return sum / (i + 1u);
+                }
+            }
+
+            // We're just taking the mean, here. To do better we could take the
+            // std. dev and exclude outliers
+            // - and potentially do more iterations if there's a high variance.
+            return sum / iterations;
+        }
+    } // namespace
+    auto getEstimatedClockResolution() -> uint64_t {
+        static auto s_resolution = estimateClockResolution();
+        return s_resolution;
+    }
+
+    void Timer::start() { m_nanoseconds = getCurrentNanosecondsSinceEpoch(); }
+    auto Timer::getElapsedNanoseconds() const -> uint64_t {
+        return getCurrentNanosecondsSinceEpoch() - m_nanoseconds;
+    }
+    auto Timer::getElapsedMicroseconds() const -> uint64_t {
+        return getElapsedNanoseconds() / 1000;
+    }
+    auto Timer::getElapsedMilliseconds() const -> unsigned int {
+        return static_cast<unsigned int>(getElapsedMicroseconds() / 1000);
+    }
+    auto Timer::getElapsedSeconds() const -> double { return getElapsedMicroseconds() / 1000000.0; }
+
+} // namespace Catch
+// end catch_timer.cpp
+// start catch_tostring.cpp
+
+#if defined(__clang__)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wexit-time-destructors"
+#pragma clang diagnostic ignored "-Wglobal-constructors"
+#endif
+
+// Enable specific decls locally
+#if !defined(CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER)
+#define CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER
+#endif
+
+#include <cmath>
+#include <iomanip>
+
+namespace Catch {
+
+    namespace Detail {
+
+        const std::string unprintableString = "{?}";
+
+        namespace {
+            const int hexThreshold = 255;
+
+            struct Endianness {
+                enum Arch { Big, Little };
+
+                static Arch which() {
+                    int one = 1;
+                    // If the lowest byte we read is non-zero, we can assume
+                    // that little endian format is used.
+                    auto value = *reinterpret_cast<char*>(&one);
+                    return value ? Little : Big;
+                }
+            };
+        } // namespace
+
+        std::string rawMemoryToString(const void* object, std::size_t size) {
+            // Reverse order for little endian architectures
+            int i = 0, end = static_cast<int>(size), inc = 1;
+            if (Endianness::which() == Endianness::Little) {
+                i = end - 1;
+                end = inc = -1;
+            }
+
+            unsigned char const* bytes = static_cast<unsigned char const*>(object);
+            ReusableStringStream rss;
+            rss << "0x" << std::setfill('0') << std::hex;
+            for (; i != end; i += inc)
+                rss << std::setw(2) << static_cast<unsigned>(bytes[i]);
+            return rss.str();
+        }
+    } // namespace Detail
+
+    template <typename T> std::string fpToString(T value, int precision) {
+        if (Catch::isnan(value)) {
+            return "nan";
+        }
+
+        ReusableStringStream rss;
+        rss << std::setprecision(precision) << std::fixed << value;
+        std::string d = rss.str();
+        std::size_t i = d.find_last_not_of('0');
+        if (i != std::string::npos && i != d.size() - 1) {
+            if (d[i] == '.')
+                i++;
+            d = d.substr(0, i + 1);
+        }
+        return d;
+    }
+
+    //// ======================================================= ////
+    //
+    //   Out-of-line defs for full specialization of StringMaker
+    //
+    //// ======================================================= ////
+
+    std::string StringMaker<std::string>::convert(const std::string& str) {
+        if (!getCurrentContext().getConfig()->showInvisibles()) {
+            return '"' + str + '"';
+        }
+
+        std::string s("\"");
+        for (char c : str) {
+            switch (c) {
+                case '\n':
+                    s.append("\\n");
+                    break;
+                case '\t':
+                    s.append("\\t");
+                    break;
+                default:
+                    s.push_back(c);
+                    break;
+            }
+        }
+        s.append("\"");
+        return s;
+    }
+
+#ifdef CATCH_CONFIG_CPP17_STRING_VIEW
+    std::string StringMaker<std::string_view>::convert(std::string_view str) {
+        return ::Catch::Detail::stringify(std::string{str});
+    }
+#endif
+
+    std::string StringMaker<char const*>::convert(char const* str) {
+        if (str) {
+            return ::Catch::Detail::stringify(std::string{str});
+        } else {
+            return {"{null string}"};
+        }
+    }
+    std::string StringMaker<char*>::convert(char* str) {
+        if (str) {
+            return ::Catch::Detail::stringify(std::string{str});
+        } else {
+            return {"{null string}"};
+        }
+    }
+
+#ifdef CATCH_CONFIG_WCHAR
+    std::string StringMaker<std::wstring>::convert(const std::wstring& wstr) {
+        std::string s;
+        s.reserve(wstr.size());
+        for (auto c : wstr) {
+            s += (c <= 0xff) ? static_cast<char>(c) : '?';
+        }
+        return ::Catch::Detail::stringify(s);
+    }
+
+#ifdef CATCH_CONFIG_CPP17_STRING_VIEW
+    std::string StringMaker<std::wstring_view>::convert(std::wstring_view str) {
+        return StringMaker<std::wstring>::convert(std::wstring(str));
+    }
+#endif
+
+    std::string StringMaker<wchar_t const*>::convert(wchar_t const* str) {
+        if (str) {
+            return ::Catch::Detail::stringify(std::wstring{str});
+        } else {
+            return {"{null string}"};
+        }
+    }
+    std::string StringMaker<wchar_t*>::convert(wchar_t* str) {
+        if (str) {
+            return ::Catch::Detail::stringify(std::wstring{str});
+        } else {
+            return {"{null string}"};
+        }
+    }
+#endif
+
+#if defined(CATCH_CONFIG_CPP17_BYTE)
+#include <cstddef>
+    std::string StringMaker<std::byte>::convert(std::byte value) {
+        return ::Catch::Detail::stringify(std::to_integer<unsigned long long>(value));
+    }
+#endif // defined(CATCH_CONFIG_CPP17_BYTE)
+
+    std::string StringMaker<int>::convert(int value) {
+        return ::Catch::Detail::stringify(static_cast<long long>(value));
+    }
+    std::string StringMaker<long>::convert(long value) {
+        return ::Catch::Detail::stringify(static_cast<long long>(value));
+    }
+    std::string StringMaker<long long>::convert(long long value) {
+        ReusableStringStream rss;
+        rss << value;
+        if (value > Detail::hexThreshold) {
+            rss << " (0x" << std::hex << value << ')';
+        }
+        return rss.str();
+    }
+
+    std::string StringMaker<unsigned int>::convert(unsigned int value) {
+        return ::Catch::Detail::stringify(static_cast<unsigned long long>(value));
+    }
+    std::string StringMaker<unsigned long>::convert(unsigned long value) {
+        return ::Catch::Detail::stringify(static_cast<unsigned long long>(value));
+    }
+    std::string StringMaker<unsigned long long>::convert(unsigned long long value) {
+        ReusableStringStream rss;
+        rss << value;
+        if (value > Detail::hexThreshold) {
+            rss << " (0x" << std::hex << value << ')';
+        }
+        return rss.str();
+    }
+
+    std::string StringMaker<bool>::convert(bool b) { return b ? "true" : "false"; }
+
+    std::string StringMaker<signed char>::convert(signed char value) {
+        if (value == '\r') {
+            return "'\\r'";
+        } else if (value == '\f') {
+            return "'\\f'";
+        } else if (value == '\n') {
+            return "'\\n'";
+        } else if (value == '\t') {
+            return "'\\t'";
+        } else if ('\0' <= value && value < ' ') {
+            return ::Catch::Detail::stringify(static_cast<unsigned int>(value));
+        } else {
+            char chstr[] = "' '";
+            chstr[1] = value;
+            return chstr;
+        }
+    }
+    std::string StringMaker<char>::convert(char c) {
+        return ::Catch::Detail::stringify(static_cast<signed char>(c));
+    }
+    std::string StringMaker<unsigned char>::convert(unsigned char c) {
+        return ::Catch::Detail::stringify(static_cast<char>(c));
+    }
+
+    std::string StringMaker<std::nullptr_t>::convert(std::nullptr_t) { return "nullptr"; }
+
+    int StringMaker<float>::precision = 5;
+
+    std::string StringMaker<float>::convert(float value) {
+        return fpToString(value, precision) + 'f';
+    }
+
+    int StringMaker<double>::precision = 10;
+
+    std::string StringMaker<double>::convert(double value) { return fpToString(value, precision); }
+
+    std::string ratio_string<std::atto>::symbol() { return "a"; }
+    std::string ratio_string<std::femto>::symbol() { return "f"; }
+    std::string ratio_string<std::pico>::symbol() { return "p"; }
+    std::string ratio_string<std::nano>::symbol() { return "n"; }
+    std::string ratio_string<std::micro>::symbol() { return "u"; }
+    std::string ratio_string<std::milli>::symbol() { return "m"; }
+
+} // end namespace Catch
+
+#if defined(__clang__)
+#pragma clang diagnostic pop
+#endif
+
+// end catch_tostring.cpp
+// start catch_totals.cpp
+
+namespace Catch {
+
+    Counts Counts::operator-(Counts const& other) const {
+        Counts diff;
+        diff.passed = passed - other.passed;
+        diff.failed = failed - other.failed;
+        diff.failedButOk = failedButOk - other.failedButOk;
+        return diff;
+    }
+
+    Counts& Counts::operator+=(Counts const& other) {
+        passed += other.passed;
+        failed += other.failed;
+        failedButOk += other.failedButOk;
+        return *this;
+    }
+
+    std::size_t Counts::total() const { return passed + failed + failedButOk; }
+    bool Counts::allPassed() const { return failed == 0 && failedButOk == 0; }
+    bool Counts::allOk() const { return failed == 0; }
+
+    Totals Totals::operator-(Totals const& other) const {
+        Totals diff;
+        diff.assertions = assertions - other.assertions;
+        diff.testCases = testCases - other.testCases;
+        return diff;
+    }
+
+    Totals& Totals::operator+=(Totals const& other) {
+        assertions += other.assertions;
+        testCases += other.testCases;
+        return *this;
+    }
+
+    Totals Totals::delta(Totals const& prevTotals) const {
+        Totals diff = *this - prevTotals;
+        if (diff.assertions.failed > 0)
+            ++diff.testCases.failed;
+        else if (diff.assertions.failedButOk > 0)
+            ++diff.testCases.failedButOk;
+        else
+            ++diff.testCases.passed;
+        return diff;
+    }
+
+} // namespace Catch
+// end catch_totals.cpp
+// start catch_uncaught_exceptions.cpp
+
+// start catch_config_uncaught_exceptions.hpp
+
+//              Copyright Catch2 Authors
+// Distributed under the Boost Software License, Version 1.0.
+//   (See accompanying file LICENSE_1_0.txt or copy at
+//        https://www.boost.org/LICENSE_1_0.txt)
+
+// SPDX-License-Identifier: BSL-1.0
+
+#ifndef CATCH_CONFIG_UNCAUGHT_EXCEPTIONS_HPP
+#define CATCH_CONFIG_UNCAUGHT_EXCEPTIONS_HPP
+
+#if defined(_MSC_VER)
+#if _MSC_VER >= 1900 // Visual Studio 2015 or newer
+#define CATCH_INTERNAL_CONFIG_CPP17_UNCAUGHT_EXCEPTIONS
+#endif
+#endif
+
+#include <exception>
+
+#if defined(__cpp_lib_uncaught_exceptions) &&                                                      \
+    !defined(CATCH_INTERNAL_CONFIG_CPP17_UNCAUGHT_EXCEPTIONS)
+
+#define CATCH_INTERNAL_CONFIG_CPP17_UNCAUGHT_EXCEPTIONS
+#endif // __cpp_lib_uncaught_exceptions
+
+#if defined(CATCH_INTERNAL_CONFIG_CPP17_UNCAUGHT_EXCEPTIONS) &&                                    \
+    !defined(CATCH_CONFIG_NO_CPP17_UNCAUGHT_EXCEPTIONS) &&                                         \
+    !defined(CATCH_CONFIG_CPP17_UNCAUGHT_EXCEPTIONS)
+
+#define CATCH_CONFIG_CPP17_UNCAUGHT_EXCEPTIONS
+#endif
+
+#endif // CATCH_CONFIG_UNCAUGHT_EXCEPTIONS_HPP
+// end catch_config_uncaught_exceptions.hpp
+#include <exception>
+
+namespace Catch {
+    bool uncaught_exceptions() {
+#if defined(CATCH_CONFIG_DISABLE_EXCEPTIONS)
+        return false;
+#elif defined(CATCH_CONFIG_CPP17_UNCAUGHT_EXCEPTIONS)
+        return std::uncaught_exceptions() > 0;
+#else
+        return std::uncaught_exception();
+#endif
+    }
+} // end namespace Catch
+// end catch_uncaught_exceptions.cpp
+// start catch_version.cpp
+
+#include <ostream>
+
+namespace Catch {
+
+    Version::Version(unsigned int _majorVersion,
+                     unsigned int _minorVersion,
+                     unsigned int _patchNumber,
+                     char const* const _branchName,
+                     unsigned int _buildNumber) :
+        majorVersion(_majorVersion),
+        minorVersion(_minorVersion),
+        patchNumber(_patchNumber),
+        branchName(_branchName),
+        buildNumber(_buildNumber) {}
+
+    std::ostream& operator<<(std::ostream& os, Version const& version) {
+        os << version.majorVersion << '.' << version.minorVersion << '.' << version.patchNumber;
+        // branchName is never null -> 0th char is \0 if it is empty
+        if (version.branchName[0]) {
+            os << '-' << version.branchName << '.' << version.buildNumber;
+        }
+        return os;
+    }
+
+    Version const& libraryVersion() {
+        static Version version(2, 13, 7, "", 0);
+        return version;
+    }
+
+} // namespace Catch
+// end catch_version.cpp
+// start catch_wildcard_pattern.cpp
+
+namespace Catch {
+
+    WildcardPattern::WildcardPattern(std::string const& pattern,
+                                     CaseSensitive::Choice caseSensitivity) :
+        m_caseSensitivity(caseSensitivity), m_pattern(normaliseString(pattern)) {
+        if (startsWith(m_pattern, '*')) {
+            m_pattern = m_pattern.substr(1);
+            m_wildcard = WildcardAtStart;
+        }
+        if (endsWith(m_pattern, '*')) {
+            m_pattern = m_pattern.substr(0, m_pattern.size() - 1);
+            m_wildcard = static_cast<WildcardPosition>(m_wildcard | WildcardAtEnd);
+        }
+    }
+
+    bool WildcardPattern::matches(std::string const& str) const {
+        switch (m_wildcard) {
+            case NoWildcard:
+                return m_pattern == normaliseString(str);
+            case WildcardAtStart:
+                return endsWith(normaliseString(str), m_pattern);
+            case WildcardAtEnd:
+                return startsWith(normaliseString(str), m_pattern);
+            case WildcardAtBothEnds:
+                return contains(normaliseString(str), m_pattern);
+            default:
+                CATCH_INTERNAL_ERROR("Unknown enum");
+        }
+    }
+
+    std::string WildcardPattern::normaliseString(std::string const& str) const {
+        return trim(m_caseSensitivity == CaseSensitive::No ? toLower(str) : str);
+    }
+} // namespace Catch
+// end catch_wildcard_pattern.cpp
+// start catch_xmlwriter.cpp
+
+#include <iomanip>
+#include <type_traits>
+
+namespace Catch {
+
+    namespace {
+
+        size_t trailingBytes(unsigned char c) {
+            if ((c & 0xE0) == 0xC0) {
+                return 2;
+            }
+            if ((c & 0xF0) == 0xE0) {
+                return 3;
+            }
+            if ((c & 0xF8) == 0xF0) {
+                return 4;
+            }
+            CATCH_INTERNAL_ERROR("Invalid multibyte utf-8 start byte encountered");
+        }
+
+        uint32_t headerValue(unsigned char c) {
+            if ((c & 0xE0) == 0xC0) {
+                return c & 0x1F;
+            }
+            if ((c & 0xF0) == 0xE0) {
+                return c & 0x0F;
+            }
+            if ((c & 0xF8) == 0xF0) {
+                return c & 0x07;
+            }
+            CATCH_INTERNAL_ERROR("Invalid multibyte utf-8 start byte encountered");
+        }
+
+        void hexEscapeChar(std::ostream& os, unsigned char c) {
+            std::ios_base::fmtflags f(os.flags());
+            os << "\\x" << std::uppercase << std::hex << std::setfill('0') << std::setw(2)
+               << static_cast<int>(c);
+            os.flags(f);
+        }
+
+        bool shouldNewline(XmlFormatting fmt) {
+            return !!(static_cast<std::underlying_type<XmlFormatting>::type>(
+                fmt & XmlFormatting::Newline));
+        }
+
+        bool shouldIndent(XmlFormatting fmt) {
+            return !!(static_cast<std::underlying_type<XmlFormatting>::type>(
+                fmt & XmlFormatting::Indent));
+        }
+
+    } // anonymous namespace
+
+    XmlFormatting operator|(XmlFormatting lhs, XmlFormatting rhs) {
+        return static_cast<XmlFormatting>(
+            static_cast<std::underlying_type<XmlFormatting>::type>(lhs) |
+            static_cast<std::underlying_type<XmlFormatting>::type>(rhs));
+    }
+
+    XmlFormatting operator&(XmlFormatting lhs, XmlFormatting rhs) {
+        return static_cast<XmlFormatting>(
+            static_cast<std::underlying_type<XmlFormatting>::type>(lhs) &
+            static_cast<std::underlying_type<XmlFormatting>::type>(rhs));
+    }
+
+    XmlEncode::XmlEncode(std::string const& str, ForWhat forWhat) :
+        m_str(str), m_forWhat(forWhat) {}
+
+    void XmlEncode::encodeTo(std::ostream& os) const {
+        // Apostrophe escaping not necessary if we always use " to write
+        // attributes (see: http://www.w3.org/TR/xml/#syntax)
+
+        for (std::size_t idx = 0; idx < m_str.size(); ++idx) {
+            unsigned char c = m_str[idx];
+            switch (c) {
+                case '<':
+                    os << "&lt;";
+                    break;
+                case '&':
+                    os << "&amp;";
+                    break;
+
+                case '>':
+                    // See: http://www.w3.org/TR/xml/#syntax
+                    if (idx > 2 && m_str[idx - 1] == ']' && m_str[idx - 2] == ']')
+                        os << "&gt;";
+                    else
+                        os << c;
+                    break;
+
+                case '\"':
+                    if (m_forWhat == ForAttributes)
+                        os << "&quot;";
+                    else
+                        os << c;
+                    break;
+
+                default:
+                    // Check for control characters and invalid utf-8
+
+                    // Escape control characters in standard ascii
+                    // see
+                    // http://stackoverflow.com/questions/404107/why-are-control-characters-illegal-in-xml-1-0
+                    if (c < 0x09 || (c > 0x0D && c < 0x20) || c == 0x7F) {
+                        hexEscapeChar(os, c);
+                        break;
+                    }
+
+                    // Plain ASCII: Write it to stream
+                    if (c < 0x7F) {
+                        os << c;
+                        break;
+                    }
+
+                    // UTF-8 territory
+                    // Check if the encoding is valid and if it is not, hex
+                    // escape bytes. Important: We do not check the exact
+                    // decoded values for validity, only the encoding format
+                    // First check that this bytes is a valid lead byte:
+                    // This means that it is not encoded as 1111 1XXX
+                    // Or as 10XX XXXX
+                    if (c < 0xC0 || c >= 0xF8) {
+                        hexEscapeChar(os, c);
+                        break;
+                    }
+
+                    auto encBytes = trailingBytes(c);
+                    // Are there enough bytes left to avoid accessing
+                    // out-of-bounds memory?
+                    if (idx + encBytes - 1 >= m_str.size()) {
+                        hexEscapeChar(os, c);
+                        break;
+                    }
+                    // The header is valid, check data
+                    // The next encBytes bytes must together be a valid utf-8
+                    // This means: bitpattern 10XX XXXX and the extracted value
+                    // is sane (ish)
+                    bool valid = true;
+                    uint32_t value = headerValue(c);
+                    for (std::size_t n = 1; n < encBytes; ++n) {
+                        unsigned char nc = m_str[idx + n];
+                        valid &= ((nc & 0xC0) == 0x80);
+                        value = (value << 6) | (nc & 0x3F);
+                    }
+
+                    if (
+                        // Wrong bit pattern of following bytes
+                        (!valid) ||
+                        // Overlong encodings
+                        (value < 0x80) || (0x80 <= value && value < 0x800 && encBytes > 2) ||
+                        (0x800 < value && value < 0x10000 && encBytes > 3) ||
+                        // Encoded value out of range
+                        (value >= 0x110000))
+                    {
+                        hexEscapeChar(os, c);
+                        break;
+                    }
+
+                    // If we got here, this is in fact a valid(ish) utf-8
+                    // sequence
+                    for (std::size_t n = 0; n < encBytes; ++n) {
+                        os << m_str[idx + n];
+                    }
+                    idx += encBytes - 1;
+                    break;
+            }
+        }
+    }
+
+    std::ostream& operator<<(std::ostream& os, XmlEncode const& xmlEncode) {
+        xmlEncode.encodeTo(os);
+        return os;
+    }
+
+    XmlWriter::ScopedElement::ScopedElement(XmlWriter* writer, XmlFormatting fmt) :
+        m_writer(writer), m_fmt(fmt) {}
+
+    XmlWriter::ScopedElement::ScopedElement(ScopedElement&& other) noexcept :
+        m_writer(other.m_writer), m_fmt(other.m_fmt) {
+        other.m_writer = nullptr;
+        other.m_fmt = XmlFormatting::None;
+    }
+    XmlWriter::ScopedElement& XmlWriter::ScopedElement::operator=(ScopedElement&& other) noexcept {
+        if (m_writer) {
+            m_writer->endElement();
+        }
+        m_writer = other.m_writer;
+        other.m_writer = nullptr;
+        m_fmt = other.m_fmt;
+        other.m_fmt = XmlFormatting::None;
+        return *this;
+    }
+
+    XmlWriter::ScopedElement::~ScopedElement() {
+        if (m_writer) {
+            m_writer->endElement(m_fmt);
+        }
+    }
+
+    XmlWriter::ScopedElement& XmlWriter::ScopedElement::writeText(std::string const& text,
+                                                                  XmlFormatting fmt) {
+        m_writer->writeText(text, fmt);
+        return *this;
+    }
+
+    XmlWriter::XmlWriter(std::ostream& os) : m_os(os) { writeDeclaration(); }
+
+    XmlWriter::~XmlWriter() {
+        while (!m_tags.empty()) {
+            endElement();
+        }
+        newlineIfNecessary();
+    }
+
+    XmlWriter& XmlWriter::startElement(std::string const& name, XmlFormatting fmt) {
+        ensureTagClosed();
+        newlineIfNecessary();
+        if (shouldIndent(fmt)) {
+            m_os << m_indent;
+            m_indent += "  ";
+        }
+        m_os << '<' << name;
+        m_tags.push_back(name);
+        m_tagIsOpen = true;
+        applyFormatting(fmt);
+        return *this;
+    }
+
+    XmlWriter::ScopedElement XmlWriter::scopedElement(std::string const& name, XmlFormatting fmt) {
+        ScopedElement scoped(this, fmt);
+        startElement(name, fmt);
+        return scoped;
+    }
+
+    XmlWriter& XmlWriter::endElement(XmlFormatting fmt) {
+        m_indent = m_indent.substr(0, m_indent.size() - 2);
+
+        if (m_tagIsOpen) {
+            m_os << "/>";
+            m_tagIsOpen = false;
+        } else {
+            newlineIfNecessary();
+            if (shouldIndent(fmt)) {
+                m_os << m_indent;
+            }
+            m_os << "</" << m_tags.back() << ">";
+        }
+        m_os << std::flush;
+        applyFormatting(fmt);
+        m_tags.pop_back();
+        return *this;
+    }
+
+    XmlWriter& XmlWriter::writeAttribute(std::string const& name, std::string const& attribute) {
+        if (!name.empty() && !attribute.empty())
+            m_os << ' ' << name << "=\"" << XmlEncode(attribute, XmlEncode::ForAttributes) << '"';
+        return *this;
+    }
+
+    XmlWriter& XmlWriter::writeAttribute(std::string const& name, bool attribute) {
+        m_os << ' ' << name << "=\"" << (attribute ? "true" : "false") << '"';
+        return *this;
+    }
+
+    XmlWriter& XmlWriter::writeText(std::string const& text, XmlFormatting fmt) {
+        if (!text.empty()) {
+            bool tagWasOpen = m_tagIsOpen;
+            ensureTagClosed();
+            if (tagWasOpen && shouldIndent(fmt)) {
+                m_os << m_indent;
+            }
+            m_os << XmlEncode(text);
+            applyFormatting(fmt);
+        }
+        return *this;
+    }
+
+    XmlWriter& XmlWriter::writeComment(std::string const& text, XmlFormatting fmt) {
+        ensureTagClosed();
+        if (shouldIndent(fmt)) {
+            m_os << m_indent;
+        }
+        m_os << "<!--" << text << "-->";
+        applyFormatting(fmt);
+        return *this;
+    }
+
+    void XmlWriter::writeStylesheetRef(std::string const& url) {
+        m_os << "<?xml-stylesheet type=\"text/xsl\" href=\"" << url << "\"?>\n";
+    }
+
+    XmlWriter& XmlWriter::writeBlankLine() {
+        ensureTagClosed();
+        m_os << '\n';
+        return *this;
+    }
+
+    void XmlWriter::ensureTagClosed() {
+        if (m_tagIsOpen) {
+            m_os << '>' << std::flush;
+            newlineIfNecessary();
+            m_tagIsOpen = false;
+        }
+    }
+
+    void XmlWriter::applyFormatting(XmlFormatting fmt) { m_needsNewline = shouldNewline(fmt); }
+
+    void XmlWriter::writeDeclaration() { m_os << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; }
+
+    void XmlWriter::newlineIfNecessary() {
+        if (m_needsNewline) {
+            m_os << std::endl;
+            m_needsNewline = false;
+        }
+    }
+} // namespace Catch
+// end catch_xmlwriter.cpp
+// start catch_reporter_bases.cpp
+
+#include <cstring>
+#include <cfloat>
+#include <cstdio>
+#include <cassert>
+#include <memory>
+
+namespace Catch {
+    void prepareExpandedExpression(AssertionResult& result) { result.getExpandedExpression(); }
+
+    // Because formatting using c++ streams is stateful, drop down to C is
+    // required Alternatively we could use stringstream, but its performance
+    // is... not good.
+    std::string getFormattedDuration(double duration) {
+        // Max exponent + 1 is required to represent the whole part
+        // + 1 for decimal point
+        // + 3 for the 3 decimal places
+        // + 1 for null terminator
+        const std::size_t maxDoubleSize = DBL_MAX_10_EXP + 1 + 1 + 3 + 1;
+        char buffer[maxDoubleSize];
+
+        // Save previous errno, to prevent sprintf from overwriting it
+        ErrnoGuard guard;
+#ifdef _MSC_VER
+        sprintf_s(buffer, "%.3f", duration);
+#else
+        std::sprintf(buffer, "%.3f", duration);
+#endif
+        return std::string(buffer);
+    }
+
+    bool shouldShowDuration(IConfig const& config, double duration) {
+        if (config.showDurations() == ShowDurations::Always) {
+            return true;
+        }
+        if (config.showDurations() == ShowDurations::Never) {
+            return false;
+        }
+        const double min = config.minDuration();
+        return min >= 0 && duration >= min;
+    }
+
+    std::string serializeFilters(std::vector<std::string> const& container) {
+        ReusableStringStream oss;
+        bool first = true;
+        for (auto&& filter : container) {
+            if (!first)
+                oss << ' ';
+            else
+                first = false;
+
+            oss << filter;
+        }
+        return oss.str();
+    }
+
+    TestEventListenerBase::TestEventListenerBase(ReporterConfig const& _config) :
+        StreamingReporterBase(_config) {}
+
+    std::set<Verbosity> TestEventListenerBase::getSupportedVerbosities() {
+        return {Verbosity::Quiet, Verbosity::Normal, Verbosity::High};
+    }
+
+    void TestEventListenerBase::assertionStarting(AssertionInfo const&) {}
+
+    bool TestEventListenerBase::assertionEnded(AssertionStats const&) { return false; }
+
+} // end namespace Catch
+// end catch_reporter_bases.cpp
+// start catch_reporter_compact.cpp
+
+namespace {
+
+#ifdef CATCH_PLATFORM_MAC
+    const char* failedString() { return "FAILED"; }
+    const char* passedString() { return "PASSED"; }
+#else
+    const char* failedString() { return "failed"; }
+    const char* passedString() { return "passed"; }
+#endif
+
+    // Colour::LightGrey
+    Catch::Colour::Code dimColour() { return Catch::Colour::FileName; }
+
+    std::string bothOrAll(std::size_t count) {
+        return count == 1 ? std::string() : count == 2 ? "both " : "all ";
+    }
+
+} // namespace
+
+namespace Catch {
+    namespace {
+        // Colour, message variants:
+        // - white: No tests ran.
+        // -   red: Failed [both/all] N test cases, failed [both/all] M
+        // assertions.
+        // - white: Passed [both/all] N test cases (no assertions).
+        // -   red: Failed N tests cases, failed M assertions.
+        // - green: Passed [both/all] N tests cases with M assertions.
+        void printTotals(std::ostream& out, const Totals& totals) {
+            if (totals.testCases.total() == 0) {
+                out << "No tests ran.";
+            } else if (totals.testCases.failed == totals.testCases.total()) {
+                Colour colour(Colour::ResultError);
+                const std::string qualify_assertions_failed =
+                    totals.assertions.failed == totals.assertions.total()
+                        ? bothOrAll(totals.assertions.failed)
+                        : std::string();
+                out << "Failed " << bothOrAll(totals.testCases.failed)
+                    << pluralise(totals.testCases.failed, "test case")
+                    << ", "
+                       "failed "
+                    << qualify_assertions_failed << pluralise(totals.assertions.failed, "assertion")
+                    << '.';
+            } else if (totals.assertions.total() == 0) {
+                out << "Passed " << bothOrAll(totals.testCases.total())
+                    << pluralise(totals.testCases.total(), "test case") << " (no assertions).";
+            } else if (totals.assertions.failed) {
+                Colour colour(Colour::ResultError);
+                out << "Failed " << pluralise(totals.testCases.failed, "test case")
+                    << ", "
+                       "failed "
+                    << pluralise(totals.assertions.failed, "assertion") << '.';
+            } else {
+                Colour colour(Colour::ResultSuccess);
+                out << "Passed " << bothOrAll(totals.testCases.passed)
+                    << pluralise(totals.testCases.passed, "test case") << " with "
+                    << pluralise(totals.assertions.passed, "assertion") << '.';
+            }
+        }
+
+        // Implementation of CompactReporter formatting
+        class AssertionPrinter {
+        public:
+            AssertionPrinter& operator=(AssertionPrinter const&) = delete;
+            AssertionPrinter(AssertionPrinter const&) = delete;
+            AssertionPrinter(std::ostream& _stream,
+                             AssertionStats const& _stats,
+                             bool _printInfoMessages) :
+                stream(_stream),
+                result(_stats.assertionResult),
+                messages(_stats.infoMessages),
+                itMessage(_stats.infoMessages.begin()),
+                printInfoMessages(_printInfoMessages) {}
+
+            void print() {
+                printSourceInfo();
+
+                itMessage = messages.begin();
+
+                switch (result.getResultType()) {
+                    case ResultWas::Ok:
+                        printResultType(Colour::ResultSuccess, passedString());
+                        printOriginalExpression();
+                        printReconstructedExpression();
+                        if (!result.hasExpression())
+                            printRemainingMessages(Colour::None);
+                        else
+                            printRemainingMessages();
+                        break;
+                    case ResultWas::ExpressionFailed:
+                        if (result.isOk())
+                            printResultType(Colour::ResultSuccess,
+                                            failedString() + std::string(" - but was ok"));
+                        else
+                            printResultType(Colour::Error, failedString());
+                        printOriginalExpression();
+                        printReconstructedExpression();
+                        printRemainingMessages();
+                        break;
+                    case ResultWas::ThrewException:
+                        printResultType(Colour::Error, failedString());
+                        printIssue("unexpected exception with message:");
+                        printMessage();
+                        printExpressionWas();
+                        printRemainingMessages();
+                        break;
+                    case ResultWas::FatalErrorCondition:
+                        printResultType(Colour::Error, failedString());
+                        printIssue("fatal error condition with message:");
+                        printMessage();
+                        printExpressionWas();
+                        printRemainingMessages();
+                        break;
+                    case ResultWas::DidntThrowException:
+                        printResultType(Colour::Error, failedString());
+                        printIssue("expected exception, got none");
+                        printExpressionWas();
+                        printRemainingMessages();
+                        break;
+                    case ResultWas::Info:
+                        printResultType(Colour::None, "info");
+                        printMessage();
+                        printRemainingMessages();
+                        break;
+                    case ResultWas::Warning:
+                        printResultType(Colour::None, "warning");
+                        printMessage();
+                        printRemainingMessages();
+                        break;
+                    case ResultWas::ExplicitFailure:
+                        printResultType(Colour::Error, failedString());
+                        printIssue("explicitly");
+                        printRemainingMessages(Colour::None);
+                        break;
+                        // These cases are here to prevent compiler warnings
+                    case ResultWas::Unknown:
+                    case ResultWas::FailureBit:
+                    case ResultWas::Exception:
+                        printResultType(Colour::Error, "** internal error **");
+                        break;
+                }
+            }
+
+        private:
+            void printSourceInfo() const {
+                Colour colourGuard(Colour::FileName);
+                stream << result.getSourceInfo() << ':';
+            }
+
+            void printResultType(Colour::Code colour, std::string const& passOrFail) const {
+                if (!passOrFail.empty()) {
+                    {
+                        Colour colourGuard(colour);
+                        stream << ' ' << passOrFail;
+                    }
+                    stream << ':';
+                }
+            }
+
+            void printIssue(std::string const& issue) const { stream << ' ' << issue; }
+
+            void printExpressionWas() {
+                if (result.hasExpression()) {
+                    stream << ';';
+                    {
+                        Colour colour(dimColour());
+                        stream << " expression was:";
+                    }
+                    printOriginalExpression();
+                }
+            }
+
+            void printOriginalExpression() const {
+                if (result.hasExpression()) {
+                    stream << ' ' << result.getExpression();
+                }
+            }
+
+            void printReconstructedExpression() const {
+                if (result.hasExpandedExpression()) {
+                    {
+                        Colour colour(dimColour());
+                        stream << " for: ";
+                    }
+                    stream << result.getExpandedExpression();
+                }
+            }
+
+            void printMessage() {
+                if (itMessage != messages.end()) {
+                    stream << " '" << itMessage->message << '\'';
+                    ++itMessage;
+                }
+            }
+
+            void printRemainingMessages(Colour::Code colour = dimColour()) {
+                if (itMessage == messages.end())
+                    return;
+
+                const auto itEnd = messages.cend();
+                const auto N = static_cast<std::size_t>(std::distance(itMessage, itEnd));
+
+                {
+                    Colour colourGuard(colour);
+                    stream << " with " << pluralise(N, "message") << ':';
+                }
+
+                while (itMessage != itEnd) {
+                    // If this assertion is a warning ignore any INFO messages
+                    if (printInfoMessages || itMessage->type != ResultWas::Info) {
+                        printMessage();
+                        if (itMessage != itEnd) {
+                            Colour colourGuard(dimColour());
+                            stream << " and";
+                        }
+                        continue;
+                    }
+                    ++itMessage;
+                }
+            }
+
+        private:
+            std::ostream& stream;
+            AssertionResult const& result;
+            std::vector<MessageInfo> messages;
+            std::vector<MessageInfo>::const_iterator itMessage;
+            bool printInfoMessages;
+        };
+
+    } // namespace
+
+    std::string CompactReporter::getDescription() {
+        return "Reports test results on a single line, suitable for IDEs";
+    }
+
+    void CompactReporter::noMatchingTestCases(std::string const& spec) {
+        stream << "No test cases matched '" << spec << '\'' << std::endl;
+    }
+
+    void CompactReporter::assertionStarting(AssertionInfo const&) {}
+
+    bool CompactReporter::assertionEnded(AssertionStats const& _assertionStats) {
+        AssertionResult const& result = _assertionStats.assertionResult;
+
+        bool printInfoMessages = true;
+
+        // Drop out if result was successful and we're not printing those
+        if (!m_config->includeSuccessfulResults() && result.isOk()) {
+            if (result.getResultType() != ResultWas::Warning)
+                return false;
+            printInfoMessages = false;
+        }
+
+        AssertionPrinter printer(stream, _assertionStats, printInfoMessages);
+        printer.print();
+
+        stream << std::endl;
+        return true;
+    }
+
+    void CompactReporter::sectionEnded(SectionStats const& _sectionStats) {
+        double dur = _sectionStats.durationInSeconds;
+        if (shouldShowDuration(*m_config, dur)) {
+            stream << getFormattedDuration(dur) << " s: " << _sectionStats.sectionInfo.name
+                   << std::endl;
+        }
+    }
+
+    void CompactReporter::testRunEnded(TestRunStats const& _testRunStats) {
+        printTotals(stream, _testRunStats.totals);
+        stream << '\n' << std::endl;
+        StreamingReporterBase::testRunEnded(_testRunStats);
+    }
+
+    CompactReporter::~CompactReporter() {}
+
+    CATCH_REGISTER_REPORTER("compact", CompactReporter)
+
+} // end namespace Catch
+// end catch_reporter_compact.cpp
+// start catch_reporter_console.cpp
+
+#include <cfloat>
+#include <cstdio>
+
+#if defined(_MSC_VER)
+#pragma warning(push)
+#pragma warning(disable : 4061) // Not all labels are EXPLICITLY handled in switch
+  // Note that 4062 (not all labels are handled and default is missing) is
+  // enabled
+#endif
+
+#if defined(__clang__)
+#pragma clang diagnostic push
+// For simplicity, benchmarking-only helpers are always enabled
+#pragma clang diagnostic ignored "-Wunused-function"
+#endif
+
+namespace Catch {
+
+    namespace {
+
+        // Formatter impl for ConsoleReporter
+        class ConsoleAssertionPrinter {
+        public:
+            ConsoleAssertionPrinter& operator=(ConsoleAssertionPrinter const&) = delete;
+            ConsoleAssertionPrinter(ConsoleAssertionPrinter const&) = delete;
+            ConsoleAssertionPrinter(std::ostream& _stream,
+                                    AssertionStats const& _stats,
+                                    bool _printInfoMessages) :
+                stream(_stream),
+                stats(_stats),
+                result(_stats.assertionResult),
+                colour(Colour::None),
+                message(result.getMessage()),
+                messages(_stats.infoMessages),
+                printInfoMessages(_printInfoMessages) {
+                switch (result.getResultType()) {
+                    case ResultWas::Ok:
+                        colour = Colour::Success;
+                        passOrFail = "PASSED";
+                        // if( result.hasMessage() )
+                        if (_stats.infoMessages.size() == 1)
+                            messageLabel = "with message";
+                        if (_stats.infoMessages.size() > 1)
+                            messageLabel = "with messages";
+                        break;
+                    case ResultWas::ExpressionFailed:
+                        if (result.isOk()) {
+                            colour = Colour::Success;
+                            passOrFail = "FAILED - but was ok";
+                        } else {
+                            colour = Colour::Error;
+                            passOrFail = "FAILED";
+                        }
+                        if (_stats.infoMessages.size() == 1)
+                            messageLabel = "with message";
+                        if (_stats.infoMessages.size() > 1)
+                            messageLabel = "with messages";
+                        break;
+                    case ResultWas::ThrewException:
+                        colour = Colour::Error;
+                        passOrFail = "FAILED";
+                        messageLabel = "due to unexpected exception with ";
+                        if (_stats.infoMessages.size() == 1)
+                            messageLabel += "message";
+                        if (_stats.infoMessages.size() > 1)
+                            messageLabel += "messages";
+                        break;
+                    case ResultWas::FatalErrorCondition:
+                        colour = Colour::Error;
+                        passOrFail = "FAILED";
+                        messageLabel = "due to a fatal error condition";
+                        break;
+                    case ResultWas::DidntThrowException:
+                        colour = Colour::Error;
+                        passOrFail = "FAILED";
+                        messageLabel = "because no exception was thrown where "
+                                       "one was expected";
+                        break;
+                    case ResultWas::Info:
+                        messageLabel = "info";
+                        break;
+                    case ResultWas::Warning:
+                        messageLabel = "warning";
+                        break;
+                    case ResultWas::ExplicitFailure:
+                        passOrFail = "FAILED";
+                        colour = Colour::Error;
+                        if (_stats.infoMessages.size() == 1)
+                            messageLabel = "explicitly with message";
+                        if (_stats.infoMessages.size() > 1)
+                            messageLabel = "explicitly with messages";
+                        break;
+                        // These cases are here to prevent compiler warnings
+                    case ResultWas::Unknown:
+                    case ResultWas::FailureBit:
+                    case ResultWas::Exception:
+                        passOrFail = "** internal error **";
+                        colour = Colour::Error;
+                        break;
+                }
+            }
+
+            void print() const {
+                printSourceInfo();
+                if (stats.totals.assertions.total() > 0) {
+                    printResultType();
+                    printOriginalExpression();
+                    printReconstructedExpression();
+                } else {
+                    stream << '\n';
+                }
+                printMessage();
+            }
+
+        private:
+            void printResultType() const {
+                if (!passOrFail.empty()) {
+                    Colour colourGuard(colour);
+                    stream << passOrFail << ":\n";
+                }
+            }
+            void printOriginalExpression() const {
+                if (result.hasExpression()) {
+                    Colour colourGuard(Colour::OriginalExpression);
+                    stream << "  ";
+                    stream << result.getExpressionInMacro();
+                    stream << '\n';
+                }
+            }
+            void printReconstructedExpression() const {
+                if (result.hasExpandedExpression()) {
+                    stream << "with expansion:\n";
+                    Colour colourGuard(Colour::ReconstructedExpression);
+                    stream << Column(result.getExpandedExpression()).indent(2) << '\n';
+                }
+            }
+            void printMessage() const {
+                if (!messageLabel.empty())
+                    stream << messageLabel << ':' << '\n';
+                for (auto const& msg : messages) {
+                    // If this assertion is a warning ignore any INFO messages
+                    if (printInfoMessages || msg.type != ResultWas::Info)
+                        stream << Column(msg.message).indent(2) << '\n';
+                }
+            }
+            void printSourceInfo() const {
+                Colour colourGuard(Colour::FileName);
+                stream << result.getSourceInfo() << ": ";
+            }
+
+            std::ostream& stream;
+            AssertionStats const& stats;
+            AssertionResult const& result;
+            Colour::Code colour;
+            std::string passOrFail;
+            std::string messageLabel;
+            std::string message;
+            std::vector<MessageInfo> messages;
+            bool printInfoMessages;
+        };
+
+        std::size_t makeRatio(std::size_t number, std::size_t total) {
+            std::size_t ratio = total > 0 ? CATCH_CONFIG_CONSOLE_WIDTH * number / total : 0;
+            return (ratio == 0 && number > 0) ? 1 : ratio;
+        }
+
+        std::size_t& findMax(std::size_t& i, std::size_t& j, std::size_t& k) {
+            if (i > j && i > k)
+                return i;
+            else if (j > k)
+                return j;
+            else
+                return k;
+        }
+
+        struct ColumnInfo {
+            enum Justification { Left, Right };
+            std::string name;
+            int width;
+            Justification justification;
+        };
+        struct ColumnBreak {};
+        struct RowBreak {};
+
+        class Duration {
+            enum class Unit { Auto, Nanoseconds, Microseconds, Milliseconds, Seconds, Minutes };
+            static const uint64_t s_nanosecondsInAMicrosecond = 1000;
+            static const uint64_t s_nanosecondsInAMillisecond = 1000 * s_nanosecondsInAMicrosecond;
+            static const uint64_t s_nanosecondsInASecond = 1000 * s_nanosecondsInAMillisecond;
+            static const uint64_t s_nanosecondsInAMinute = 60 * s_nanosecondsInASecond;
+
+            double m_inNanoseconds;
+            Unit m_units;
+
+        public:
+            explicit Duration(double inNanoseconds, Unit units = Unit::Auto) :
+                m_inNanoseconds(inNanoseconds), m_units(units) {
+                if (m_units == Unit::Auto) {
+                    if (m_inNanoseconds < s_nanosecondsInAMicrosecond)
+                        m_units = Unit::Nanoseconds;
+                    else if (m_inNanoseconds < s_nanosecondsInAMillisecond)
+                        m_units = Unit::Microseconds;
+                    else if (m_inNanoseconds < s_nanosecondsInASecond)
+                        m_units = Unit::Milliseconds;
+                    else if (m_inNanoseconds < s_nanosecondsInAMinute)
+                        m_units = Unit::Seconds;
+                    else
+                        m_units = Unit::Minutes;
+                }
+            }
+
+            auto value() const -> double {
+                switch (m_units) {
+                    case Unit::Microseconds:
+                        return m_inNanoseconds / static_cast<double>(s_nanosecondsInAMicrosecond);
+                    case Unit::Milliseconds:
+                        return m_inNanoseconds / static_cast<double>(s_nanosecondsInAMillisecond);
+                    case Unit::Seconds:
+                        return m_inNanoseconds / static_cast<double>(s_nanosecondsInASecond);
+                    case Unit::Minutes:
+                        return m_inNanoseconds / static_cast<double>(s_nanosecondsInAMinute);
+                    default:
+                        return m_inNanoseconds;
+                }
+            }
+            auto unitsAsString() const -> std::string {
+                switch (m_units) {
+                    case Unit::Nanoseconds:
+                        return "ns";
+                    case Unit::Microseconds:
+                        return "us";
+                    case Unit::Milliseconds:
+                        return "ms";
+                    case Unit::Seconds:
+                        return "s";
+                    case Unit::Minutes:
+                        return "m";
+                    default:
+                        return "** internal error **";
+                }
+            }
+            friend auto operator<<(std::ostream& os, Duration const& duration) -> std::ostream& {
+                return os << duration.value() << ' ' << duration.unitsAsString();
+            }
+        };
+    } // namespace
+
+    class TablePrinter {
+        std::ostream& m_os;
+        std::vector<ColumnInfo> m_columnInfos;
+        std::ostringstream m_oss;
+        int m_currentColumn = -1;
+        bool m_isOpen = false;
+
+    public:
+        TablePrinter(std::ostream& os, std::vector<ColumnInfo> columnInfos) :
+            m_os(os), m_columnInfos(std::move(columnInfos)) {}
+
+        auto columnInfos() const -> std::vector<ColumnInfo> const& { return m_columnInfos; }
+
+        void open() {
+            if (!m_isOpen) {
+                m_isOpen = true;
+                *this << RowBreak();
+
+                Columns headerCols;
+                Spacer spacer(2);
+                for (auto const& info : m_columnInfos) {
+                    headerCols += Column(info.name).width(static_cast<std::size_t>(info.width - 2));
+                    headerCols += spacer;
+                }
+                m_os << headerCols << '\n';
+
+                m_os << Catch::getLineOfChars<'-'>() << '\n';
+            }
+        }
+        void close() {
+            if (m_isOpen) {
+                *this << RowBreak();
+                m_os << std::endl;
+                m_isOpen = false;
+            }
+        }
+
+        template <typename T> friend TablePrinter& operator<<(TablePrinter& tp, T const& value) {
+            tp.m_oss << value;
+            return tp;
+        }
+
+        friend TablePrinter& operator<<(TablePrinter& tp, ColumnBreak) {
+            auto colStr = tp.m_oss.str();
+            const auto strSize = colStr.size();
+            tp.m_oss.str("");
+            tp.open();
+            if (tp.m_currentColumn == static_cast<int>(tp.m_columnInfos.size() - 1)) {
+                tp.m_currentColumn = -1;
+                tp.m_os << '\n';
+            }
+            tp.m_currentColumn++;
+
+            auto colInfo = tp.m_columnInfos[tp.m_currentColumn];
+            auto padding = (strSize + 1 < static_cast<std::size_t>(colInfo.width))
+                               ? std::string(colInfo.width - (strSize + 1), ' ')
+                               : std::string();
+            if (colInfo.justification == ColumnInfo::Left)
+                tp.m_os << colStr << padding << ' ';
+            else
+                tp.m_os << padding << colStr << ' ';
+            return tp;
+        }
+
+        friend TablePrinter& operator<<(TablePrinter& tp, RowBreak) {
+            if (tp.m_currentColumn > 0) {
+                tp.m_os << '\n';
+                tp.m_currentColumn = -1;
+            }
+            return tp;
+        }
+    };
+
+    ConsoleReporter::ConsoleReporter(ReporterConfig const& config) :
+        StreamingReporterBase(config),
+        m_tablePrinter(new TablePrinter(config.stream(), [&config]() -> std::vector<ColumnInfo> {
+            if (config.fullConfig()->benchmarkNoAnalysis()) {
+                return {{"benchmark name", CATCH_CONFIG_CONSOLE_WIDTH - 43, ColumnInfo::Left},
+                        {"     samples", 14, ColumnInfo::Right},
+                        {"  iterations", 14, ColumnInfo::Right},
+                        {"        mean", 14, ColumnInfo::Right}};
+            } else {
+                return {{"benchmark name", CATCH_CONFIG_CONSOLE_WIDTH - 43, ColumnInfo::Left},
+                        {"samples      mean       std dev", 14, ColumnInfo::Right},
+                        {"iterations   low mean   low std dev", 14, ColumnInfo::Right},
+                        {"estimated    high mean  high std dev", 14, ColumnInfo::Right}};
+            }
+        }())) {}
+    ConsoleReporter::~ConsoleReporter() = default;
+
+    std::string ConsoleReporter::getDescription() {
+        return "Reports test results as plain lines of text";
+    }
+
+    void ConsoleReporter::noMatchingTestCases(std::string const& spec) {
+        stream << "No test cases matched '" << spec << '\'' << std::endl;
+    }
+
+    void ConsoleReporter::reportInvalidArguments(std::string const& arg) {
+        stream << "Invalid Filter: " << arg << std::endl;
+    }
+
+    void ConsoleReporter::assertionStarting(AssertionInfo const&) {}
+
+    bool ConsoleReporter::assertionEnded(AssertionStats const& _assertionStats) {
+        AssertionResult const& result = _assertionStats.assertionResult;
+
+        bool includeResults = m_config->includeSuccessfulResults() || !result.isOk();
+
+        // Drop out if result was successful but we're not printing them.
+        if (!includeResults && result.getResultType() != ResultWas::Warning)
+            return false;
+
+        lazyPrint();
+
+        ConsoleAssertionPrinter printer(stream, _assertionStats, includeResults);
+        printer.print();
+        stream << std::endl;
+        return true;
+    }
+
+    void ConsoleReporter::sectionStarting(SectionInfo const& _sectionInfo) {
+        m_tablePrinter->close();
+        m_headerPrinted = false;
+        StreamingReporterBase::sectionStarting(_sectionInfo);
+    }
+    void ConsoleReporter::sectionEnded(SectionStats const& _sectionStats) {
+        m_tablePrinter->close();
+        if (_sectionStats.missingAssertions) {
+            lazyPrint();
+            Colour colour(Colour::ResultError);
+            if (m_sectionStack.size() > 1)
+                stream << "\nNo assertions in section";
+            else
+                stream << "\nNo assertions in test case";
+            stream << " '" << _sectionStats.sectionInfo.name << "'\n" << std::endl;
+        }
+        double dur = _sectionStats.durationInSeconds;
+        if (shouldShowDuration(*m_config, dur)) {
+            stream << getFormattedDuration(dur) << " s: " << _sectionStats.sectionInfo.name
+                   << std::endl;
+        }
+        if (m_headerPrinted) {
+            m_headerPrinted = false;
+        }
+        StreamingReporterBase::sectionEnded(_sectionStats);
+    }
+
+#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING)
+    void ConsoleReporter::benchmarkPreparing(std::string const& name) {
+        lazyPrintWithoutClosingBenchmarkTable();
+
+        auto nameCol = Column(name).width(
+            static_cast<std::size_t>(m_tablePrinter->columnInfos()[0].width - 2));
+
+        bool firstLine = true;
+        for (auto line : nameCol) {
+            if (!firstLine)
+                (*m_tablePrinter) << ColumnBreak() << ColumnBreak() << ColumnBreak();
+            else
+                firstLine = false;
+
+            (*m_tablePrinter) << line << ColumnBreak();
+        }
+    }
+
+    void ConsoleReporter::benchmarkStarting(BenchmarkInfo const& info) {
+        (*m_tablePrinter) << info.samples << ColumnBreak() << info.iterations << ColumnBreak();
+        if (!m_config->benchmarkNoAnalysis())
+            (*m_tablePrinter) << Duration(info.estimatedDuration) << ColumnBreak();
+    }
+    void ConsoleReporter::benchmarkEnded(BenchmarkStats<> const& stats) {
+        if (m_config->benchmarkNoAnalysis()) {
+            (*m_tablePrinter) << Duration(stats.mean.point.count()) << ColumnBreak();
+        } else {
+            (*m_tablePrinter) << ColumnBreak() << Duration(stats.mean.point.count())
+                              << ColumnBreak() << Duration(stats.mean.lower_bound.count())
+                              << ColumnBreak() << Duration(stats.mean.upper_bound.count())
+                              << ColumnBreak() << ColumnBreak()
+                              << Duration(stats.standardDeviation.point.count()) << ColumnBreak()
+                              << Duration(stats.standardDeviation.lower_bound.count())
+                              << ColumnBreak()
+                              << Duration(stats.standardDeviation.upper_bound.count())
+                              << ColumnBreak() << ColumnBreak() << ColumnBreak() << ColumnBreak()
+                              << ColumnBreak();
+        }
+    }
+
+    void ConsoleReporter::benchmarkFailed(std::string const& error) {
+        Colour colour(Colour::Red);
+        (*m_tablePrinter) << "Benchmark failed (" << error << ')' << ColumnBreak() << RowBreak();
+    }
+#endif // CATCH_CONFIG_ENABLE_BENCHMARKING
+
+    void ConsoleReporter::testCaseEnded(TestCaseStats const& _testCaseStats) {
+        m_tablePrinter->close();
+        StreamingReporterBase::testCaseEnded(_testCaseStats);
+        m_headerPrinted = false;
+    }
+    void ConsoleReporter::testGroupEnded(TestGroupStats const& _testGroupStats) {
+        if (currentGroupInfo.used) {
+            printSummaryDivider();
+            stream << "Summary for group '" << _testGroupStats.groupInfo.name << "':\n";
+            printTotals(_testGroupStats.totals);
+            stream << '\n' << std::endl;
+        }
+        StreamingReporterBase::testGroupEnded(_testGroupStats);
+    }
+    void ConsoleReporter::testRunEnded(TestRunStats const& _testRunStats) {
+        printTotalsDivider(_testRunStats.totals);
+        printTotals(_testRunStats.totals);
+        stream << std::endl;
+        StreamingReporterBase::testRunEnded(_testRunStats);
+    }
+    void ConsoleReporter::testRunStarting(TestRunInfo const& _testInfo) {
+        StreamingReporterBase::testRunStarting(_testInfo);
+        printTestFilters();
+    }
+
+    void ConsoleReporter::lazyPrint() {
+
+        m_tablePrinter->close();
+        lazyPrintWithoutClosingBenchmarkTable();
+    }
+
+    void ConsoleReporter::lazyPrintWithoutClosingBenchmarkTable() {
+
+        if (!currentTestRunInfo.used)
+            lazyPrintRunInfo();
+        if (!currentGroupInfo.used)
+            lazyPrintGroupInfo();
+
+        if (!m_headerPrinted) {
+            printTestCaseAndSectionHeader();
+            m_headerPrinted = true;
+        }
+    }
+    void ConsoleReporter::lazyPrintRunInfo() {
+        stream << '\n' << getLineOfChars<'~'>() << '\n';
+        Colour colour(Colour::SecondaryText);
+        stream << currentTestRunInfo->name << " is a Catch v" << libraryVersion()
+               << " host application.\n"
+               << "Run with -? for options\n\n";
+
+        if (m_config->rngSeed() != 0)
+            stream << "Randomness seeded to: " << m_config->rngSeed() << "\n\n";
+
+        currentTestRunInfo.used = true;
+    }
+    void ConsoleReporter::lazyPrintGroupInfo() {
+        if (!currentGroupInfo->name.empty() && currentGroupInfo->groupsCounts > 1) {
+            printClosedHeader("Group: " + currentGroupInfo->name);
+            currentGroupInfo.used = true;
+        }
+    }
+    void ConsoleReporter::printTestCaseAndSectionHeader() {
+        assert(!m_sectionStack.empty());
+        printOpenHeader(currentTestCaseInfo->name);
+
+        if (m_sectionStack.size() > 1) {
+            Colour colourGuard(Colour::Headers);
+
+            auto it = m_sectionStack.begin() + 1, // Skip first section (test case)
+                itEnd = m_sectionStack.end();
+            for (; it != itEnd; ++it)
+                printHeaderString(it->name, 2);
+        }
+
+        SourceLineInfo lineInfo = m_sectionStack.back().lineInfo;
+
+        stream << getLineOfChars<'-'>() << '\n';
+        Colour colourGuard(Colour::FileName);
+        stream << lineInfo << '\n';
+        stream << getLineOfChars<'.'>() << '\n' << std::endl;
+    }
+
+    void ConsoleReporter::printClosedHeader(std::string const& _name) {
+        printOpenHeader(_name);
+        stream << getLineOfChars<'.'>() << '\n';
+    }
+    void ConsoleReporter::printOpenHeader(std::string const& _name) {
+        stream << getLineOfChars<'-'>() << '\n';
+        {
+            Colour colourGuard(Colour::Headers);
+            printHeaderString(_name);
+        }
+    }
+
+    // if string has a : in first line will set indent to follow it on
+    // subsequent lines
+    void ConsoleReporter::printHeaderString(std::string const& _string, std::size_t indent) {
+        std::size_t i = _string.find(": ");
+        if (i != std::string::npos)
+            i += 2;
+        else
+            i = 0;
+        stream << Column(_string).indent(indent + i).initialIndent(indent) << '\n';
+    }
+
+    struct SummaryColumn {
+
+        SummaryColumn(std::string _label, Colour::Code _colour) :
+            label(std::move(_label)), colour(_colour) {}
+        SummaryColumn addRow(std::size_t count) {
+            ReusableStringStream rss;
+            rss << count;
+            std::string row = rss.str();
+            for (auto& oldRow : rows) {
+                while (oldRow.size() < row.size())
+                    oldRow = ' ' + oldRow;
+                while (oldRow.size() > row.size())
+                    row = ' ' + row;
+            }
+            rows.push_back(row);
+            return *this;
+        }
+
+        std::string label;
+        Colour::Code colour;
+        std::vector<std::string> rows;
+    };
+
+    void ConsoleReporter::printTotals(Totals const& totals) {
+        if (totals.testCases.total() == 0) {
+            stream << Colour(Colour::Warning) << "No tests ran\n";
+        } else if (totals.assertions.total() > 0 && totals.testCases.allPassed()) {
+            stream << Colour(Colour::ResultSuccess) << "All tests passed";
+            stream << " (" << pluralise(totals.assertions.passed, "assertion") << " in "
+                   << pluralise(totals.testCases.passed, "test case") << ')' << '\n';
+        } else {
+
+            std::vector<SummaryColumn> columns;
+            columns.push_back(SummaryColumn("", Colour::None)
+                                  .addRow(totals.testCases.total())
+                                  .addRow(totals.assertions.total()));
+            columns.push_back(SummaryColumn("passed", Colour::Success)
+                                  .addRow(totals.testCases.passed)
+                                  .addRow(totals.assertions.passed));
+            columns.push_back(SummaryColumn("failed", Colour::ResultError)
+                                  .addRow(totals.testCases.failed)
+                                  .addRow(totals.assertions.failed));
+            columns.push_back(SummaryColumn("failed as expected", Colour::ResultExpectedFailure)
+                                  .addRow(totals.testCases.failedButOk)
+                                  .addRow(totals.assertions.failedButOk));
+
+            printSummaryRow("test cases", columns, 0);
+            printSummaryRow("assertions", columns, 1);
+        }
+    }
+    void ConsoleReporter::printSummaryRow(std::string const& label,
+                                          std::vector<SummaryColumn> const& cols,
+                                          std::size_t row) {
+        for (auto col : cols) {
+            std::string value = col.rows[row];
+            if (col.label.empty()) {
+                stream << label << ": ";
+                if (value != "0")
+                    stream << value;
+                else
+                    stream << Colour(Colour::Warning) << "- none -";
+            } else if (value != "0") {
+                stream << Colour(Colour::LightGrey) << " | ";
+                stream << Colour(col.colour) << value << ' ' << col.label;
+            }
+        }
+        stream << '\n';
+    }
+
+    void ConsoleReporter::printTotalsDivider(Totals const& totals) {
+        if (totals.testCases.total() > 0) {
+            std::size_t failedRatio = makeRatio(totals.testCases.failed, totals.testCases.total());
+            std::size_t failedButOkRatio =
+                makeRatio(totals.testCases.failedButOk, totals.testCases.total());
+            std::size_t passedRatio = makeRatio(totals.testCases.passed, totals.testCases.total());
+            while (failedRatio + failedButOkRatio + passedRatio < CATCH_CONFIG_CONSOLE_WIDTH - 1)
+                findMax(failedRatio, failedButOkRatio, passedRatio)++;
+            while (failedRatio + failedButOkRatio + passedRatio > CATCH_CONFIG_CONSOLE_WIDTH - 1)
+                findMax(failedRatio, failedButOkRatio, passedRatio)--;
+
+            stream << Colour(Colour::Error) << std::string(failedRatio, '=');
+            stream << Colour(Colour::ResultExpectedFailure) << std::string(failedButOkRatio, '=');
+            if (totals.testCases.allPassed())
+                stream << Colour(Colour::ResultSuccess) << std::string(passedRatio, '=');
+            else
+                stream << Colour(Colour::Success) << std::string(passedRatio, '=');
+        } else {
+            stream << Colour(Colour::Warning) << std::string(CATCH_CONFIG_CONSOLE_WIDTH - 1, '=');
+        }
+        stream << '\n';
+    }
+    void ConsoleReporter::printSummaryDivider() { stream << getLineOfChars<'-'>() << '\n'; }
+
+    void ConsoleReporter::printTestFilters() {
+        if (m_config->testSpec().hasFilters()) {
+            Colour guard(Colour::BrightYellow);
+            stream << "Filters: " << serializeFilters(m_config->getTestsOrTags()) << '\n';
+        }
+    }
+
+    CATCH_REGISTER_REPORTER("console", ConsoleReporter)
+
+} // end namespace Catch
+
+#if defined(_MSC_VER)
+#pragma warning(pop)
+#endif
+
+#if defined(__clang__)
+#pragma clang diagnostic pop
+#endif
+// end catch_reporter_console.cpp
+// start catch_reporter_junit.cpp
+
+#include <cassert>
+#include <sstream>
+#include <ctime>
+#include <algorithm>
+#include <iomanip>
+
+namespace Catch {
+
+    namespace {
+        std::string getCurrentTimestamp() {
+            // Beware, this is not reentrant because of backward compatibility
+            // issues Also, UTC only, again because of backward compatibility
+            // (%z is C++11)
+            time_t rawtime;
+            std::time(&rawtime);
+            auto const timeStampSize = sizeof("2017-01-16T17:06:45Z");
+
+#ifdef _MSC_VER
+            std::tm timeInfo = {};
+            gmtime_s(&timeInfo, &rawtime);
+#else
+            std::tm* timeInfo;
+            timeInfo = std::gmtime(&rawtime);
+#endif
+
+            char timeStamp[timeStampSize];
+            const char* const fmt = "%Y-%m-%dT%H:%M:%SZ";
+
+#ifdef _MSC_VER
+            std::strftime(timeStamp, timeStampSize, fmt, &timeInfo);
+#else
+            std::strftime(timeStamp, timeStampSize, fmt, timeInfo);
+#endif
+            return std::string(timeStamp, timeStampSize - 1);
+        }
+
+        std::string fileNameTag(const std::vector<std::string>& tags) {
+            auto it = std::find_if(
+                begin(tags), end(tags), [](std::string const& tag) { return tag.front() == '#'; });
+            if (it != tags.end())
+                return it->substr(1);
+            return std::string();
+        }
+
+        // Formats the duration in seconds to 3 decimal places.
+        // This is done because some genius defined Maven Surefire schema
+        // in a way that only accepts 3 decimal places, and tools like
+        // Jenkins use that schema for validation JUnit reporter output.
+        std::string formatDuration(double seconds) {
+            ReusableStringStream rss;
+            rss << std::fixed << std::setprecision(3) << seconds;
+            return rss.str();
+        }
+
+    } // anonymous namespace
+
+    JunitReporter::JunitReporter(ReporterConfig const& _config) :
+        CumulativeReporterBase(_config), xml(_config.stream()) {
+        m_reporterPrefs.shouldRedirectStdOut = true;
+        m_reporterPrefs.shouldReportAllAssertions = true;
+    }
+
+    JunitReporter::~JunitReporter() {}
+
+    std::string JunitReporter::getDescription() {
+        return "Reports test results in an XML format that looks like Ant's "
+               "junitreport target";
+    }
+
+    void JunitReporter::noMatchingTestCases(std::string const& /*spec*/) {}
+
+    void JunitReporter::testRunStarting(TestRunInfo const& runInfo) {
+        CumulativeReporterBase::testRunStarting(runInfo);
+        xml.startElement("testsuites");
+    }
+
+    void JunitReporter::testGroupStarting(GroupInfo const& groupInfo) {
+        suiteTimer.start();
+        stdOutForSuite.clear();
+        stdErrForSuite.clear();
+        unexpectedExceptions = 0;
+        CumulativeReporterBase::testGroupStarting(groupInfo);
+    }
+
+    void JunitReporter::testCaseStarting(TestCaseInfo const& testCaseInfo) {
+        m_okToFail = testCaseInfo.okToFail();
+    }
+
+    bool JunitReporter::assertionEnded(AssertionStats const& assertionStats) {
+        if (assertionStats.assertionResult.getResultType() == ResultWas::ThrewException &&
+            !m_okToFail)
+            unexpectedExceptions++;
+        return CumulativeReporterBase::assertionEnded(assertionStats);
+    }
+
+    void JunitReporter::testCaseEnded(TestCaseStats const& testCaseStats) {
+        stdOutForSuite += testCaseStats.stdOut;
+        stdErrForSuite += testCaseStats.stdErr;
+        CumulativeReporterBase::testCaseEnded(testCaseStats);
+    }
+
+    void JunitReporter::testGroupEnded(TestGroupStats const& testGroupStats) {
+        double suiteTime = suiteTimer.getElapsedSeconds();
+        CumulativeReporterBase::testGroupEnded(testGroupStats);
+        writeGroup(*m_testGroups.back(), suiteTime);
+    }
+
+    void JunitReporter::testRunEndedCumulative() { xml.endElement(); }
+
+    void JunitReporter::writeGroup(TestGroupNode const& groupNode, double suiteTime) {
+        XmlWriter::ScopedElement e = xml.scopedElement("testsuite");
+
+        TestGroupStats const& stats = groupNode.value;
+        xml.writeAttribute("name", stats.groupInfo.name);
+        xml.writeAttribute("errors", unexpectedExceptions);
+        xml.writeAttribute("failures", stats.totals.assertions.failed - unexpectedExceptions);
+        xml.writeAttribute("tests", stats.totals.assertions.total());
+        xml.writeAttribute("hostname", "tbd"); // !TBD
+        if (m_config->showDurations() == ShowDurations::Never)
+            xml.writeAttribute("time", "");
+        else
+            xml.writeAttribute("time", formatDuration(suiteTime));
+        xml.writeAttribute("timestamp", getCurrentTimestamp());
+
+        // Write properties if there are any
+        if (m_config->hasTestFilters() || m_config->rngSeed() != 0) {
+            auto properties = xml.scopedElement("properties");
+            if (m_config->hasTestFilters()) {
+                xml.scopedElement("property")
+                    .writeAttribute("name", "filters")
+                    .writeAttribute("value", serializeFilters(m_config->getTestsOrTags()));
+            }
+            if (m_config->rngSeed() != 0) {
+                xml.scopedElement("property")
+                    .writeAttribute("name", "random-seed")
+                    .writeAttribute("value", m_config->rngSeed());
+            }
+        }
+
+        // Write test cases
+        for (auto const& child : groupNode.children)
+            writeTestCase(*child);
+
+        xml.scopedElement("system-out").writeText(trim(stdOutForSuite), XmlFormatting::Newline);
+        xml.scopedElement("system-err").writeText(trim(stdErrForSuite), XmlFormatting::Newline);
+    }
+
+    void JunitReporter::writeTestCase(TestCaseNode const& testCaseNode) {
+        TestCaseStats const& stats = testCaseNode.value;
+
+        // All test cases have exactly one section - which represents the
+        // test case itself. That section may have 0-n nested sections
+        assert(testCaseNode.children.size() == 1);
+        SectionNode const& rootSection = *testCaseNode.children.front();
+
+        std::string className = stats.testInfo.className;
+
+        if (className.empty()) {
+            className = fileNameTag(stats.testInfo.tags);
+            if (className.empty())
+                className = "global";
+        }
+
+        if (!m_config->name().empty())
+            className = m_config->name() + "." + className;
+
+        writeSection(className, "", rootSection, stats.testInfo.okToFail());
+    }
+
+    void JunitReporter::writeSection(std::string const& className,
+                                     std::string const& rootName,
+                                     SectionNode const& sectionNode,
+                                     bool testOkToFail) {
+        std::string name = trim(sectionNode.stats.sectionInfo.name);
+        if (!rootName.empty())
+            name = rootName + '/' + name;
+
+        if (!sectionNode.assertions.empty() || !sectionNode.stdOut.empty() ||
+            !sectionNode.stdErr.empty()) {
+            XmlWriter::ScopedElement e = xml.scopedElement("testcase");
+            if (className.empty()) {
+                xml.writeAttribute("classname", name);
+                xml.writeAttribute("name", "root");
+            } else {
+                xml.writeAttribute("classname", className);
+                xml.writeAttribute("name", name);
+            }
+            xml.writeAttribute("time", formatDuration(sectionNode.stats.durationInSeconds));
+            // This is not ideal, but it should be enough to mimic gtest's
+            // junit output.
+            // Ideally the JUnit reporter would also handle `skipTest`
+            // events and write those out appropriately.
+            xml.writeAttribute("status", "run");
+
+            if (sectionNode.stats.assertions.failedButOk) {
+                xml.scopedElement("skipped").writeAttribute("message",
+                                                            "TEST_CASE tagged with !mayfail");
+            }
+
+            writeAssertions(sectionNode);
+
+            if (!sectionNode.stdOut.empty())
+                xml.scopedElement("system-out")
+                    .writeText(trim(sectionNode.stdOut), XmlFormatting::Newline);
+            if (!sectionNode.stdErr.empty())
+                xml.scopedElement("system-err")
+                    .writeText(trim(sectionNode.stdErr), XmlFormatting::Newline);
+        }
+        for (auto const& childNode : sectionNode.childSections)
+            if (className.empty())
+                writeSection(name, "", *childNode, testOkToFail);
+            else
+                writeSection(className, name, *childNode, testOkToFail);
+    }
+
+    void JunitReporter::writeAssertions(SectionNode const& sectionNode) {
+        for (auto const& assertion : sectionNode.assertions)
+            writeAssertion(assertion);
+    }
+
+    void JunitReporter::writeAssertion(AssertionStats const& stats) {
+        AssertionResult const& result = stats.assertionResult;
+        if (!result.isOk()) {
+            std::string elementName;
+            switch (result.getResultType()) {
+                case ResultWas::ThrewException:
+                case ResultWas::FatalErrorCondition:
+                    elementName = "error";
+                    break;
+                case ResultWas::ExplicitFailure:
+                case ResultWas::ExpressionFailed:
+                case ResultWas::DidntThrowException:
+                    elementName = "failure";
+                    break;
+
+                // We should never see these here:
+                case ResultWas::Info:
+                case ResultWas::Warning:
+                case ResultWas::Ok:
+                case ResultWas::Unknown:
+                case ResultWas::FailureBit:
+                case ResultWas::Exception:
+                    elementName = "internalError";
+                    break;
+            }
+
+            XmlWriter::ScopedElement e = xml.scopedElement(elementName);
+
+            xml.writeAttribute("message", result.getExpression());
+            xml.writeAttribute("type", result.getTestMacroName());
+
+            ReusableStringStream rss;
+            if (stats.totals.assertions.total() > 0) {
+                rss << "FAILED"
+                    << ":\n";
+                if (result.hasExpression()) {
+                    rss << "  ";
+                    rss << result.getExpressionInMacro();
+                    rss << '\n';
+                }
+                if (result.hasExpandedExpression()) {
+                    rss << "with expansion:\n";
+                    rss << Column(result.getExpandedExpression()).indent(2) << '\n';
+                }
+            } else {
+                rss << '\n';
+            }
+
+            if (!result.getMessage().empty())
+                rss << result.getMessage() << '\n';
+            for (auto const& msg : stats.infoMessages)
+                if (msg.type == ResultWas::Info)
+                    rss << msg.message << '\n';
+
+            rss << "at " << result.getSourceInfo();
+            xml.writeText(rss.str(), XmlFormatting::Newline);
+        }
+    }
+
+    CATCH_REGISTER_REPORTER("junit", JunitReporter)
+
+} // end namespace Catch
+// end catch_reporter_junit.cpp
+// start catch_reporter_listening.cpp
+
+#include <cassert>
+
+namespace Catch {
+
+    ListeningReporter::ListeningReporter() {
+        // We will assume that listeners will always want all assertions
+        m_preferences.shouldReportAllAssertions = true;
+    }
+
+    void ListeningReporter::addListener(IStreamingReporterPtr&& listener) {
+        m_listeners.push_back(std::move(listener));
+    }
+
+    void ListeningReporter::addReporter(IStreamingReporterPtr&& reporter) {
+        assert(!m_reporter && "Listening reporter can wrap only 1 real reporter");
+        m_reporter = std::move(reporter);
+        m_preferences.shouldRedirectStdOut = m_reporter->getPreferences().shouldRedirectStdOut;
+    }
+
+    ReporterPreferences ListeningReporter::getPreferences() const { return m_preferences; }
+
+    std::set<Verbosity> ListeningReporter::getSupportedVerbosities() {
+        return std::set<Verbosity>{};
+    }
+
+    void ListeningReporter::noMatchingTestCases(std::string const& spec) {
+        for (auto const& listener : m_listeners) {
+            listener->noMatchingTestCases(spec);
+        }
+        m_reporter->noMatchingTestCases(spec);
+    }
+
+    void ListeningReporter::reportInvalidArguments(std::string const& arg) {
+        for (auto const& listener : m_listeners) {
+            listener->reportInvalidArguments(arg);
+        }
+        m_reporter->reportInvalidArguments(arg);
+    }
+
+#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING)
+    void ListeningReporter::benchmarkPreparing(std::string const& name) {
+        for (auto const& listener : m_listeners) {
+            listener->benchmarkPreparing(name);
+        }
+        m_reporter->benchmarkPreparing(name);
+    }
+    void ListeningReporter::benchmarkStarting(BenchmarkInfo const& benchmarkInfo) {
+        for (auto const& listener : m_listeners) {
+            listener->benchmarkStarting(benchmarkInfo);
+        }
+        m_reporter->benchmarkStarting(benchmarkInfo);
+    }
+    void ListeningReporter::benchmarkEnded(BenchmarkStats<> const& benchmarkStats) {
+        for (auto const& listener : m_listeners) {
+            listener->benchmarkEnded(benchmarkStats);
+        }
+        m_reporter->benchmarkEnded(benchmarkStats);
+    }
+
+    void ListeningReporter::benchmarkFailed(std::string const& error) {
+        for (auto const& listener : m_listeners) {
+            listener->benchmarkFailed(error);
+        }
+        m_reporter->benchmarkFailed(error);
+    }
+#endif // CATCH_CONFIG_ENABLE_BENCHMARKING
+
+    void ListeningReporter::testRunStarting(TestRunInfo const& testRunInfo) {
+        for (auto const& listener : m_listeners) {
+            listener->testRunStarting(testRunInfo);
+        }
+        m_reporter->testRunStarting(testRunInfo);
+    }
+
+    void ListeningReporter::testGroupStarting(GroupInfo const& groupInfo) {
+        for (auto const& listener : m_listeners) {
+            listener->testGroupStarting(groupInfo);
+        }
+        m_reporter->testGroupStarting(groupInfo);
+    }
+
+    void ListeningReporter::testCaseStarting(TestCaseInfo const& testInfo) {
+        for (auto const& listener : m_listeners) {
+            listener->testCaseStarting(testInfo);
+        }
+        m_reporter->testCaseStarting(testInfo);
+    }
+
+    void ListeningReporter::sectionStarting(SectionInfo const& sectionInfo) {
+        for (auto const& listener : m_listeners) {
+            listener->sectionStarting(sectionInfo);
+        }
+        m_reporter->sectionStarting(sectionInfo);
+    }
+
+    void ListeningReporter::assertionStarting(AssertionInfo const& assertionInfo) {
+        for (auto const& listener : m_listeners) {
+            listener->assertionStarting(assertionInfo);
+        }
+        m_reporter->assertionStarting(assertionInfo);
+    }
+
+    // The return value indicates if the messages buffer should be cleared:
+    bool ListeningReporter::assertionEnded(AssertionStats const& assertionStats) {
+        for (auto const& listener : m_listeners) {
+            static_cast<void>(listener->assertionEnded(assertionStats));
+        }
+        return m_reporter->assertionEnded(assertionStats);
+    }
+
+    void ListeningReporter::sectionEnded(SectionStats const& sectionStats) {
+        for (auto const& listener : m_listeners) {
+            listener->sectionEnded(sectionStats);
+        }
+        m_reporter->sectionEnded(sectionStats);
+    }
+
+    void ListeningReporter::testCaseEnded(TestCaseStats const& testCaseStats) {
+        for (auto const& listener : m_listeners) {
+            listener->testCaseEnded(testCaseStats);
+        }
+        m_reporter->testCaseEnded(testCaseStats);
+    }
+
+    void ListeningReporter::testGroupEnded(TestGroupStats const& testGroupStats) {
+        for (auto const& listener : m_listeners) {
+            listener->testGroupEnded(testGroupStats);
+        }
+        m_reporter->testGroupEnded(testGroupStats);
+    }
+
+    void ListeningReporter::testRunEnded(TestRunStats const& testRunStats) {
+        for (auto const& listener : m_listeners) {
+            listener->testRunEnded(testRunStats);
+        }
+        m_reporter->testRunEnded(testRunStats);
+    }
+
+    void ListeningReporter::skipTest(TestCaseInfo const& testInfo) {
+        for (auto const& listener : m_listeners) {
+            listener->skipTest(testInfo);
+        }
+        m_reporter->skipTest(testInfo);
+    }
+
+    bool ListeningReporter::isMulti() const { return true; }
+
+} // end namespace Catch
+// end catch_reporter_listening.cpp
+// start catch_reporter_xml.cpp
+
+#if defined(_MSC_VER)
+#pragma warning(push)
+#pragma warning(disable : 4061) // Not all labels are EXPLICITLY handled in
+                                // switch Note that 4062 (not all labels are
+                                // handled and default is missing) is enabled
+#endif
+
+namespace Catch {
+    XmlReporter::XmlReporter(ReporterConfig const& _config) :
+        StreamingReporterBase(_config), m_xml(_config.stream()) {
+        m_reporterPrefs.shouldRedirectStdOut = true;
+        m_reporterPrefs.shouldReportAllAssertions = true;
+    }
+
+    XmlReporter::~XmlReporter() = default;
+
+    std::string XmlReporter::getDescription() { return "Reports test results as an XML document"; }
+
+    std::string XmlReporter::getStylesheetRef() const { return std::string(); }
+
+    void XmlReporter::writeSourceInfo(SourceLineInfo const& sourceInfo) {
+        m_xml.writeAttribute("filename", sourceInfo.file).writeAttribute("line", sourceInfo.line);
+    }
+
+    void XmlReporter::noMatchingTestCases(std::string const& s) {
+        StreamingReporterBase::noMatchingTestCases(s);
+    }
+
+    void XmlReporter::testRunStarting(TestRunInfo const& testInfo) {
+        StreamingReporterBase::testRunStarting(testInfo);
+        std::string stylesheetRef = getStylesheetRef();
+        if (!stylesheetRef.empty())
+            m_xml.writeStylesheetRef(stylesheetRef);
+        m_xml.startElement("Catch");
+        if (!m_config->name().empty())
+            m_xml.writeAttribute("name", m_config->name());
+        if (m_config->testSpec().hasFilters())
+            m_xml.writeAttribute("filters", serializeFilters(m_config->getTestsOrTags()));
+        if (m_config->rngSeed() != 0)
+            m_xml.scopedElement("Randomness").writeAttribute("seed", m_config->rngSeed());
+    }
+
+    void XmlReporter::testGroupStarting(GroupInfo const& groupInfo) {
+        StreamingReporterBase::testGroupStarting(groupInfo);
+        m_xml.startElement("Group").writeAttribute("name", groupInfo.name);
+    }
+
+    void XmlReporter::testCaseStarting(TestCaseInfo const& testInfo) {
+        StreamingReporterBase::testCaseStarting(testInfo);
+        m_xml.startElement("TestCase")
+            .writeAttribute("name", trim(testInfo.name))
+            .writeAttribute("description", testInfo.description)
+            .writeAttribute("tags", testInfo.tagsAsString());
+
+        writeSourceInfo(testInfo.lineInfo);
+
+        if (m_config->showDurations() == ShowDurations::Always)
+            m_testCaseTimer.start();
+        m_xml.ensureTagClosed();
+    }
+
+    void XmlReporter::sectionStarting(SectionInfo const& sectionInfo) {
+        StreamingReporterBase::sectionStarting(sectionInfo);
+        if (m_sectionDepth++ > 0) {
+            m_xml.startElement("Section").writeAttribute("name", trim(sectionInfo.name));
+            writeSourceInfo(sectionInfo.lineInfo);
+            m_xml.ensureTagClosed();
+        }
+    }
+
+    void XmlReporter::assertionStarting(AssertionInfo const&) {}
+
+    bool XmlReporter::assertionEnded(AssertionStats const& assertionStats) {
+
+        AssertionResult const& result = assertionStats.assertionResult;
+
+        bool includeResults = m_config->includeSuccessfulResults() || !result.isOk();
+
+        if (includeResults || result.getResultType() == ResultWas::Warning) {
+            // Print any info messages in <Info> tags.
+            for (auto const& msg : assertionStats.infoMessages) {
+                if (msg.type == ResultWas::Info && includeResults) {
+                    m_xml.scopedElement("Info").writeText(msg.message);
+                } else if (msg.type == ResultWas::Warning) {
+                    m_xml.scopedElement("Warning").writeText(msg.message);
+                }
+            }
+        }
+
+        // Drop out if result was successful but we're not printing them.
+        if (!includeResults && result.getResultType() != ResultWas::Warning)
+            return true;
+
+        // Print the expression if there is one.
+        if (result.hasExpression()) {
+            m_xml.startElement("Expression")
+                .writeAttribute("success", result.succeeded())
+                .writeAttribute("type", result.getTestMacroName());
+
+            writeSourceInfo(result.getSourceInfo());
+
+            m_xml.scopedElement("Original").writeText(result.getExpression());
+            m_xml.scopedElement("Expanded").writeText(result.getExpandedExpression());
+        }
+
+        // And... Print a result applicable to each result type.
+        switch (result.getResultType()) {
+            case ResultWas::ThrewException:
+                m_xml.startElement("Exception");
+                writeSourceInfo(result.getSourceInfo());
+                m_xml.writeText(result.getMessage());
+                m_xml.endElement();
+                break;
+            case ResultWas::FatalErrorCondition:
+                m_xml.startElement("FatalErrorCondition");
+                writeSourceInfo(result.getSourceInfo());
+                m_xml.writeText(result.getMessage());
+                m_xml.endElement();
+                break;
+            case ResultWas::Info:
+                m_xml.scopedElement("Info").writeText(result.getMessage());
+                break;
+            case ResultWas::Warning:
+                // Warning will already have been written
+                break;
+            case ResultWas::ExplicitFailure:
+                m_xml.startElement("Failure");
+                writeSourceInfo(result.getSourceInfo());
+                m_xml.writeText(result.getMessage());
+                m_xml.endElement();
+                break;
+            default:
+                break;
+        }
+
+        if (result.hasExpression())
+            m_xml.endElement();
+
+        return true;
+    }
+
+    void XmlReporter::sectionEnded(SectionStats const& sectionStats) {
+        StreamingReporterBase::sectionEnded(sectionStats);
+        if (--m_sectionDepth > 0) {
+            XmlWriter::ScopedElement e = m_xml.scopedElement("OverallResults");
+            e.writeAttribute("successes", sectionStats.assertions.passed);
+            e.writeAttribute("failures", sectionStats.assertions.failed);
+            e.writeAttribute("expectedFailures", sectionStats.assertions.failedButOk);
+
+            if (m_config->showDurations() == ShowDurations::Always)
+                e.writeAttribute("durationInSeconds", sectionStats.durationInSeconds);
+
+            m_xml.endElement();
+        }
+    }
+
+    void XmlReporter::testCaseEnded(TestCaseStats const& testCaseStats) {
+        StreamingReporterBase::testCaseEnded(testCaseStats);
+        XmlWriter::ScopedElement e = m_xml.scopedElement("OverallResult");
+        e.writeAttribute("success", testCaseStats.totals.assertions.allOk());
+
+        if (m_config->showDurations() == ShowDurations::Always)
+            e.writeAttribute("durationInSeconds", m_testCaseTimer.getElapsedSeconds());
+
+        if (!testCaseStats.stdOut.empty())
+            m_xml.scopedElement("StdOut").writeText(trim(testCaseStats.stdOut),
+                                                    XmlFormatting::Newline);
+        if (!testCaseStats.stdErr.empty())
+            m_xml.scopedElement("StdErr").writeText(trim(testCaseStats.stdErr),
+                                                    XmlFormatting::Newline);
+
+        m_xml.endElement();
+    }
+
+    void XmlReporter::testGroupEnded(TestGroupStats const& testGroupStats) {
+        StreamingReporterBase::testGroupEnded(testGroupStats);
+        // TODO: Check testGroupStats.aborting and act accordingly.
+        m_xml.scopedElement("OverallResults")
+            .writeAttribute("successes", testGroupStats.totals.assertions.passed)
+            .writeAttribute("failures", testGroupStats.totals.assertions.failed)
+            .writeAttribute("expectedFailures", testGroupStats.totals.assertions.failedButOk);
+        m_xml.scopedElement("OverallResultsCases")
+            .writeAttribute("successes", testGroupStats.totals.testCases.passed)
+            .writeAttribute("failures", testGroupStats.totals.testCases.failed)
+            .writeAttribute("expectedFailures", testGroupStats.totals.testCases.failedButOk);
+        m_xml.endElement();
+    }
+
+    void XmlReporter::testRunEnded(TestRunStats const& testRunStats) {
+        StreamingReporterBase::testRunEnded(testRunStats);
+        m_xml.scopedElement("OverallResults")
+            .writeAttribute("successes", testRunStats.totals.assertions.passed)
+            .writeAttribute("failures", testRunStats.totals.assertions.failed)
+            .writeAttribute("expectedFailures", testRunStats.totals.assertions.failedButOk);
+        m_xml.scopedElement("OverallResultsCases")
+            .writeAttribute("successes", testRunStats.totals.testCases.passed)
+            .writeAttribute("failures", testRunStats.totals.testCases.failed)
+            .writeAttribute("expectedFailures", testRunStats.totals.testCases.failedButOk);
+        m_xml.endElement();
+    }
+
+#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING)
+    void XmlReporter::benchmarkPreparing(std::string const& name) {
+        m_xml.startElement("BenchmarkResults").writeAttribute("name", name);
+    }
+
+    void XmlReporter::benchmarkStarting(BenchmarkInfo const& info) {
+        m_xml.writeAttribute("samples", info.samples)
+            .writeAttribute("resamples", info.resamples)
+            .writeAttribute("iterations", info.iterations)
+            .writeAttribute("clockResolution", info.clockResolution)
+            .writeAttribute("estimatedDuration", info.estimatedDuration)
+            .writeComment("All values in nano seconds");
+    }
+
+    void XmlReporter::benchmarkEnded(BenchmarkStats<> const& benchmarkStats) {
+        m_xml.startElement("mean")
+            .writeAttribute("value", benchmarkStats.mean.point.count())
+            .writeAttribute("lowerBound", benchmarkStats.mean.lower_bound.count())
+            .writeAttribute("upperBound", benchmarkStats.mean.upper_bound.count())
+            .writeAttribute("ci", benchmarkStats.mean.confidence_interval);
+        m_xml.endElement();
+        m_xml.startElement("standardDeviation")
+            .writeAttribute("value", benchmarkStats.standardDeviation.point.count())
+            .writeAttribute("lowerBound", benchmarkStats.standardDeviation.lower_bound.count())
+            .writeAttribute("upperBound", benchmarkStats.standardDeviation.upper_bound.count())
+            .writeAttribute("ci", benchmarkStats.standardDeviation.confidence_interval);
+        m_xml.endElement();
+        m_xml.startElement("outliers")
+            .writeAttribute("variance", benchmarkStats.outlierVariance)
+            .writeAttribute("lowMild", benchmarkStats.outliers.low_mild)
+            .writeAttribute("lowSevere", benchmarkStats.outliers.low_severe)
+            .writeAttribute("highMild", benchmarkStats.outliers.high_mild)
+            .writeAttribute("highSevere", benchmarkStats.outliers.high_severe);
+        m_xml.endElement();
+        m_xml.endElement();
+    }
+
+    void XmlReporter::benchmarkFailed(std::string const& error) {
+        m_xml.scopedElement("failed").writeAttribute("message", error);
+        m_xml.endElement();
+    }
+#endif // CATCH_CONFIG_ENABLE_BENCHMARKING
+
+    CATCH_REGISTER_REPORTER("xml", XmlReporter)
+
+} // end namespace Catch
+
+#if defined(_MSC_VER)
+#pragma warning(pop)
+#endif
+// end catch_reporter_xml.cpp
+
+namespace Catch {
+    LeakDetector leakDetector;
+}
+
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
+
+// end catch_impl.hpp
+#endif
+
+#ifdef CATCH_CONFIG_MAIN
+// start catch_default_main.hpp
+
+#ifndef __OBJC__
+
+#if defined(CATCH_CONFIG_WCHAR) && defined(CATCH_PLATFORM_WINDOWS) && defined(_UNICODE) &&         \
+    !defined(DO_NOT_USE_WMAIN)
+// Standard C/C++ Win32 Unicode wmain entry point
+extern "C" int wmain(int argc, wchar_t* argv[], wchar_t*[]) {
+#else
+// Standard C/C++ main entry point
+int main(int argc, char* argv[]) {
+#endif
+
+    return Catch::Session().run(argc, argv);
+}
+
+#else // __OBJC__
+
+// Objective-C entry point
+int main(int argc, char* const argv[]) {
+#if !CATCH_ARC_ENABLED
+    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
+#endif
+
+    Catch::registerTestMethods();
+    int result = Catch::Session().run(argc, (char**)argv);
+
+#if !CATCH_ARC_ENABLED
+    [pool drain];
+#endif
+
+    return result;
+}
+
+#endif // __OBJC__
+
+// end catch_default_main.hpp
+#endif
+
+#if !defined(CATCH_CONFIG_IMPL_ONLY)
+
+#ifdef CLARA_CONFIG_MAIN_NOT_DEFINED
+#undef CLARA_CONFIG_MAIN
+#endif
+
+#if !defined(CATCH_CONFIG_DISABLE)
+//////
+// If this config identifier is defined then all CATCH macros are prefixed with
+// CATCH_
+#ifdef CATCH_CONFIG_PREFIX_ALL
+
+#define CATCH_REQUIRE(...)                                                                         \
+    INTERNAL_CATCH_TEST("CATCH_REQUIRE", Catch::ResultDisposition::Normal, __VA_ARGS__)
+#define CATCH_REQUIRE_FALSE(...)                                                                   \
+    INTERNAL_CATCH_TEST("CATCH_REQUIRE_FALSE",                                                     \
+                        Catch::ResultDisposition::Normal | Catch::ResultDisposition::FalseTest,    \
+                        __VA_ARGS__)
+
+#define CATCH_REQUIRE_THROWS(...)                                                                  \
+    INTERNAL_CATCH_THROWS("CATCH_REQUIRE_THROWS", Catch::ResultDisposition::Normal, __VA_ARGS__)
+#define CATCH_REQUIRE_THROWS_AS(expr, exceptionType)                                               \
+    INTERNAL_CATCH_THROWS_AS(                                                                      \
+        "CATCH_REQUIRE_THROWS_AS", exceptionType, Catch::ResultDisposition::Normal, expr)
+#define CATCH_REQUIRE_THROWS_WITH(expr, matcher)                                                   \
+    INTERNAL_CATCH_THROWS_STR_MATCHES(                                                             \
+        "CATCH_REQUIRE_THROWS_WITH", Catch::ResultDisposition::Normal, matcher, expr)
+#if !defined(CATCH_CONFIG_DISABLE_MATCHERS)
+#define CATCH_REQUIRE_THROWS_MATCHES(expr, exceptionType, matcher)                                 \
+    INTERNAL_CATCH_THROWS_MATCHES("CATCH_REQUIRE_THROWS_MATCHES",                                  \
+                                  exceptionType,                                                   \
+                                  Catch::ResultDisposition::Normal,                                \
+                                  matcher,                                                         \
+                                  expr)
+#endif // CATCH_CONFIG_DISABLE_MATCHERS
+#define CATCH_REQUIRE_NOTHROW(...)                                                                 \
+    INTERNAL_CATCH_NO_THROW("CATCH_REQUIRE_NOTHROW", Catch::ResultDisposition::Normal, __VA_ARGS__)
+
+#define CATCH_CHECK(...)                                                                           \
+    INTERNAL_CATCH_TEST("CATCH_CHECK", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__)
+#define CATCH_CHECK_FALSE(...)                                                                     \
+    INTERNAL_CATCH_TEST("CATCH_CHECK_FALSE",                                                       \
+                        Catch::ResultDisposition::ContinueOnFailure |                              \
+                            Catch::ResultDisposition::FalseTest,                                   \
+                        __VA_ARGS__)
+#define CATCH_CHECKED_IF(...)                                                                      \
+    INTERNAL_CATCH_IF("CATCH_CHECKED_IF", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__)
+#define CATCH_CHECKED_ELSE(...)                                                                    \
+    INTERNAL_CATCH_ELSE(                                                                           \
+        "CATCH_CHECKED_ELSE", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__)
+#define CATCH_CHECK_NOFAIL(...)                                                                    \
+    INTERNAL_CATCH_TEST("CATCH_CHECK_NOFAIL",                                                      \
+                        Catch::ResultDisposition::ContinueOnFailure |                              \
+                            Catch::ResultDisposition::SuppressFail,                                \
+                        __VA_ARGS__)
+
+#define CATCH_CHECK_THROWS(...)                                                                    \
+    INTERNAL_CATCH_THROWS(                                                                         \
+        "CATCH_CHECK_THROWS", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__)
+#define CATCH_CHECK_THROWS_AS(expr, exceptionType)                                                 \
+    INTERNAL_CATCH_THROWS_AS(                                                                      \
+        "CATCH_CHECK_THROWS_AS", exceptionType, Catch::ResultDisposition::ContinueOnFailure, expr)
+#define CATCH_CHECK_THROWS_WITH(expr, matcher)                                                     \
+    INTERNAL_CATCH_THROWS_STR_MATCHES(                                                             \
+        "CATCH_CHECK_THROWS_WITH", Catch::ResultDisposition::ContinueOnFailure, matcher, expr)
+#if !defined(CATCH_CONFIG_DISABLE_MATCHERS)
+#define CATCH_CHECK_THROWS_MATCHES(expr, exceptionType, matcher)                                   \
+    INTERNAL_CATCH_THROWS_MATCHES("CATCH_CHECK_THROWS_MATCHES",                                    \
+                                  exceptionType,                                                   \
+                                  Catch::ResultDisposition::ContinueOnFailure,                     \
+                                  matcher,                                                         \
+                                  expr)
+#endif // CATCH_CONFIG_DISABLE_MATCHERS
+#define CATCH_CHECK_NOTHROW(...)                                                                   \
+    INTERNAL_CATCH_NO_THROW(                                                                       \
+        "CATCH_CHECK_NOTHROW", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__)
+
+#if !defined(CATCH_CONFIG_DISABLE_MATCHERS)
+#define CATCH_CHECK_THAT(arg, matcher)                                                             \
+    INTERNAL_CHECK_THAT(                                                                           \
+        "CATCH_CHECK_THAT", matcher, Catch::ResultDisposition::ContinueOnFailure, arg)
+
+#define CATCH_REQUIRE_THAT(arg, matcher)                                                           \
+    INTERNAL_CHECK_THAT("CATCH_REQUIRE_THAT", matcher, Catch::ResultDisposition::Normal, arg)
+#endif // CATCH_CONFIG_DISABLE_MATCHERS
+
+#define CATCH_INFO(msg) INTERNAL_CATCH_INFO("CATCH_INFO", msg)
+#define CATCH_UNSCOPED_INFO(msg) INTERNAL_CATCH_UNSCOPED_INFO("CATCH_UNSCOPED_INFO", msg)
+#define CATCH_WARN(msg)                                                                            \
+    INTERNAL_CATCH_MSG(                                                                            \
+        "CATCH_WARN", Catch::ResultWas::Warning, Catch::ResultDisposition::ContinueOnFailure, msg)
+#define CATCH_CAPTURE(...)                                                                         \
+    INTERNAL_CATCH_CAPTURE(INTERNAL_CATCH_UNIQUE_NAME(capturer), "CATCH_CAPTURE", __VA_ARGS__)
+
+#define CATCH_TEST_CASE(...) INTERNAL_CATCH_TESTCASE(__VA_ARGS__)
+#define CATCH_TEST_CASE_METHOD(className, ...)                                                     \
+    INTERNAL_CATCH_TEST_CASE_METHOD(className, __VA_ARGS__)
+#define CATCH_METHOD_AS_TEST_CASE(method, ...)                                                     \
+    INTERNAL_CATCH_METHOD_AS_TEST_CASE(method, __VA_ARGS__)
+#define CATCH_REGISTER_TEST_CASE(Function, ...)                                                    \
+    INTERNAL_CATCH_REGISTER_TESTCASE(Function, __VA_ARGS__)
+#define CATCH_SECTION(...) INTERNAL_CATCH_SECTION(__VA_ARGS__)
+#define CATCH_DYNAMIC_SECTION(...) INTERNAL_CATCH_DYNAMIC_SECTION(__VA_ARGS__)
+#define CATCH_FAIL(...)                                                                            \
+    INTERNAL_CATCH_MSG("CATCH_FAIL",                                                               \
+                       Catch::ResultWas::ExplicitFailure,                                          \
+                       Catch::ResultDisposition::Normal,                                           \
+                       __VA_ARGS__)
+#define CATCH_FAIL_CHECK(...)                                                                      \
+    INTERNAL_CATCH_MSG("CATCH_FAIL_CHECK",                                                         \
+                       Catch::ResultWas::ExplicitFailure,                                          \
+                       Catch::ResultDisposition::ContinueOnFailure,                                \
+                       __VA_ARGS__)
+#define CATCH_SUCCEED(...)                                                                         \
+    INTERNAL_CATCH_MSG("CATCH_SUCCEED",                                                            \
+                       Catch::ResultWas::Ok,                                                       \
+                       Catch::ResultDisposition::ContinueOnFailure,                                \
+                       __VA_ARGS__)
+
+#define CATCH_ANON_TEST_CASE() INTERNAL_CATCH_TESTCASE()
+
+#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#define CATCH_TEMPLATE_TEST_CASE(...) INTERNAL_CATCH_TEMPLATE_TEST_CASE(__VA_ARGS__)
+#define CATCH_TEMPLATE_TEST_CASE_SIG(...) INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG(__VA_ARGS__)
+#define CATCH_TEMPLATE_TEST_CASE_METHOD(className, ...)                                            \
+    INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD(className, __VA_ARGS__)
+#define CATCH_TEMPLATE_TEST_CASE_METHOD_SIG(className, ...)                                        \
+    INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG(className, __VA_ARGS__)
+#define CATCH_TEMPLATE_PRODUCT_TEST_CASE(...) INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE(__VA_ARGS__)
+#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG(...)                                                  \
+    INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG(__VA_ARGS__)
+#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD(className, ...)                                    \
+    INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD(className, __VA_ARGS__)
+#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG(className, ...)                                \
+    INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG(className, __VA_ARGS__)
+#else
+#define CATCH_TEMPLATE_TEST_CASE(...)                                                              \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_TEST_CASE(__VA_ARGS__))
+#define CATCH_TEMPLATE_TEST_CASE_SIG(...)                                                          \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG(__VA_ARGS__))
+#define CATCH_TEMPLATE_TEST_CASE_METHOD(className, ...)                                            \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD(className, __VA_ARGS__))
+#define CATCH_TEMPLATE_TEST_CASE_METHOD_SIG(className, ...)                                        \
+    INTERNAL_CATCH_EXPAND_VARGS(                                                                   \
+        INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG(className, __VA_ARGS__))
+#define CATCH_TEMPLATE_PRODUCT_TEST_CASE(...)                                                      \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE(__VA_ARGS__))
+#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG(...)                                                  \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG(__VA_ARGS__))
+#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD(className, ...)                                    \
+    INTERNAL_CATCH_EXPAND_VARGS(                                                                   \
+        INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD(className, __VA_ARGS__))
+#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG(className, ...)                                \
+    INTERNAL_CATCH_EXPAND_VARGS(                                                                   \
+        INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG(className, __VA_ARGS__))
+#endif
+
+#if !defined(CATCH_CONFIG_RUNTIME_STATIC_REQUIRE)
+#define CATCH_STATIC_REQUIRE(...)                                                                  \
+    static_assert(__VA_ARGS__, #__VA_ARGS__);                                                      \
+    CATCH_SUCCEED(#__VA_ARGS__)
+#define CATCH_STATIC_REQUIRE_FALSE(...)                                                            \
+    static_assert(!(__VA_ARGS__), "!(" #__VA_ARGS__ ")");                                          \
+    CATCH_SUCCEED(#__VA_ARGS__)
+#else
+#define CATCH_STATIC_REQUIRE(...) CATCH_REQUIRE(__VA_ARGS__)
+#define CATCH_STATIC_REQUIRE_FALSE(...) CATCH_REQUIRE_FALSE(__VA_ARGS__)
+#endif
+
+// "BDD-style" convenience wrappers
+#define CATCH_SCENARIO(...) CATCH_TEST_CASE("Scenario: " __VA_ARGS__)
+#define CATCH_SCENARIO_METHOD(className, ...)                                                      \
+    INTERNAL_CATCH_TEST_CASE_METHOD(className, "Scenario: " __VA_ARGS__)
+#define CATCH_GIVEN(desc) INTERNAL_CATCH_DYNAMIC_SECTION("    Given: " << desc)
+#define CATCH_AND_GIVEN(desc) INTERNAL_CATCH_DYNAMIC_SECTION("And given: " << desc)
+#define CATCH_WHEN(desc) INTERNAL_CATCH_DYNAMIC_SECTION("     When: " << desc)
+#define CATCH_AND_WHEN(desc) INTERNAL_CATCH_DYNAMIC_SECTION(" And when: " << desc)
+#define CATCH_THEN(desc) INTERNAL_CATCH_DYNAMIC_SECTION("     Then: " << desc)
+#define CATCH_AND_THEN(desc) INTERNAL_CATCH_DYNAMIC_SECTION("      And: " << desc)
+
+#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING)
+#define CATCH_BENCHMARK(...)                                                                       \
+    INTERNAL_CATCH_BENCHMARK(INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____B_E_N_C_H____),           \
+                             INTERNAL_CATCH_GET_1_ARG(__VA_ARGS__, , ),                            \
+                             INTERNAL_CATCH_GET_2_ARG(__VA_ARGS__, , ))
+#define CATCH_BENCHMARK_ADVANCED(name)                                                             \
+    INTERNAL_CATCH_BENCHMARK_ADVANCED(INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____B_E_N_C_H____),  \
+                                      name)
+#endif // CATCH_CONFIG_ENABLE_BENCHMARKING
+
+// If CATCH_CONFIG_PREFIX_ALL is not defined then the CATCH_ prefix is not
+// required
+#else
+
+#define REQUIRE(...) INTERNAL_CATCH_TEST("REQUIRE", Catch::ResultDisposition::Normal, __VA_ARGS__)
+#define REQUIRE_FALSE(...)                                                                         \
+    INTERNAL_CATCH_TEST("REQUIRE_FALSE",                                                           \
+                        Catch::ResultDisposition::Normal | Catch::ResultDisposition::FalseTest,    \
+                        __VA_ARGS__)
+
+#define REQUIRE_THROWS(...)                                                                        \
+    INTERNAL_CATCH_THROWS("REQUIRE_THROWS", Catch::ResultDisposition::Normal, __VA_ARGS__)
+#define REQUIRE_THROWS_AS(expr, exceptionType)                                                     \
+    INTERNAL_CATCH_THROWS_AS(                                                                      \
+        "REQUIRE_THROWS_AS", exceptionType, Catch::ResultDisposition::Normal, expr)
+#define REQUIRE_THROWS_WITH(expr, matcher)                                                         \
+    INTERNAL_CATCH_THROWS_STR_MATCHES(                                                             \
+        "REQUIRE_THROWS_WITH", Catch::ResultDisposition::Normal, matcher, expr)
+#if !defined(CATCH_CONFIG_DISABLE_MATCHERS)
+#define REQUIRE_THROWS_MATCHES(expr, exceptionType, matcher)                                       \
+    INTERNAL_CATCH_THROWS_MATCHES(                                                                 \
+        "REQUIRE_THROWS_MATCHES", exceptionType, Catch::ResultDisposition::Normal, matcher, expr)
+#endif // CATCH_CONFIG_DISABLE_MATCHERS
+#define REQUIRE_NOTHROW(...)                                                                       \
+    INTERNAL_CATCH_NO_THROW("REQUIRE_NOTHROW", Catch::ResultDisposition::Normal, __VA_ARGS__)
+
+#define CHECK(...)                                                                                 \
+    INTERNAL_CATCH_TEST("CHECK", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__)
+#define CHECK_FALSE(...)                                                                           \
+    INTERNAL_CATCH_TEST("CHECK_FALSE",                                                             \
+                        Catch::ResultDisposition::ContinueOnFailure |                              \
+                            Catch::ResultDisposition::FalseTest,                                   \
+                        __VA_ARGS__)
+#define CHECKED_IF(...)                                                                            \
+    INTERNAL_CATCH_IF("CHECKED_IF", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__)
+#define CHECKED_ELSE(...)                                                                          \
+    INTERNAL_CATCH_ELSE("CHECKED_ELSE", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__)
+#define CHECK_NOFAIL(...)                                                                          \
+    INTERNAL_CATCH_TEST("CHECK_NOFAIL",                                                            \
+                        Catch::ResultDisposition::ContinueOnFailure |                              \
+                            Catch::ResultDisposition::SuppressFail,                                \
+                        __VA_ARGS__)
+
+#define CHECK_THROWS(...)                                                                          \
+    INTERNAL_CATCH_THROWS("CHECK_THROWS", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__)
+#define CHECK_THROWS_AS(expr, exceptionType)                                                       \
+    INTERNAL_CATCH_THROWS_AS(                                                                      \
+        "CHECK_THROWS_AS", exceptionType, Catch::ResultDisposition::ContinueOnFailure, expr)
+#define CHECK_THROWS_WITH(expr, matcher)                                                           \
+    INTERNAL_CATCH_THROWS_STR_MATCHES(                                                             \
+        "CHECK_THROWS_WITH", Catch::ResultDisposition::ContinueOnFailure, matcher, expr)
+#if !defined(CATCH_CONFIG_DISABLE_MATCHERS)
+#define CHECK_THROWS_MATCHES(expr, exceptionType, matcher)                                         \
+    INTERNAL_CATCH_THROWS_MATCHES("CHECK_THROWS_MATCHES",                                          \
+                                  exceptionType,                                                   \
+                                  Catch::ResultDisposition::ContinueOnFailure,                     \
+                                  matcher,                                                         \
+                                  expr)
+#endif // CATCH_CONFIG_DISABLE_MATCHERS
+#define CHECK_NOTHROW(...)                                                                         \
+    INTERNAL_CATCH_NO_THROW(                                                                       \
+        "CHECK_NOTHROW", Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__)
+
+#if !defined(CATCH_CONFIG_DISABLE_MATCHERS)
+#define CHECK_THAT(arg, matcher)                                                                   \
+    INTERNAL_CHECK_THAT("CHECK_THAT", matcher, Catch::ResultDisposition::ContinueOnFailure, arg)
+
+#define REQUIRE_THAT(arg, matcher)                                                                 \
+    INTERNAL_CHECK_THAT("REQUIRE_THAT", matcher, Catch::ResultDisposition::Normal, arg)
+#endif // CATCH_CONFIG_DISABLE_MATCHERS
+
+#define INFO(msg) INTERNAL_CATCH_INFO("INFO", msg)
+#define UNSCOPED_INFO(msg) INTERNAL_CATCH_UNSCOPED_INFO("UNSCOPED_INFO", msg)
+#define WARN(msg)                                                                                  \
+    INTERNAL_CATCH_MSG(                                                                            \
+        "WARN", Catch::ResultWas::Warning, Catch::ResultDisposition::ContinueOnFailure, msg)
+#define CAPTURE(...)                                                                               \
+    INTERNAL_CATCH_CAPTURE(INTERNAL_CATCH_UNIQUE_NAME(capturer), "CAPTURE", __VA_ARGS__)
+
+#define TEST_CASE(...) INTERNAL_CATCH_TESTCASE(__VA_ARGS__)
+#define TEST_CASE_METHOD(className, ...) INTERNAL_CATCH_TEST_CASE_METHOD(className, __VA_ARGS__)
+#define METHOD_AS_TEST_CASE(method, ...) INTERNAL_CATCH_METHOD_AS_TEST_CASE(method, __VA_ARGS__)
+#define REGISTER_TEST_CASE(Function, ...) INTERNAL_CATCH_REGISTER_TESTCASE(Function, __VA_ARGS__)
+#define SECTION(...) INTERNAL_CATCH_SECTION(__VA_ARGS__)
+#define DYNAMIC_SECTION(...) INTERNAL_CATCH_DYNAMIC_SECTION(__VA_ARGS__)
+#define FAIL(...)                                                                                  \
+    INTERNAL_CATCH_MSG(                                                                            \
+        "FAIL", Catch::ResultWas::ExplicitFailure, Catch::ResultDisposition::Normal, __VA_ARGS__)
+#define FAIL_CHECK(...)                                                                            \
+    INTERNAL_CATCH_MSG("FAIL_CHECK",                                                               \
+                       Catch::ResultWas::ExplicitFailure,                                          \
+                       Catch::ResultDisposition::ContinueOnFailure,                                \
+                       __VA_ARGS__)
+#define SUCCEED(...)                                                                               \
+    INTERNAL_CATCH_MSG(                                                                            \
+        "SUCCEED", Catch::ResultWas::Ok, Catch::ResultDisposition::ContinueOnFailure, __VA_ARGS__)
+#define ANON_TEST_CASE() INTERNAL_CATCH_TESTCASE()
+
+#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#define TEMPLATE_TEST_CASE(...) INTERNAL_CATCH_TEMPLATE_TEST_CASE(__VA_ARGS__)
+#define TEMPLATE_TEST_CASE_SIG(...) INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG(__VA_ARGS__)
+#define TEMPLATE_TEST_CASE_METHOD(className, ...)                                                  \
+    INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD(className, __VA_ARGS__)
+#define TEMPLATE_TEST_CASE_METHOD_SIG(className, ...)                                              \
+    INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG(className, __VA_ARGS__)
+#define TEMPLATE_PRODUCT_TEST_CASE(...) INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE(__VA_ARGS__)
+#define TEMPLATE_PRODUCT_TEST_CASE_SIG(...)                                                        \
+    INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG(__VA_ARGS__)
+#define TEMPLATE_PRODUCT_TEST_CASE_METHOD(className, ...)                                          \
+    INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD(className, __VA_ARGS__)
+#define TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG(className, ...)                                      \
+    INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG(className, __VA_ARGS__)
+#define TEMPLATE_LIST_TEST_CASE(...) INTERNAL_CATCH_TEMPLATE_LIST_TEST_CASE(__VA_ARGS__)
+#define TEMPLATE_LIST_TEST_CASE_METHOD(className, ...)                                             \
+    INTERNAL_CATCH_TEMPLATE_LIST_TEST_CASE_METHOD(className, __VA_ARGS__)
+#else
+#define TEMPLATE_TEST_CASE(...)                                                                    \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_TEST_CASE(__VA_ARGS__))
+#define TEMPLATE_TEST_CASE_SIG(...)                                                                \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG(__VA_ARGS__))
+#define TEMPLATE_TEST_CASE_METHOD(className, ...)                                                  \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD(className, __VA_ARGS__))
+#define TEMPLATE_TEST_CASE_METHOD_SIG(className, ...)                                              \
+    INTERNAL_CATCH_EXPAND_VARGS(                                                                   \
+        INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG(className, __VA_ARGS__))
+#define TEMPLATE_PRODUCT_TEST_CASE(...)                                                            \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE(__VA_ARGS__))
+#define TEMPLATE_PRODUCT_TEST_CASE_SIG(...)                                                        \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG(__VA_ARGS__))
+#define TEMPLATE_PRODUCT_TEST_CASE_METHOD(className, ...)                                          \
+    INTERNAL_CATCH_EXPAND_VARGS(                                                                   \
+        INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD(className, __VA_ARGS__))
+#define TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG(className, ...)                                      \
+    INTERNAL_CATCH_EXPAND_VARGS(                                                                   \
+        INTERNAL_CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG(className, __VA_ARGS__))
+#define TEMPLATE_LIST_TEST_CASE(...)                                                               \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_LIST_TEST_CASE(__VA_ARGS__))
+#define TEMPLATE_LIST_TEST_CASE_METHOD(className, ...)                                             \
+    INTERNAL_CATCH_EXPAND_VARGS(                                                                   \
+        INTERNAL_CATCH_TEMPLATE_LIST_TEST_CASE_METHOD(className, __VA_ARGS__))
+#endif
+
+#if !defined(CATCH_CONFIG_RUNTIME_STATIC_REQUIRE)
+#define STATIC_REQUIRE(...)                                                                        \
+    static_assert(__VA_ARGS__, #__VA_ARGS__);                                                      \
+    SUCCEED(#__VA_ARGS__)
+#define STATIC_REQUIRE_FALSE(...)                                                                  \
+    static_assert(!(__VA_ARGS__), "!(" #__VA_ARGS__ ")");                                          \
+    SUCCEED("!(" #__VA_ARGS__ ")")
+#else
+#define STATIC_REQUIRE(...) REQUIRE(__VA_ARGS__)
+#define STATIC_REQUIRE_FALSE(...) REQUIRE_FALSE(__VA_ARGS__)
+#endif
+
+#endif
+
+#define CATCH_TRANSLATE_EXCEPTION(signature) INTERNAL_CATCH_TRANSLATE_EXCEPTION(signature)
+
+// "BDD-style" convenience wrappers
+#define SCENARIO(...) TEST_CASE("Scenario: " __VA_ARGS__)
+#define SCENARIO_METHOD(className, ...)                                                            \
+    INTERNAL_CATCH_TEST_CASE_METHOD(className, "Scenario: " __VA_ARGS__)
+
+#define GIVEN(desc) INTERNAL_CATCH_DYNAMIC_SECTION("    Given: " << desc)
+#define AND_GIVEN(desc) INTERNAL_CATCH_DYNAMIC_SECTION("And given: " << desc)
+#define WHEN(desc) INTERNAL_CATCH_DYNAMIC_SECTION("     When: " << desc)
+#define AND_WHEN(desc) INTERNAL_CATCH_DYNAMIC_SECTION(" And when: " << desc)
+#define THEN(desc) INTERNAL_CATCH_DYNAMIC_SECTION("     Then: " << desc)
+#define AND_THEN(desc) INTERNAL_CATCH_DYNAMIC_SECTION("      And: " << desc)
+
+#if defined(CATCH_CONFIG_ENABLE_BENCHMARKING)
+#define BENCHMARK(...)                                                                             \
+    INTERNAL_CATCH_BENCHMARK(INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____B_E_N_C_H____),           \
+                             INTERNAL_CATCH_GET_1_ARG(__VA_ARGS__, , ),                            \
+                             INTERNAL_CATCH_GET_2_ARG(__VA_ARGS__, , ))
+#define BENCHMARK_ADVANCED(name)                                                                   \
+    INTERNAL_CATCH_BENCHMARK_ADVANCED(INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____B_E_N_C_H____),  \
+                                      name)
+#endif // CATCH_CONFIG_ENABLE_BENCHMARKING
+
+using Catch::Detail::Approx;
+
+#else // CATCH_CONFIG_DISABLE
+
+//////
+// If this config identifier is defined then all CATCH macros are prefixed with
+// CATCH_
+#ifdef CATCH_CONFIG_PREFIX_ALL
+
+#define CATCH_REQUIRE(...) (void)(0)
+#define CATCH_REQUIRE_FALSE(...) (void)(0)
+
+#define CATCH_REQUIRE_THROWS(...) (void)(0)
+#define CATCH_REQUIRE_THROWS_AS(expr, exceptionType) (void)(0)
+#define CATCH_REQUIRE_THROWS_WITH(expr, matcher) (void)(0)
+#if !defined(CATCH_CONFIG_DISABLE_MATCHERS)
+#define CATCH_REQUIRE_THROWS_MATCHES(expr, exceptionType, matcher) (void)(0)
+#endif // CATCH_CONFIG_DISABLE_MATCHERS
+#define CATCH_REQUIRE_NOTHROW(...) (void)(0)
+
+#define CATCH_CHECK(...) (void)(0)
+#define CATCH_CHECK_FALSE(...) (void)(0)
+#define CATCH_CHECKED_IF(...) if (__VA_ARGS__)
+#define CATCH_CHECKED_ELSE(...) if (!(__VA_ARGS__))
+#define CATCH_CHECK_NOFAIL(...) (void)(0)
+
+#define CATCH_CHECK_THROWS(...) (void)(0)
+#define CATCH_CHECK_THROWS_AS(expr, exceptionType) (void)(0)
+#define CATCH_CHECK_THROWS_WITH(expr, matcher) (void)(0)
+#if !defined(CATCH_CONFIG_DISABLE_MATCHERS)
+#define CATCH_CHECK_THROWS_MATCHES(expr, exceptionType, matcher) (void)(0)
+#endif // CATCH_CONFIG_DISABLE_MATCHERS
+#define CATCH_CHECK_NOTHROW(...) (void)(0)
+
+#if !defined(CATCH_CONFIG_DISABLE_MATCHERS)
+#define CATCH_CHECK_THAT(arg, matcher) (void)(0)
+
+#define CATCH_REQUIRE_THAT(arg, matcher) (void)(0)
+#endif // CATCH_CONFIG_DISABLE_MATCHERS
+
+#define CATCH_INFO(msg) (void)(0)
+#define CATCH_UNSCOPED_INFO(msg) (void)(0)
+#define CATCH_WARN(msg) (void)(0)
+#define CATCH_CAPTURE(msg) (void)(0)
+
+#define CATCH_TEST_CASE(...)                                                                       \
+    INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(                                                       \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_S_T____))
+#define CATCH_TEST_CASE_METHOD(className, ...)                                                     \
+    INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(                                                       \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_S_T____))
+#define CATCH_METHOD_AS_TEST_CASE(method, ...)
+#define CATCH_REGISTER_TEST_CASE(Function, ...) (void)(0)
+#define CATCH_SECTION(...)
+#define CATCH_DYNAMIC_SECTION(...)
+#define CATCH_FAIL(...) (void)(0)
+#define CATCH_FAIL_CHECK(...) (void)(0)
+#define CATCH_SUCCEED(...) (void)(0)
+
+#define CATCH_ANON_TEST_CASE()                                                                     \
+    INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(                                                       \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_S_T____))
+
+#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#define CATCH_TEMPLATE_TEST_CASE(...) INTERNAL_CATCH_TEMPLATE_TEST_CASE_NO_REGISTRATION(__VA_ARGS__)
+#define CATCH_TEMPLATE_TEST_CASE_SIG(...)                                                          \
+    INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG_NO_REGISTRATION(__VA_ARGS__)
+#define CATCH_TEMPLATE_TEST_CASE_METHOD(className, ...)                                            \
+    INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_NO_REGISTRATION(className, __VA_ARGS__)
+#define CATCH_TEMPLATE_TEST_CASE_METHOD_SIG(className, ...)                                        \
+    INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG_NO_REGISTRATION(className, __VA_ARGS__)
+#define CATCH_TEMPLATE_PRODUCT_TEST_CASE(...) CATCH_TEMPLATE_TEST_CASE(__VA_ARGS__)
+#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG(...) CATCH_TEMPLATE_TEST_CASE(__VA_ARGS__)
+#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD(className, ...)                                    \
+    CATCH_TEMPLATE_TEST_CASE_METHOD(className, __VA_ARGS__)
+#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG(className, ...)                                \
+    CATCH_TEMPLATE_TEST_CASE_METHOD(className, __VA_ARGS__)
+#else
+#define CATCH_TEMPLATE_TEST_CASE(...)                                                              \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_TEST_CASE_NO_REGISTRATION(__VA_ARGS__))
+#define CATCH_TEMPLATE_TEST_CASE_SIG(...)                                                          \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG_NO_REGISTRATION(__VA_ARGS__))
+#define CATCH_TEMPLATE_TEST_CASE_METHOD(className, ...)                                            \
+    INTERNAL_CATCH_EXPAND_VARGS(                                                                   \
+        INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_NO_REGISTRATION(className, __VA_ARGS__))
+#define CATCH_TEMPLATE_TEST_CASE_METHOD_SIG(className, ...)                                        \
+    INTERNAL_CATCH_EXPAND_VARGS(                                                                   \
+        INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG_NO_REGISTRATION(className, __VA_ARGS__))
+#define CATCH_TEMPLATE_PRODUCT_TEST_CASE(...) CATCH_TEMPLATE_TEST_CASE(__VA_ARGS__)
+#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_SIG(...) CATCH_TEMPLATE_TEST_CASE(__VA_ARGS__)
+#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD(className, ...)                                    \
+    CATCH_TEMPLATE_TEST_CASE_METHOD(className, __VA_ARGS__)
+#define CATCH_TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG(className, ...)                                \
+    CATCH_TEMPLATE_TEST_CASE_METHOD(className, __VA_ARGS__)
+#endif
+
+// "BDD-style" convenience wrappers
+#define CATCH_SCENARIO(...)                                                                        \
+    INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(                                                       \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_S_T____))
+#define CATCH_SCENARIO_METHOD(className, ...)                                                      \
+    INTERNAL_CATCH_TESTCASE_METHOD_NO_REGISTRATION(                                                \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_S_T____), className)
+#define CATCH_GIVEN(desc)
+#define CATCH_AND_GIVEN(desc)
+#define CATCH_WHEN(desc)
+#define CATCH_AND_WHEN(desc)
+#define CATCH_THEN(desc)
+#define CATCH_AND_THEN(desc)
+
+#define CATCH_STATIC_REQUIRE(...) (void)(0)
+#define CATCH_STATIC_REQUIRE_FALSE(...) (void)(0)
+
+// If CATCH_CONFIG_PREFIX_ALL is not defined then the CATCH_ prefix is not
+// required
+#else
+
+#define REQUIRE(...) (void)(0)
+#define REQUIRE_FALSE(...) (void)(0)
+
+#define REQUIRE_THROWS(...) (void)(0)
+#define REQUIRE_THROWS_AS(expr, exceptionType) (void)(0)
+#define REQUIRE_THROWS_WITH(expr, matcher) (void)(0)
+#if !defined(CATCH_CONFIG_DISABLE_MATCHERS)
+#define REQUIRE_THROWS_MATCHES(expr, exceptionType, matcher) (void)(0)
+#endif // CATCH_CONFIG_DISABLE_MATCHERS
+#define REQUIRE_NOTHROW(...) (void)(0)
+
+#define CHECK(...) (void)(0)
+#define CHECK_FALSE(...) (void)(0)
+#define CHECKED_IF(...) if (__VA_ARGS__)
+#define CHECKED_ELSE(...) if (!(__VA_ARGS__))
+#define CHECK_NOFAIL(...) (void)(0)
+
+#define CHECK_THROWS(...) (void)(0)
+#define CHECK_THROWS_AS(expr, exceptionType) (void)(0)
+#define CHECK_THROWS_WITH(expr, matcher) (void)(0)
+#if !defined(CATCH_CONFIG_DISABLE_MATCHERS)
+#define CHECK_THROWS_MATCHES(expr, exceptionType, matcher) (void)(0)
+#endif // CATCH_CONFIG_DISABLE_MATCHERS
+#define CHECK_NOTHROW(...) (void)(0)
+
+#if !defined(CATCH_CONFIG_DISABLE_MATCHERS)
+#define CHECK_THAT(arg, matcher) (void)(0)
+
+#define REQUIRE_THAT(arg, matcher) (void)(0)
+#endif // CATCH_CONFIG_DISABLE_MATCHERS
+
+#define INFO(msg) (void)(0)
+#define UNSCOPED_INFO(msg) (void)(0)
+#define WARN(msg) (void)(0)
+#define CAPTURE(msg) (void)(0)
+
+#define TEST_CASE(...)                                                                             \
+    INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(                                                       \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_S_T____))
+#define TEST_CASE_METHOD(className, ...)                                                           \
+    INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(                                                       \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_S_T____))
+#define METHOD_AS_TEST_CASE(method, ...)
+#define REGISTER_TEST_CASE(Function, ...) (void)(0)
+#define SECTION(...)
+#define DYNAMIC_SECTION(...)
+#define FAIL(...) (void)(0)
+#define FAIL_CHECK(...) (void)(0)
+#define SUCCEED(...) (void)(0)
+#define ANON_TEST_CASE()                                                                           \
+    INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(                                                       \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_S_T____))
+
+#ifndef CATCH_CONFIG_TRADITIONAL_MSVC_PREPROCESSOR
+#define TEMPLATE_TEST_CASE(...) INTERNAL_CATCH_TEMPLATE_TEST_CASE_NO_REGISTRATION(__VA_ARGS__)
+#define TEMPLATE_TEST_CASE_SIG(...)                                                                \
+    INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG_NO_REGISTRATION(__VA_ARGS__)
+#define TEMPLATE_TEST_CASE_METHOD(className, ...)                                                  \
+    INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_NO_REGISTRATION(className, __VA_ARGS__)
+#define TEMPLATE_TEST_CASE_METHOD_SIG(className, ...)                                              \
+    INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG_NO_REGISTRATION(className, __VA_ARGS__)
+#define TEMPLATE_PRODUCT_TEST_CASE(...) TEMPLATE_TEST_CASE(__VA_ARGS__)
+#define TEMPLATE_PRODUCT_TEST_CASE_SIG(...) TEMPLATE_TEST_CASE(__VA_ARGS__)
+#define TEMPLATE_PRODUCT_TEST_CASE_METHOD(className, ...)                                          \
+    TEMPLATE_TEST_CASE_METHOD(className, __VA_ARGS__)
+#define TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG(className, ...)                                      \
+    TEMPLATE_TEST_CASE_METHOD(className, __VA_ARGS__)
+#else
+#define TEMPLATE_TEST_CASE(...)                                                                    \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_TEST_CASE_NO_REGISTRATION(__VA_ARGS__))
+#define TEMPLATE_TEST_CASE_SIG(...)                                                                \
+    INTERNAL_CATCH_EXPAND_VARGS(INTERNAL_CATCH_TEMPLATE_TEST_CASE_SIG_NO_REGISTRATION(__VA_ARGS__))
+#define TEMPLATE_TEST_CASE_METHOD(className, ...)                                                  \
+    INTERNAL_CATCH_EXPAND_VARGS(                                                                   \
+        INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_NO_REGISTRATION(className, __VA_ARGS__))
+#define TEMPLATE_TEST_CASE_METHOD_SIG(className, ...)                                              \
+    INTERNAL_CATCH_EXPAND_VARGS(                                                                   \
+        INTERNAL_CATCH_TEMPLATE_TEST_CASE_METHOD_SIG_NO_REGISTRATION(className, __VA_ARGS__))
+#define TEMPLATE_PRODUCT_TEST_CASE(...) TEMPLATE_TEST_CASE(__VA_ARGS__)
+#define TEMPLATE_PRODUCT_TEST_CASE_SIG(...) TEMPLATE_TEST_CASE(__VA_ARGS__)
+#define TEMPLATE_PRODUCT_TEST_CASE_METHOD(className, ...)                                          \
+    TEMPLATE_TEST_CASE_METHOD(className, __VA_ARGS__)
+#define TEMPLATE_PRODUCT_TEST_CASE_METHOD_SIG(className, ...)                                      \
+    TEMPLATE_TEST_CASE_METHOD(className, __VA_ARGS__)
+#endif
+
+#define STATIC_REQUIRE(...) (void)(0)
+#define STATIC_REQUIRE_FALSE(...) (void)(0)
+
+#endif
+
+#define CATCH_TRANSLATE_EXCEPTION(signature)                                                       \
+    INTERNAL_CATCH_TRANSLATE_EXCEPTION_NO_REG(                                                     \
+        INTERNAL_CATCH_UNIQUE_NAME(catch_internal_ExceptionTranslator), signature)
+
+// "BDD-style" convenience wrappers
+#define SCENARIO(...)                                                                              \
+    INTERNAL_CATCH_TESTCASE_NO_REGISTRATION(                                                       \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_S_T____))
+#define SCENARIO_METHOD(className, ...)                                                            \
+    INTERNAL_CATCH_TESTCASE_METHOD_NO_REGISTRATION(                                                \
+        INTERNAL_CATCH_UNIQUE_NAME(____C_A_T_C_H____T_E_S_T____), className)
+
+#define GIVEN(desc)
+#define AND_GIVEN(desc)
+#define WHEN(desc)
+#define AND_WHEN(desc)
+#define THEN(desc)
+#define AND_THEN(desc)
+
+using Catch::Detail::Approx;
+
+#endif
+
+#endif // ! CATCH_CONFIG_IMPL_ONLY
+
+// start catch_reenable_warnings.h
+
+#ifdef __clang__
+#ifdef __ICC // icpc defines the __clang__ macro
+#pragma warning(pop)
+#else
+#pragma clang diagnostic pop
+#endif
+#elif defined __GNUC__
+#pragma GCC diagnostic pop
+#endif
+
+// end catch_reenable_warnings.h
+// end catch.hpp
+#endif // TWOBLUECUBES_SINGLE_INCLUDE_CATCH_HPP_INCLUDED
\ No newline at end of file
diff --git a/dev/test/premake5.lua b/dev/test/premake5.lua
new file mode 100644
index 0000000..e6e2cc7
--- /dev/null
+++ b/dev/test/premake5.lua
@@ -0,0 +1,76 @@
+dofile('rive_build_config.lua')
+
+defines({
+    'TESTING',
+    'ENABLE_QUERY_FLAT_VERTICES',
+    'WITH_RIVE_TOOLS',
+    'WITH_RIVE_TEXT',
+    'WITH_RIVE_AUDIO',
+    'WITH_RIVE_AUDIO_TOOLS',
+    'WITH_RIVE_LAYOUT',
+    'YOGA_EXPORT=',
+})
+
+dofile(path.join(path.getabsolute('../../'), 'premake5_v2.lua'))
+dofile(path.join(path.getabsolute('../../decoders/'), 'premake5_v2.lua'))
+
+project('tests')
+do
+    kind('ConsoleApp')
+    exceptionhandling('On')
+
+    includedirs({
+        './include',
+        '../../include',
+        '../../decoders/include',
+        miniaudio,
+        yoga,
+    })
+
+    links({
+        'rive',
+        'rive_harfbuzz',
+        'rive_sheenbidi',
+        'rive_yoga',
+        'rive_decoders',
+        'libpng',
+        'zlib',
+        'libjpeg',
+        'libwebp',
+    })
+
+    files({
+        '../../test/**.cpp', -- the tests
+        '../../utils/**.cpp', -- no_op utils
+    })
+
+    filter('system:linux')
+    do
+        links({ 'dl', 'pthread' })
+    end
+    filter({ 'options:not no-harfbuzz-renames' })
+    do
+        includedirs({
+            dependencies,
+        })
+        forceincludes({ 'rive_harfbuzz_renames.h' })
+    end
+
+    filter({ 'options:not no-yoga-renames' })
+    do
+        includedirs({
+            dependencies,
+        })
+        forceincludes({ 'rive_yoga_renames.h' })
+    end
+
+    filter({ 'system:macosx' })
+    do
+        links({
+            'Foundation.framework',
+            'ImageIO.framework',
+            'CoreGraphics.framework',
+            'CoreText.framework',
+        })
+    end
+end
diff --git a/dev/update_defs.sh b/dev/update_defs.sh
new file mode 100755
index 0000000..e8d86c3
--- /dev/null
+++ b/dev/update_defs.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+rm -fR .clone_defs 2> /dev/null
+mkdir .clone_defs
+cd .clone_defs
+git init
+git remote add origin -f git@github.com:rive-app/rive.git
+git config core.sparseCheckout true
+echo '/dev/defs/*' > .git/info/sparse-checkout
+git pull origin master
+
+rm -fR ../defs
+mv dev/defs ../
+cd ..
+rm -fR .clone_defs
\ No newline at end of file
diff --git a/include/rive/animation/advanceable_state.hpp b/include/rive/animation/advanceable_state.hpp
new file mode 100644
index 0000000..58008fe
--- /dev/null
+++ b/include/rive/animation/advanceable_state.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_ADVANCEABLE_STATE_HPP_
+#define _RIVE_ADVANCEABLE_STATE_HPP_
+#include "rive/generated/animation/advanceable_state_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class AdvanceableState : public AdvanceableStateBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/animation.hpp b/include/rive/animation/animation.hpp
new file mode 100644
index 0000000..757bca8
--- /dev/null
+++ b/include/rive/animation/animation.hpp
@@ -0,0 +1,10 @@
+#ifndef _RIVE_ANIMATION_HPP_
+#define _RIVE_ANIMATION_HPP_
+#include "rive/generated/animation/animation_base.hpp"
+namespace rive
+{
+class Animation : public AnimationBase
+{};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/animation_reset.hpp b/include/rive/animation/animation_reset.hpp
new file mode 100644
index 0000000..2828da3
--- /dev/null
+++ b/include/rive/animation/animation_reset.hpp
@@ -0,0 +1,32 @@
+#ifndef _RIVE_ANIMATION_RESET_HPP_
+#define _RIVE_ANIMATION_RESET_HPP_
+
+#include <string>
+#include "rive/artboard.hpp"
+#include "rive/animation/animation_reset.hpp"
+#include "rive/core/binary_writer.hpp"
+#include "rive/core/vector_binary_writer.hpp"
+#include "rive/core/binary_data_reader.hpp"
+
+namespace rive
+{
+
+class AnimationReset
+{
+private:
+    VectorBinaryWriter m_binaryWriter;
+    BinaryDataReader m_binaryReader;
+    std::vector<uint8_t> m_WriteBuffer;
+
+public:
+    AnimationReset();
+    void writeObjectId(uint32_t objectId);
+    void writeTotalProperties(uint32_t value);
+    void writePropertyKey(uint32_t value);
+    void writePropertyValue(float value);
+    void apply(Artboard* artboard);
+    void complete();
+    void clear();
+};
+} // namespace rive
+#endif
diff --git a/include/rive/animation/animation_reset_factory.hpp b/include/rive/animation/animation_reset_factory.hpp
new file mode 100644
index 0000000..8a4db56
--- /dev/null
+++ b/include/rive/animation/animation_reset_factory.hpp
@@ -0,0 +1,40 @@
+#ifndef _RIVE_ANIMATION_RESET_FACTORY_HPP_
+#define _RIVE_ANIMATION_RESET_FACTORY_HPP_
+
+#include <string>
+#include <mutex>
+#include "rive/animation/animation_reset.hpp"
+#include "rive/animation/state_instance.hpp"
+#include "rive/animation/linear_animation.hpp"
+#include "rive/artboard.hpp"
+
+namespace rive
+{
+
+class AnimationResetFactory
+{
+    static std::vector<std::unique_ptr<AnimationReset>> m_resources;
+    static std::mutex m_mutex;
+
+private:
+    static void fromState(StateInstance* stateInstance,
+                          std::vector<const LinearAnimation*>& animations);
+
+public:
+    static std::unique_ptr<AnimationReset> getInstance();
+    static std::unique_ptr<AnimationReset> fromStates(StateInstance* stateFrom,
+                                                      StateInstance* currentState,
+                                                      ArtboardInstance* artboard);
+    static std::unique_ptr<AnimationReset> fromAnimations(
+        std::vector<const LinearAnimation*>& animations,
+        ArtboardInstance* artboard,
+        bool useFirstAsBaseline);
+    static void release(std::unique_ptr<AnimationReset> value);
+#ifdef TESTING
+    // Used in testing to check pooled resources;
+    static int resourcesCount() { return m_resources.size(); };
+    static void releaseResources() { m_resources.clear(); };
+#endif
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/animation_state.hpp b/include/rive/animation/animation_state.hpp
new file mode 100644
index 0000000..67a60a4
--- /dev/null
+++ b/include/rive/animation/animation_state.hpp
@@ -0,0 +1,29 @@
+#ifndef _RIVE_ANIMATION_STATE_HPP_
+#define _RIVE_ANIMATION_STATE_HPP_
+#include "rive/generated/animation/animation_state_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class LinearAnimation;
+class ArtboardInstance;
+class StateMachineLayerImporter;
+
+class AnimationState : public AnimationStateBase
+{
+    friend class StateMachineLayerImporter;
+
+private:
+    LinearAnimation* m_Animation = nullptr;
+
+public:
+    const LinearAnimation* animation() const { return m_Animation; }
+
+#ifdef TESTING
+    void animation(LinearAnimation* animation);
+#endif
+
+    std::unique_ptr<StateInstance> makeInstance(ArtboardInstance*) const override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/animation_state_instance.hpp b/include/rive/animation/animation_state_instance.hpp
new file mode 100644
index 0000000..7e73343
--- /dev/null
+++ b/include/rive/animation/animation_state_instance.hpp
@@ -0,0 +1,33 @@
+#ifndef _RIVE_ANIMATION_STATE_INSTANCE_HPP_
+#define _RIVE_ANIMATION_STATE_INSTANCE_HPP_
+
+#include <string>
+#include "rive/animation/state_instance.hpp"
+#include "rive/animation/linear_animation_instance.hpp"
+
+namespace rive
+{
+class AnimationState;
+
+/// Represents an instance of an animation state.
+class AnimationStateInstance : public StateInstance
+{
+private:
+    LinearAnimationInstance m_AnimationInstance;
+    bool m_KeepGoing;
+
+public:
+    AnimationStateInstance(const AnimationState* animationState, ArtboardInstance* instance);
+
+    void advance(float seconds, StateMachineInstance* stateMachineInstance) override;
+    void apply(ArtboardInstance* instance, float mix) override;
+
+    bool keepGoing() const override;
+    void clearSpilledTime() override;
+
+    const LinearAnimationInstance* animationInstance() const { return &m_AnimationInstance; }
+
+    LinearAnimationInstance* animationInstance() { return &m_AnimationInstance; }
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/any_state.hpp b/include/rive/animation/any_state.hpp
new file mode 100644
index 0000000..bd07c14
--- /dev/null
+++ b/include/rive/animation/any_state.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_ANY_STATE_HPP_
+#define _RIVE_ANY_STATE_HPP_
+#include "rive/generated/animation/any_state_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class AnyState : public AnyStateBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/arithmetic_operation.hpp b/include/rive/animation/arithmetic_operation.hpp
new file mode 100644
index 0000000..1870b8d
--- /dev/null
+++ b/include/rive/animation/arithmetic_operation.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_ARITHMETIC_OPERATION_HPP_
+#define _RIVE_ARITHMETIC_OPERATION_HPP_
+
+namespace rive
+{
+enum class ArithmeticOperation : int
+{
+    add = 0,
+    subtract = 1,
+    multiply = 2,
+    divide = 3,
+    modulo = 4,
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/artboard_property.hpp b/include/rive/animation/artboard_property.hpp
new file mode 100644
index 0000000..0a4d606
--- /dev/null
+++ b/include/rive/animation/artboard_property.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_ARTBOARD_PROPERTY_HPP_
+#define _RIVE_ARTBOARD_PROPERTY_HPP_
+
+namespace rive
+{
+enum class ArtboardProperty : int
+{
+    width = 0,
+    height = 1,
+    ratio = 2,
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/blend_animation.hpp b/include/rive/animation/blend_animation.hpp
new file mode 100644
index 0000000..1e0b88a
--- /dev/null
+++ b/include/rive/animation/blend_animation.hpp
@@ -0,0 +1,22 @@
+#ifndef _RIVE_BLEND_ANIMATION_HPP_
+#define _RIVE_BLEND_ANIMATION_HPP_
+#include "rive/generated/animation/blend_animation_base.hpp"
+namespace rive
+{
+class LinearAnimation;
+class BlendAnimation : public BlendAnimationBase
+{
+private:
+    static LinearAnimation m_EmptyAnimation;
+    LinearAnimation* m_Animation = nullptr;
+
+public:
+    const LinearAnimation* animation() const
+    {
+        return m_Animation == nullptr ? &m_EmptyAnimation : m_Animation;
+    }
+    StatusCode import(ImportStack& importStack) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/blend_animation_1d.hpp b/include/rive/animation/blend_animation_1d.hpp
new file mode 100644
index 0000000..42b77e1
--- /dev/null
+++ b/include/rive/animation/blend_animation_1d.hpp
@@ -0,0 +1,15 @@
+#ifndef _RIVE_BLEND_ANIMATION1_D_HPP_
+#define _RIVE_BLEND_ANIMATION1_D_HPP_
+#include "rive/generated/animation/blend_animation_1d_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class BlendAnimation1D : public BlendAnimation1DBase
+{
+public:
+    StatusCode onAddedDirty(CoreContext* context) override;
+    StatusCode onAddedClean(CoreContext* context) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/blend_animation_direct.hpp b/include/rive/animation/blend_animation_direct.hpp
new file mode 100644
index 0000000..f631ed5
--- /dev/null
+++ b/include/rive/animation/blend_animation_direct.hpp
@@ -0,0 +1,23 @@
+#ifndef _RIVE_BLEND_ANIMATION_DIRECT_HPP_
+#define _RIVE_BLEND_ANIMATION_DIRECT_HPP_
+#include "rive/generated/animation/blend_animation_direct_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+
+enum class DirectBlendSource : unsigned int
+{
+    inputId = 0,
+    mixValue = 1,
+};
+
+class BlendAnimationDirect : public BlendAnimationDirectBase
+{
+public:
+    StatusCode onAddedDirty(CoreContext* context) override;
+    StatusCode onAddedClean(CoreContext* context) override;
+    StatusCode import(ImportStack& importStack) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/blend_state.hpp b/include/rive/animation/blend_state.hpp
new file mode 100644
index 0000000..6dded33
--- /dev/null
+++ b/include/rive/animation/blend_state.hpp
@@ -0,0 +1,32 @@
+#ifndef _RIVE_BLEND_STATE_HPP_
+#define _RIVE_BLEND_STATE_HPP_
+#include "rive/generated/animation/blend_state_base.hpp"
+#include <stdio.h>
+#include <vector>
+#include <algorithm>
+
+namespace rive
+{
+class BlendAnimation;
+class LayerStateImporter;
+
+class BlendState : public BlendStateBase
+{
+    friend class LayerStateImporter;
+
+private:
+    std::vector<BlendAnimation*> m_Animations;
+    void addAnimation(BlendAnimation* animation);
+
+public:
+    ~BlendState() override;
+    inline const std::vector<BlendAnimation*>& animations() const { return m_Animations; }
+
+#ifdef TESTING
+    size_t animationCount() { return m_Animations.size(); }
+    BlendAnimation* animation(size_t index) { return m_Animations[index]; }
+#endif
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/animation/blend_state_1d.hpp b/include/rive/animation/blend_state_1d.hpp
new file mode 100644
index 0000000..8b3092d
--- /dev/null
+++ b/include/rive/animation/blend_state_1d.hpp
@@ -0,0 +1,18 @@
+#ifndef _RIVE_BLEND_STATE1_D_HPP_
+#define _RIVE_BLEND_STATE1_D_HPP_
+#include "rive/generated/animation/blend_state_1d_base.hpp"
+
+namespace rive
+{
+class BlendState1D : public BlendState1DBase
+{
+public:
+    bool hasValidInputId() const { return inputId() != Core::emptyId; }
+
+    StatusCode import(ImportStack& importStack) override;
+
+    std::unique_ptr<StateInstance> makeInstance(ArtboardInstance*) const override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/blend_state_1d_instance.hpp b/include/rive/animation/blend_state_1d_instance.hpp
new file mode 100644
index 0000000..ed1df09
--- /dev/null
+++ b/include/rive/animation/blend_state_1d_instance.hpp
@@ -0,0 +1,27 @@
+#ifndef _RIVE_BLEND_STATE_1D_INSTANCE_HPP_
+#define _RIVE_BLEND_STATE_1D_INSTANCE_HPP_
+
+#include "rive/animation/blend_state_instance.hpp"
+#include "rive/animation/blend_state_1d.hpp"
+#include "rive/animation/blend_animation_1d.hpp"
+#include "rive/animation/animation_reset.hpp"
+#include "rive/animation/animation_reset_factory.hpp"
+
+namespace rive
+{
+class BlendState1DInstance : public BlendStateInstance<BlendState1D, BlendAnimation1D>
+{
+private:
+    BlendStateAnimationInstance<BlendAnimation1D>* m_From = nullptr;
+    BlendStateAnimationInstance<BlendAnimation1D>* m_To = nullptr;
+    std::unique_ptr<AnimationReset> m_AnimationReset;
+    int animationIndex(float value);
+
+public:
+    BlendState1DInstance(const BlendState1D* blendState, ArtboardInstance* instance);
+    ~BlendState1DInstance();
+    void advance(float seconds, StateMachineInstance* stateMachineInstance) override;
+    void apply(ArtboardInstance* instance, float mix) override;
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/blend_state_direct.hpp b/include/rive/animation/blend_state_direct.hpp
new file mode 100644
index 0000000..1d8707e
--- /dev/null
+++ b/include/rive/animation/blend_state_direct.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_BLEND_STATE_DIRECT_HPP_
+#define _RIVE_BLEND_STATE_DIRECT_HPP_
+#include "rive/generated/animation/blend_state_direct_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class BlendStateDirect : public BlendStateDirectBase
+{
+public:
+    std::unique_ptr<StateInstance> makeInstance(ArtboardInstance*) const override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/blend_state_direct_instance.hpp b/include/rive/animation/blend_state_direct_instance.hpp
new file mode 100644
index 0000000..75fcfdf
--- /dev/null
+++ b/include/rive/animation/blend_state_direct_instance.hpp
@@ -0,0 +1,17 @@
+#ifndef _RIVE_BLEND_STATE_DIRECT_INSTANCE_HPP_
+#define _RIVE_BLEND_STATE_DIRECT_INSTANCE_HPP_
+
+#include "rive/animation/blend_state_instance.hpp"
+#include "rive/animation/blend_state_direct.hpp"
+#include "rive/animation/blend_animation_direct.hpp"
+
+namespace rive
+{
+class BlendStateDirectInstance : public BlendStateInstance<BlendStateDirect, BlendAnimationDirect>
+{
+public:
+    BlendStateDirectInstance(const BlendStateDirect* blendState, ArtboardInstance* instance);
+    void advance(float seconds, StateMachineInstance* stateMachineInstance) override;
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/blend_state_instance.hpp b/include/rive/animation/blend_state_instance.hpp
new file mode 100644
index 0000000..bf34466
--- /dev/null
+++ b/include/rive/animation/blend_state_instance.hpp
@@ -0,0 +1,113 @@
+#ifndef _RIVE_BLEND_STATE_INSTANCE_HPP_
+#define _RIVE_BLEND_STATE_INSTANCE_HPP_
+
+#include <string>
+#include <vector>
+#include "rive/animation/state_instance.hpp"
+#include "rive/animation/blend_state.hpp"
+#include "rive/animation/layer_state_flags.hpp"
+#include "rive/animation/linear_animation_instance.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/animation/animation_reset.hpp"
+#include "rive/animation/animation_reset_factory.hpp"
+
+namespace rive
+{
+class AnimationState;
+
+template <class K, class T> class BlendStateInstance;
+template <class T> class BlendStateAnimationInstance
+{
+    template <class A, class B> friend class BlendStateInstance;
+
+private:
+    const T* m_BlendAnimation;
+    LinearAnimationInstance m_AnimationInstance;
+    float m_Mix = 0.0f;
+
+public:
+    const T* blendAnimation() const { return m_BlendAnimation; }
+    const LinearAnimationInstance* animationInstance() const { return &m_AnimationInstance; }
+
+    BlendStateAnimationInstance(const T* blendAnimation, ArtboardInstance* instance) :
+        m_BlendAnimation(blendAnimation), m_AnimationInstance(blendAnimation->animation(), instance)
+    {}
+
+    void mix(float value) { m_Mix = value; }
+};
+
+template <class K, class T> class BlendStateInstance : public StateInstance
+{
+protected:
+    std::vector<BlendStateAnimationInstance<T>> m_AnimationInstances;
+    bool m_KeepGoing = true;
+
+public:
+    BlendStateInstance(const K* blendState, ArtboardInstance* instance) : StateInstance(blendState)
+    {
+        m_AnimationInstances.reserve(blendState->animations().size());
+
+        for (auto blendAnimation : blendState->animations())
+        {
+            m_AnimationInstances.emplace_back(
+                BlendStateAnimationInstance<T>(static_cast<T*>(blendAnimation), instance));
+        }
+        if ((static_cast<LayerStateFlags>(blendState->flags()) & LayerStateFlags::Reset) ==
+            LayerStateFlags::Reset)
+        {
+            auto animations = std::vector<const LinearAnimation*>();
+            for (auto blendAnimation : blendState->animations())
+            {
+                animations.push_back(blendAnimation->animation());
+            }
+        }
+    }
+
+    bool keepGoing() const override { return m_KeepGoing; }
+
+    void advance(float seconds, StateMachineInstance* stateMachineInstance) override
+    {
+        // NOTE: we are intentionally ignoring the animationInstances' keepGoing
+        // return value.
+        // Blend states need to keep blending forever, as even if the animation
+        // does not change the mix values may
+        for (auto& animation : m_AnimationInstances)
+        {
+            if (animation.m_AnimationInstance.keepGoing())
+            {
+                // Should animations with m_Mix == 0.0 advance? They will
+                // trigger events and the event properties (if any) will not be
+                // updated by animationInstance.apply
+                animation.m_AnimationInstance.advance(seconds, stateMachineInstance);
+            }
+        }
+    }
+
+    void apply(ArtboardInstance* instance, float mix) override
+    {
+        for (auto& animation : m_AnimationInstances)
+        {
+            float m = mix * animation.m_Mix;
+            if (m == 0.0f)
+            {
+                continue;
+            }
+            animation.m_AnimationInstance.apply(m);
+        }
+    }
+
+    // Find the animationInstance that corresponds to the blendAnimation.
+    const LinearAnimationInstance* animationInstance(const BlendAnimation* blendAnimation) const
+    {
+        for (auto& animation : m_AnimationInstances)
+        {
+            if (animation.m_BlendAnimation == blendAnimation)
+            {
+                return animation.animationInstance();
+            }
+        }
+        return nullptr;
+    }
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/blend_state_transition.hpp b/include/rive/animation/blend_state_transition.hpp
new file mode 100644
index 0000000..caf316c
--- /dev/null
+++ b/include/rive/animation/blend_state_transition.hpp
@@ -0,0 +1,27 @@
+#ifndef _RIVE_BLEND_STATE_TRANSITION_HPP_
+#define _RIVE_BLEND_STATE_TRANSITION_HPP_
+#include "rive/generated/animation/blend_state_transition_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class BlendAnimation;
+class LayerStateImporter;
+class BlendStateTransition : public BlendStateTransitionBase
+{
+    friend class LayerStateImporter;
+
+private:
+    BlendAnimation* m_ExitBlendAnimation = nullptr;
+
+public:
+    BlendAnimation* exitBlendAnimation() const { return m_ExitBlendAnimation; }
+
+    const LinearAnimationInstance* exitTimeAnimationInstance(
+        const StateInstance* from) const override;
+
+    const LinearAnimation* exitTimeAnimation(const LayerState* from) const override;
+};
+
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/cubic_ease_interpolator.hpp b/include/rive/animation/cubic_ease_interpolator.hpp
new file mode 100644
index 0000000..ce68f53
--- /dev/null
+++ b/include/rive/animation/cubic_ease_interpolator.hpp
@@ -0,0 +1,15 @@
+#ifndef _RIVE_CUBIC_EASE_INTERPOLATOR_HPP_
+#define _RIVE_CUBIC_EASE_INTERPOLATOR_HPP_
+#include "rive/generated/animation/cubic_ease_interpolator_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class CubicEaseInterpolator : public CubicEaseInterpolatorBase
+{
+public:
+    float transformValue(float valueFrom, float valueTo, float factor) override;
+    float transform(float factor) const override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/cubic_interpolator.hpp b/include/rive/animation/cubic_interpolator.hpp
new file mode 100644
index 0000000..4b27621
--- /dev/null
+++ b/include/rive/animation/cubic_interpolator.hpp
@@ -0,0 +1,18 @@
+#ifndef _RIVE_CUBIC_INTERPOLATOR_HPP_
+#define _RIVE_CUBIC_INTERPOLATOR_HPP_
+#include "rive/generated/animation/cubic_interpolator_base.hpp"
+#include "rive/animation/cubic_interpolator_solver.hpp"
+
+namespace rive
+{
+class CubicInterpolator : public CubicInterpolatorBase
+{
+public:
+    StatusCode onAddedDirty(CoreContext* context) override;
+
+protected:
+    CubicInterpolatorSolver m_solver;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/cubic_interpolator_component.hpp b/include/rive/animation/cubic_interpolator_component.hpp
new file mode 100644
index 0000000..89aefda
--- /dev/null
+++ b/include/rive/animation/cubic_interpolator_component.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_CUBIC_INTERPOLATOR_COMPONENT_HPP_
+#define _RIVE_CUBIC_INTERPOLATOR_COMPONENT_HPP_
+#include "rive/generated/animation/cubic_interpolator_component_base.hpp"
+#include "rive/animation/cubic_interpolator_solver.hpp"
+
+namespace rive
+{
+class CubicInterpolatorComponent : public CubicInterpolatorComponentBase
+{
+public:
+    StatusCode onAddedDirty(CoreContext* context) override;
+    float transform(float factor) const;
+
+private:
+    CubicInterpolatorSolver m_solver;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/cubic_interpolator_solver.hpp b/include/rive/animation/cubic_interpolator_solver.hpp
new file mode 100644
index 0000000..5aeebe8
--- /dev/null
+++ b/include/rive/animation/cubic_interpolator_solver.hpp
@@ -0,0 +1,23 @@
+#ifndef _RIVE_CUBIC_INTERPOLATOR_SOLVER_HPP_
+#define _RIVE_CUBIC_INTERPOLATOR_SOLVER_HPP_
+
+namespace rive
+{
+// A helper for finding T based on X value.
+class CubicInterpolatorSolver
+{
+public:
+    void build(float x1, float x2);
+    float getT(float x) const;
+    static float calcBezier(float aT, float aA1, float aA2);
+
+private:
+    static constexpr int SplineTableSize = 11;
+    static constexpr float SampleStepSize = 1.0f / (SplineTableSize - 1.0f);
+    float m_values[SplineTableSize];
+    float m_x1;
+    float m_x2;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/cubic_value_interpolator.hpp b/include/rive/animation/cubic_value_interpolator.hpp
new file mode 100644
index 0000000..bbd724b
--- /dev/null
+++ b/include/rive/animation/cubic_value_interpolator.hpp
@@ -0,0 +1,26 @@
+#ifndef _RIVE_CUBIC_VALUE_INTERPOLATOR_HPP_
+#define _RIVE_CUBIC_VALUE_INTERPOLATOR_HPP_
+#include "rive/generated/animation/cubic_value_interpolator_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class CubicValueInterpolator : public CubicValueInterpolatorBase
+{
+private:
+    float m_A;
+    float m_B;
+    float m_C;
+    float m_D;
+    float m_ValueTo;
+
+    void computeParameters();
+
+public:
+    CubicValueInterpolator();
+    float transformValue(float valueFrom, float valueTo, float factor) override;
+    float transform(float factor) const override;
+    StatusCode onAddedDirty(CoreContext* context) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/easing.hpp b/include/rive/animation/easing.hpp
new file mode 100644
index 0000000..46fd973
--- /dev/null
+++ b/include/rive/animation/easing.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_EASING_HPP_
+#define _RIVE_EASING_HPP_
+#include <cstdint>
+
+namespace rive
+{
+enum class Easing : uint8_t
+{
+    easeIn = 0,
+    easeOut = 1,
+    easeInOut = 2
+};
+}
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/elastic_ease.hpp b/include/rive/animation/elastic_ease.hpp
new file mode 100644
index 0000000..27fecae
--- /dev/null
+++ b/include/rive/animation/elastic_ease.hpp
@@ -0,0 +1,24 @@
+#ifndef _RIVE_ELASTIC_EASE_HPP_
+#define _RIVE_ELASTIC_EASE_HPP_
+
+namespace rive
+{
+class ElasticEase
+{
+public:
+    ElasticEase(float amplitude, float period);
+    float easeOut(float factor) const;
+    float easeIn(float factor) const;
+    float easeInOut(float factor) const;
+
+private:
+    float computeActualAmplitude(float time) const;
+    float m_amplitude;
+    float m_period;
+
+    // Computed phase shift for starting the sin function at 0 at factor 0.
+    float m_s;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/elastic_interpolator.hpp b/include/rive/animation/elastic_interpolator.hpp
new file mode 100644
index 0000000..0661f38
--- /dev/null
+++ b/include/rive/animation/elastic_interpolator.hpp
@@ -0,0 +1,24 @@
+#ifndef _RIVE_ELASTIC_INTERPOLATOR_HPP_
+#define _RIVE_ELASTIC_INTERPOLATOR_HPP_
+#include "rive/generated/animation/elastic_interpolator_base.hpp"
+#include "rive/animation/elastic_ease.hpp"
+#include "rive/animation/easing.hpp"
+
+namespace rive
+{
+class ElasticInterpolator : public ElasticInterpolatorBase
+{
+public:
+    ElasticInterpolator();
+    StatusCode onAddedDirty(CoreContext* context) override;
+    float transformValue(float valueFrom, float valueTo, float factor) override;
+    float transform(float factor) const override;
+
+    Easing easing() const { return (Easing)easingValue(); }
+
+private:
+    ElasticEase m_elastic;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/entry_state.hpp b/include/rive/animation/entry_state.hpp
new file mode 100644
index 0000000..e9bbfa2
--- /dev/null
+++ b/include/rive/animation/entry_state.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_ENTRY_STATE_HPP_
+#define _RIVE_ENTRY_STATE_HPP_
+#include "rive/generated/animation/entry_state_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class EntryState : public EntryStateBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/exit_state.hpp b/include/rive/animation/exit_state.hpp
new file mode 100644
index 0000000..607526d
--- /dev/null
+++ b/include/rive/animation/exit_state.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_EXIT_STATE_HPP_
+#define _RIVE_EXIT_STATE_HPP_
+#include "rive/generated/animation/exit_state_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ExitState : public ExitStateBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/interpolating_keyframe.hpp b/include/rive/animation/interpolating_keyframe.hpp
new file mode 100644
index 0000000..a2d31f6
--- /dev/null
+++ b/include/rive/animation/interpolating_keyframe.hpp
@@ -0,0 +1,25 @@
+#ifndef _RIVE_INTERPOLATING_KEY_FRAME_HPP_
+#define _RIVE_INTERPOLATING_KEY_FRAME_HPP_
+#include "rive/generated/animation/interpolating_keyframe_base.hpp"
+
+namespace rive
+{
+class KeyFrameInterpolator;
+class InterpolatingKeyFrame : public InterpolatingKeyFrameBase
+{
+public:
+    inline KeyFrameInterpolator* interpolator() const { return m_interpolator; }
+    virtual void apply(Core* object, int propertyKey, float mix) = 0;
+    virtual void applyInterpolation(Core* object,
+                                    int propertyKey,
+                                    float seconds,
+                                    const KeyFrame* nextFrame,
+                                    float mix) = 0;
+    StatusCode onAddedDirty(CoreContext* context) override;
+
+private:
+    KeyFrameInterpolator* m_interpolator = nullptr;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/keyed_callback_reporter.hpp b/include/rive/animation/keyed_callback_reporter.hpp
new file mode 100644
index 0000000..7f691a3
--- /dev/null
+++ b/include/rive/animation/keyed_callback_reporter.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_KEYED_CALLBACK_REPORTER_HPP_
+#define _RIVE_KEYED_CALLBACK_REPORTER_HPP_
+
+namespace rive
+{
+class KeyedCallbackReporter
+{
+public:
+    virtual ~KeyedCallbackReporter() {}
+    virtual void reportKeyedCallback(uint32_t objectId,
+                                     uint32_t propertyKey,
+                                     float elapsedSeconds) = 0;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/keyed_object.hpp b/include/rive/animation/keyed_object.hpp
new file mode 100644
index 0000000..3aba7b9
--- /dev/null
+++ b/include/rive/animation/keyed_object.hpp
@@ -0,0 +1,46 @@
+#ifndef _RIVE_KEYED_OBJECT_HPP_
+#define _RIVE_KEYED_OBJECT_HPP_
+#include "rive/generated/animation/keyed_object_base.hpp"
+#include <vector>
+namespace rive
+{
+class Artboard;
+class KeyedProperty;
+class KeyedCallbackReporter;
+class KeyedObject : public KeyedObjectBase
+{
+public:
+    KeyedObject();
+    ~KeyedObject() override;
+    void addKeyedProperty(std::unique_ptr<KeyedProperty>);
+
+    StatusCode onAddedDirty(CoreContext* context) override;
+    StatusCode onAddedClean(CoreContext* context) override;
+    void reportKeyedCallbacks(KeyedCallbackReporter* reporter,
+                              float secondsFrom,
+                              float secondsTo,
+                              bool isAtStartFrame) const;
+    void apply(Artboard* coreContext, float time, float mix);
+
+    StatusCode import(ImportStack& importStack) override;
+
+    const KeyedProperty* getProperty(size_t index) const
+    {
+        if (index < m_keyedProperties.size())
+        {
+            return m_keyedProperties[index].get();
+        }
+        else
+        {
+            return nullptr;
+        }
+    }
+
+    size_t numKeyedProperties() const { return m_keyedProperties.size(); }
+
+private:
+    std::vector<std::unique_ptr<KeyedProperty>> m_keyedProperties;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/keyed_property.hpp b/include/rive/animation/keyed_property.hpp
new file mode 100644
index 0000000..d9d0425
--- /dev/null
+++ b/include/rive/animation/keyed_property.hpp
@@ -0,0 +1,44 @@
+#ifndef _RIVE_KEYED_PROPERTY_HPP_
+#define _RIVE_KEYED_PROPERTY_HPP_
+#include "rive/generated/animation/keyed_property_base.hpp"
+#include <vector>
+namespace rive
+{
+class KeyFrame;
+class KeyedCallbackReporter;
+class KeyedProperty : public KeyedPropertyBase
+{
+public:
+    KeyedProperty();
+    ~KeyedProperty() override;
+    void addKeyFrame(std::unique_ptr<KeyFrame>);
+    StatusCode onAddedClean(CoreContext* context) override;
+    StatusCode onAddedDirty(CoreContext* context) override;
+
+    /// Report any keyframes that occured between secondsFrom and secondsTo.
+    void reportKeyedCallbacks(KeyedCallbackReporter* reporter,
+                              uint32_t objectId,
+                              float secondsFrom,
+                              float secondsTo,
+                              bool isAtStartFrame) const;
+
+    /// Apply interpolating key frames.
+    void apply(Core* object, float time, float mix);
+
+    StatusCode import(ImportStack& importStack) override;
+    KeyFrame* first() const
+    {
+        if (m_keyFrames.size() > 0)
+        {
+            return m_keyFrames.front().get();
+        }
+        return nullptr;
+    }
+
+private:
+    int closestFrameIndex(float seconds, int exactOffset = 0) const;
+    std::vector<std::unique_ptr<KeyFrame>> m_keyFrames;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/keyframe.hpp b/include/rive/animation/keyframe.hpp
new file mode 100644
index 0000000..b1621c8
--- /dev/null
+++ b/include/rive/animation/keyframe.hpp
@@ -0,0 +1,20 @@
+#ifndef _RIVE_KEY_FRAME_HPP_
+#define _RIVE_KEY_FRAME_HPP_
+#include "rive/generated/animation/keyframe_base.hpp"
+namespace rive
+{
+class KeyFrame : public KeyFrameBase
+{
+public:
+    inline float seconds() const { return m_seconds; }
+
+    void computeSeconds(int fps);
+
+    StatusCode import(ImportStack& importStack) override;
+
+private:
+    float m_seconds;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/keyframe_bool.hpp b/include/rive/animation/keyframe_bool.hpp
new file mode 100644
index 0000000..226fd87
--- /dev/null
+++ b/include/rive/animation/keyframe_bool.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_KEY_FRAME_BOOL_HPP_
+#define _RIVE_KEY_FRAME_BOOL_HPP_
+#include "rive/generated/animation/keyframe_bool_base.hpp"
+
+namespace rive
+{
+class KeyFrameBool : public KeyFrameBoolBase
+{
+public:
+    void apply(Core* object, int propertyKey, float mix) override;
+    void applyInterpolation(Core* object,
+                            int propertyKey,
+                            float seconds,
+                            const KeyFrame* nextFrame,
+                            float mix) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/keyframe_callback.hpp b/include/rive/animation/keyframe_callback.hpp
new file mode 100644
index 0000000..9c8c344
--- /dev/null
+++ b/include/rive/animation/keyframe_callback.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_KEY_FRAME_CALLBACK_HPP_
+#define _RIVE_KEY_FRAME_CALLBACK_HPP_
+#include "rive/generated/animation/keyframe_callback_base.hpp"
+
+namespace rive
+{
+class KeyFrameCallback : public KeyFrameCallbackBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/keyframe_color.hpp b/include/rive/animation/keyframe_color.hpp
new file mode 100644
index 0000000..82c8bd9
--- /dev/null
+++ b/include/rive/animation/keyframe_color.hpp
@@ -0,0 +1,18 @@
+#ifndef _RIVE_KEY_FRAME_COLOR_HPP_
+#define _RIVE_KEY_FRAME_COLOR_HPP_
+#include "rive/generated/animation/keyframe_color_base.hpp"
+namespace rive
+{
+class KeyFrameColor : public KeyFrameColorBase
+{
+public:
+    void apply(Core* object, int propertyKey, float mix) override;
+    void applyInterpolation(Core* object,
+                            int propertyKey,
+                            float seconds,
+                            const KeyFrame* nextFrame,
+                            float mix) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/keyframe_double.hpp b/include/rive/animation/keyframe_double.hpp
new file mode 100644
index 0000000..2bdff78
--- /dev/null
+++ b/include/rive/animation/keyframe_double.hpp
@@ -0,0 +1,18 @@
+#ifndef _RIVE_KEY_FRAME_DOUBLE_HPP_
+#define _RIVE_KEY_FRAME_DOUBLE_HPP_
+#include "rive/generated/animation/keyframe_double_base.hpp"
+namespace rive
+{
+class KeyFrameDouble : public KeyFrameDoubleBase
+{
+public:
+    void apply(Core* object, int propertyKey, float mix) override;
+    void applyInterpolation(Core* object,
+                            int propertyKey,
+                            float seconds,
+                            const KeyFrame* nextFrame,
+                            float mix) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/keyframe_id.hpp b/include/rive/animation/keyframe_id.hpp
new file mode 100644
index 0000000..5182c6e
--- /dev/null
+++ b/include/rive/animation/keyframe_id.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_KEY_FRAME_ID_HPP_
+#define _RIVE_KEY_FRAME_ID_HPP_
+#include "rive/generated/animation/keyframe_id_base.hpp"
+
+namespace rive
+{
+class KeyFrameId : public KeyFrameIdBase
+{
+public:
+    void apply(Core* object, int propertyKey, float mix) override;
+    void applyInterpolation(Core* object,
+                            int propertyKey,
+                            float seconds,
+                            const KeyFrame* nextFrame,
+                            float mix) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/keyframe_interpolator.hpp b/include/rive/animation/keyframe_interpolator.hpp
new file mode 100644
index 0000000..392d634
--- /dev/null
+++ b/include/rive/animation/keyframe_interpolator.hpp
@@ -0,0 +1,20 @@
+#ifndef _RIVE_KEY_FRAME_INTERPOLATOR_HPP_
+#define _RIVE_KEY_FRAME_INTERPOLATOR_HPP_
+#include "rive/generated/animation/keyframe_interpolator_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class KeyFrameInterpolator : public KeyFrameInterpolatorBase
+{
+public:
+    /// Convert a linear interpolation value to an eased one.
+    virtual float transformValue(float valueFrom, float valueTo, float factor) = 0;
+
+    /// Convert a linear interpolation factor to an eased one.
+    virtual float transform(float factor) const = 0;
+
+    StatusCode import(ImportStack& importStack) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/keyframe_string.hpp b/include/rive/animation/keyframe_string.hpp
new file mode 100644
index 0000000..4749dc2
--- /dev/null
+++ b/include/rive/animation/keyframe_string.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_KEY_FRAME_STRING_HPP_
+#define _RIVE_KEY_FRAME_STRING_HPP_
+#include "rive/generated/animation/keyframe_string_base.hpp"
+
+namespace rive
+{
+class KeyFrameString : public KeyFrameStringBase
+{
+public:
+    void apply(Core* object, int propertyKey, float mix) override;
+    void applyInterpolation(Core* object,
+                            int propertyKey,
+                            float seconds,
+                            const KeyFrame* nextFrame,
+                            float mix) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/keyframe_uint.hpp b/include/rive/animation/keyframe_uint.hpp
new file mode 100644
index 0000000..2b398de
--- /dev/null
+++ b/include/rive/animation/keyframe_uint.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_KEY_FRAME_UINT_HPP_
+#define _RIVE_KEY_FRAME_UINT_HPP_
+#include "rive/generated/animation/keyframe_uint_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class KeyFrameUint : public KeyFrameUintBase
+{
+public:
+    void apply(Core* object, int propertyKey, float mix) override;
+    void applyInterpolation(Core* object,
+                            int propertyKey,
+                            float seconds,
+                            const KeyFrame* nextFrame,
+                            float mix) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/layer_state.hpp b/include/rive/animation/layer_state.hpp
new file mode 100644
index 0000000..7229640
--- /dev/null
+++ b/include/rive/animation/layer_state.hpp
@@ -0,0 +1,47 @@
+#ifndef _RIVE_LAYER_STATE_HPP_
+#define _RIVE_LAYER_STATE_HPP_
+#include "rive/generated/animation/layer_state_base.hpp"
+#include <stdio.h>
+#include <vector>
+
+namespace rive
+{
+class ArtboardInstance;
+class StateTransition;
+class LayerStateImporter;
+class StateMachineLayerImporter;
+class StateInstance;
+
+class LayerState : public LayerStateBase
+{
+    friend class LayerStateImporter;
+    friend class StateMachineLayerImporter;
+
+private:
+    std::vector<StateTransition*> m_Transitions;
+    void addTransition(StateTransition* transition);
+
+public:
+    ~LayerState() override;
+    StatusCode onAddedDirty(CoreContext* context) override;
+    StatusCode onAddedClean(CoreContext* context) override;
+
+    StatusCode import(ImportStack& importStack) override;
+
+    size_t transitionCount() const { return m_Transitions.size(); }
+    StateTransition* transition(size_t index) const
+    {
+        if (index < m_Transitions.size())
+        {
+            return m_Transitions[index];
+        }
+        return nullptr;
+    }
+
+    /// Make an instance of this state that can be advanced and applied by
+    /// the state machine when it is active or being transitioned from.
+    virtual std::unique_ptr<StateInstance> makeInstance(ArtboardInstance* instance) const;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/layer_state_flags.hpp b/include/rive/animation/layer_state_flags.hpp
new file mode 100644
index 0000000..c9d6134
--- /dev/null
+++ b/include/rive/animation/layer_state_flags.hpp
@@ -0,0 +1,26 @@
+#ifndef _RIVE_LAYER_STATE_FLAGS_HPP_
+#define _RIVE_LAYER_STATE_FLAGS_HPP_
+
+#include <type_traits>
+
+namespace rive
+{
+enum class LayerStateFlags : unsigned char
+{
+    None = 0,
+
+    /// Whether the transition is disabled.
+    Random = 1 << 0,
+
+    /// Whether the blend should include an instance to reset values on apply
+    Reset = 1 << 1,
+};
+
+inline constexpr LayerStateFlags operator&(LayerStateFlags lhs, LayerStateFlags rhs)
+{
+    return static_cast<LayerStateFlags>(
+        static_cast<std::underlying_type<LayerStateFlags>::type>(lhs) &
+        static_cast<std::underlying_type<LayerStateFlags>::type>(rhs));
+}
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/linear_animation.hpp b/include/rive/animation/linear_animation.hpp
new file mode 100644
index 0000000..01f9205
--- /dev/null
+++ b/include/rive/animation/linear_animation.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_LINEAR_ANIMATION_HPP_
+#define _RIVE_LINEAR_ANIMATION_HPP_
+#include "rive/animation/loop.hpp"
+#include "rive/generated/animation/linear_animation_base.hpp"
+#include <vector>
+namespace rive
+{
+class Artboard;
+class KeyedObject;
+class KeyedCallbackReporter;
+
+class LinearAnimation : public LinearAnimationBase
+{
+private:
+    std::vector<std::unique_ptr<KeyedObject>> m_KeyedObjects;
+
+    friend class Artboard;
+
+public:
+    LinearAnimation();
+    ~LinearAnimation() override;
+    StatusCode onAddedDirty(CoreContext* context) override;
+    StatusCode onAddedClean(CoreContext* context) override;
+    void addKeyedObject(std::unique_ptr<KeyedObject>);
+    void apply(Artboard* artboard, float time, float mix = 1.0f) const;
+
+    Loop loop() const { return (Loop)loopValue(); }
+
+    StatusCode import(ImportStack& importStack) override;
+
+    float durationSeconds() const;
+    /// Returns the start time/ end time of the animation in seconds
+    float startSeconds() const;
+    float endSeconds() const;
+
+    /// Returns the start time/ end time of the animation in seconds, considering speed
+    float startTime() const;
+    float startTime(float multiplier) const;
+    float endTime() const;
+
+    /// Convert a global clock to local seconds (takes into consideration
+    /// work area start/end, speed, looping).
+    float globalToLocalSeconds(float seconds) const;
+
+    const KeyedObject* getObject(size_t index) const
+    {
+        if (index < m_KeyedObjects.size())
+        {
+            return m_KeyedObjects[index].get();
+        }
+        else
+        {
+            return nullptr;
+        }
+    }
+
+    size_t numKeyedObjects() const { return m_KeyedObjects.size(); }
+
+#ifdef TESTING
+    // Used in testing to check how many animations gets deleted.
+    static int deleteCount;
+#endif
+
+    void reportKeyedCallbacks(KeyedCallbackReporter* reporter,
+                              float secondsFrom,
+                              float secondsTo,
+                              float speedDirection,
+                              bool fromPong) const;
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/animation/linear_animation_instance.hpp b/include/rive/animation/linear_animation_instance.hpp
new file mode 100644
index 0000000..d04d596
--- /dev/null
+++ b/include/rive/animation/linear_animation_instance.hpp
@@ -0,0 +1,118 @@
+#ifndef _RIVE_LINEAR_ANIMATION_INSTANCE_HPP_
+#define _RIVE_LINEAR_ANIMATION_INSTANCE_HPP_
+
+#include "rive/artboard.hpp"
+#include "rive/core/field_types/core_callback_type.hpp"
+#include "rive/nested_animation.hpp"
+#include "rive/scene.hpp"
+
+namespace rive
+{
+class LinearAnimation;
+class NestedEventNotifier;
+
+class LinearAnimationInstance : public Scene, public NestedEventNotifier
+{
+public:
+    LinearAnimationInstance(const LinearAnimation*, ArtboardInstance*, float speedMultiplier = 1.0);
+    LinearAnimationInstance(LinearAnimationInstance const&);
+    ~LinearAnimationInstance() override;
+
+    // Advance the animation by the specified time. Returns true if the
+    // animation will continue to animate after this advance.
+    bool advance(float seconds, KeyedCallbackReporter* reporter);
+    bool advance(float seconds) { return advance(seconds, nullptr); }
+
+    void clearSpilledTime() { m_spilledTime = 0; }
+
+    // Returns a pointer to the instance's animation
+    const LinearAnimation* animation() const { return m_animation; }
+
+    // Returns the current point in time at which this instance has advance
+    // to
+    float time() const { return m_time; }
+
+    // Returns the direction that we are currently playing in
+    float direction() const { return m_direction; }
+
+    // Returns speed in the current direction of the animation.
+    float directedSpeed() const { return m_direction * speed(); }
+
+    // Update the direction of the animation instance, positive value for
+    // forwards Negative for backwards
+    void direction(int direction)
+    {
+        if (direction > 0)
+        {
+            m_direction = 1;
+        }
+        else
+        {
+            m_direction = -1;
+        }
+    }
+
+    // Sets the animation's point in time.
+    void time(float value);
+
+    // Applies the animation instance to its artboard instance. The mix (a value
+    // between 0 and 1) is the strength at which the animation is mixed with
+    // other animations applied to the artboard.
+    void apply(float mix = 1.0f) const { m_animation->apply(m_artboardInstance, m_time, mix); }
+
+    // Set when the animation is advanced, true if the animation has stopped
+    // (oneShot), reached the end (loop), or changed direction (pingPong)
+    bool didLoop() const { return m_didLoop; }
+
+    bool keepGoing() const
+    {
+        return this->loopValue() != static_cast<int>(rive::Loop::oneShot) ||
+               (directedSpeed() > 0 && m_time < m_animation->endSeconds()) ||
+               (directedSpeed() < 0 && m_time > m_animation->startSeconds());
+    }
+
+    bool keepGoing(float speedMultiplier) const
+    {
+        return this->loopValue() != static_cast<int>(rive::Loop::oneShot) ||
+               (directedSpeed() * speedMultiplier > 0 && m_time < m_animation->endSeconds()) ||
+               (directedSpeed() * speedMultiplier < 0 && m_time > m_animation->startSeconds());
+    }
+
+    float totalTime() const { return m_totalTime; }
+    float lastTotalTime() const { return m_lastTotalTime; }
+    float spilledTime() const { return m_spilledTime; }
+    float durationSeconds() const override;
+
+    // Forwarded from animation
+    uint32_t fps() const;
+    uint32_t duration() const;
+    float speed() const;
+    float startTime() const;
+
+    // Returns either the animation's default or overridden loop values
+    Loop loop() const override { return (Loop)loopValue(); }
+    int loopValue() const;
+    // Override the animation's default loop
+    void loopValue(int value);
+
+    bool isTranslucent() const override;
+    bool advanceAndApply(float seconds) override;
+    std::string name() const override;
+    void reset(float speedMultiplier);
+    void reportEvent(Event* event, float secondsDelay = 0.0f) override;
+
+private:
+    const LinearAnimation* m_animation = nullptr;
+    float m_time;
+    float m_speedDirection;
+    float m_totalTime;
+    float m_lastTotalTime;
+    float m_spilledTime;
+
+    // float because it gets multiplied with other floats
+    float m_direction;
+    bool m_didLoop;
+    int m_loopValue = -1;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/animation/listener_action.hpp b/include/rive/animation/listener_action.hpp
new file mode 100644
index 0000000..691ab0f
--- /dev/null
+++ b/include/rive/animation/listener_action.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_LISTENER_ACTION_HPP_
+#define _RIVE_LISTENER_ACTION_HPP_
+#include "rive/generated/animation/listener_action_base.hpp"
+#include "rive/math/vec2d.hpp"
+
+namespace rive
+{
+class StateMachineInstance;
+class ListenerAction : public ListenerActionBase
+{
+public:
+    StatusCode import(ImportStack& importStack) override;
+    virtual void perform(StateMachineInstance* stateMachineInstance,
+                         Vec2D position,
+                         Vec2D previousPosition) const = 0;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/listener_align_target.hpp b/include/rive/animation/listener_align_target.hpp
new file mode 100644
index 0000000..2a150d6
--- /dev/null
+++ b/include/rive/animation/listener_align_target.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_LISTENER_ALIGN_TARGET_HPP_
+#define _RIVE_LISTENER_ALIGN_TARGET_HPP_
+#include "rive/generated/animation/listener_align_target_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ListenerAlignTarget : public ListenerAlignTargetBase
+{
+public:
+    void perform(StateMachineInstance* stateMachineInstance,
+                 Vec2D position,
+                 Vec2D previousPosition) const override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/listener_bool_change.hpp b/include/rive/animation/listener_bool_change.hpp
new file mode 100644
index 0000000..a6f25c2
--- /dev/null
+++ b/include/rive/animation/listener_bool_change.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_LISTENER_BOOL_CHANGE_HPP_
+#define _RIVE_LISTENER_BOOL_CHANGE_HPP_
+#include "rive/generated/animation/listener_bool_change_base.hpp"
+
+namespace rive
+{
+class NestedInput;
+class ListenerBoolChange : public ListenerBoolChangeBase
+{
+public:
+    bool validateInputType(const StateMachineInput* input) const override;
+    bool validateNestedInputType(const NestedInput* input) const override;
+    void perform(StateMachineInstance* stateMachineInstance,
+                 Vec2D position,
+                 Vec2D previousPosition) const override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/listener_fire_event.hpp b/include/rive/animation/listener_fire_event.hpp
new file mode 100644
index 0000000..4e4a7d2
--- /dev/null
+++ b/include/rive/animation/listener_fire_event.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_LISTENER_FIRE_EVENT_HPP_
+#define _RIVE_LISTENER_FIRE_EVENT_HPP_
+#include "rive/generated/animation/listener_fire_event_base.hpp"
+
+namespace rive
+{
+class ListenerFireEvent : public ListenerFireEventBase
+{
+public:
+    void perform(StateMachineInstance* stateMachineInstance,
+                 Vec2D position,
+                 Vec2D previousPosition) const override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/listener_input_change.hpp b/include/rive/animation/listener_input_change.hpp
new file mode 100644
index 0000000..584b43a
--- /dev/null
+++ b/include/rive/animation/listener_input_change.hpp
@@ -0,0 +1,18 @@
+#ifndef _RIVE_LISTENER_INPUT_CHANGE_HPP_
+#define _RIVE_LISTENER_INPUT_CHANGE_HPP_
+#include "rive/generated/animation/listener_input_change_base.hpp"
+
+namespace rive
+{
+class NestedInput;
+class StateMachineInput;
+class ListenerInputChange : public ListenerInputChangeBase
+{
+public:
+    StatusCode import(ImportStack& importStack) override;
+    virtual bool validateInputType(const StateMachineInput* input) const { return true; }
+    virtual bool validateNestedInputType(const NestedInput* input) const { return true; }
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/listener_number_change.hpp b/include/rive/animation/listener_number_change.hpp
new file mode 100644
index 0000000..613d7d3
--- /dev/null
+++ b/include/rive/animation/listener_number_change.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_LISTENER_NUMBER_CHANGE_HPP_
+#define _RIVE_LISTENER_NUMBER_CHANGE_HPP_
+#include "rive/generated/animation/listener_number_change_base.hpp"
+
+namespace rive
+{
+class NestedInput;
+class ListenerNumberChange : public ListenerNumberChangeBase
+{
+public:
+    bool validateInputType(const StateMachineInput* input) const override;
+    bool validateNestedInputType(const NestedInput* input) const override;
+    void perform(StateMachineInstance* stateMachineInstance,
+                 Vec2D position,
+                 Vec2D previousPosition) const override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/listener_trigger_change.hpp b/include/rive/animation/listener_trigger_change.hpp
new file mode 100644
index 0000000..005f472
--- /dev/null
+++ b/include/rive/animation/listener_trigger_change.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_LISTENER_TRIGGER_CHANGE_HPP_
+#define _RIVE_LISTENER_TRIGGER_CHANGE_HPP_
+#include "rive/generated/animation/listener_trigger_change_base.hpp"
+
+namespace rive
+{
+class NestedInput;
+class ListenerTriggerChange : public ListenerTriggerChangeBase
+{
+public:
+    bool validateInputType(const StateMachineInput* input) const override;
+    bool validateNestedInputType(const NestedInput* input) const override;
+    void perform(StateMachineInstance* stateMachineInstance,
+                 Vec2D position,
+                 Vec2D previousPosition) const override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/listener_viewmodel_change.hpp b/include/rive/animation/listener_viewmodel_change.hpp
new file mode 100644
index 0000000..a9b82a1
--- /dev/null
+++ b/include/rive/animation/listener_viewmodel_change.hpp
@@ -0,0 +1,21 @@
+#ifndef _RIVE_LISTENER_VIEW_MODEL_CHANGE_HPP_
+#define _RIVE_LISTENER_VIEW_MODEL_CHANGE_HPP_
+#include "rive/generated/animation/listener_viewmodel_change_base.hpp"
+#include "rive/data_bind/bindable_property.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ListenerViewModelChange : public ListenerViewModelChangeBase
+{
+public:
+    void perform(StateMachineInstance* stateMachineInstance,
+                 Vec2D position,
+                 Vec2D previousPosition) const override;
+    StatusCode import(ImportStack& importStack) override;
+
+private:
+    BindableProperty* m_bindableProperty;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/loop.hpp b/include/rive/animation/loop.hpp
new file mode 100644
index 0000000..d4f0d53
--- /dev/null
+++ b/include/rive/animation/loop.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_LOOP_HPP_
+#define _RIVE_LOOP_HPP_
+namespace rive
+{
+/// Loop options for linear animations.
+enum class Loop : unsigned int
+{
+    /// Play until the duration or end of work area of the animation.
+    oneShot = 0,
+
+    /// Play until the duration or end of work area of the animation and
+    /// then go back to the start (0 seconds).
+    loop = 1,
+
+    /// Play to the end of the duration/work area and then play back.
+    pingPong = 2
+};
+} // namespace rive
+#endif
diff --git a/include/rive/animation/nested_bool.hpp b/include/rive/animation/nested_bool.hpp
new file mode 100644
index 0000000..6421069
--- /dev/null
+++ b/include/rive/animation/nested_bool.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_NESTED_BOOL_HPP_
+#define _RIVE_NESTED_BOOL_HPP_
+#include "rive/generated/animation/nested_bool_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class NestedBool : public NestedBoolBase
+{
+public:
+    void nestedValue(bool value) override;
+    bool nestedValue() const override;
+
+    void applyValue() override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/nested_input.hpp b/include/rive/animation/nested_input.hpp
new file mode 100644
index 0000000..fe75467
--- /dev/null
+++ b/include/rive/animation/nested_input.hpp
@@ -0,0 +1,50 @@
+#ifndef _RIVE_NESTED_INPUT_HPP_
+#define _RIVE_NESTED_INPUT_HPP_
+#include "rive/animation/nested_state_machine.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+#include "rive/generated/animation/nested_input_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class NestedInput : public NestedInputBase
+{
+public:
+    StatusCode onAddedDirty(CoreContext* context) override
+    {
+        StatusCode result = Super::onAddedDirty(context);
+        auto parent = this->parent();
+        if (parent != nullptr && parent->is<NestedStateMachine>())
+        {
+            parent->as<NestedStateMachine>()->addNestedInput(this);
+        }
+        return result;
+    }
+
+    virtual void applyValue() {}
+
+    SMIInput* input() const
+    {
+        auto parent = this->parent();
+        if (parent != nullptr && parent->is<NestedStateMachine>())
+        {
+            StateMachineInstance* smInstance =
+                parent->as<NestedStateMachine>()->stateMachineInstance();
+            auto inputInstance = smInstance->input(this->inputId());
+            return inputInstance;
+        }
+        return nullptr;
+    }
+
+    const std::string name() const
+    {
+        auto smi = input();
+        if (smi != nullptr)
+        {
+            return smi->name();
+        }
+        return std::string();
+    }
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/nested_linear_animation.hpp b/include/rive/animation/nested_linear_animation.hpp
new file mode 100644
index 0000000..6d30727
--- /dev/null
+++ b/include/rive/animation/nested_linear_animation.hpp
@@ -0,0 +1,22 @@
+#ifndef _RIVE_NESTED_LINEAR_ANIMATION_HPP_
+#define _RIVE_NESTED_LINEAR_ANIMATION_HPP_
+#include "rive/generated/animation/nested_linear_animation_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class LinearAnimationInstance;
+class NestedLinearAnimation : public NestedLinearAnimationBase
+{
+protected:
+    std::unique_ptr<LinearAnimationInstance> m_AnimationInstance;
+
+public:
+    NestedLinearAnimation();
+    ~NestedLinearAnimation() override;
+
+    void initializeAnimation(ArtboardInstance*) override;
+    LinearAnimationInstance* animationInstance() { return m_AnimationInstance.get(); }
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/nested_number.hpp b/include/rive/animation/nested_number.hpp
new file mode 100644
index 0000000..f965126
--- /dev/null
+++ b/include/rive/animation/nested_number.hpp
@@ -0,0 +1,18 @@
+#ifndef _RIVE_NESTED_NUMBER_HPP_
+#define _RIVE_NESTED_NUMBER_HPP_
+#include "rive/generated/animation/nested_number_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class NestedNumber : public NestedNumberBase
+{
+public:
+    void nestedValue(float value) override;
+    float nestedValue() const override;
+    void applyValue() override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/nested_remap_animation.hpp b/include/rive/animation/nested_remap_animation.hpp
new file mode 100644
index 0000000..3b304fd
--- /dev/null
+++ b/include/rive/animation/nested_remap_animation.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_NESTED_REMAP_ANIMATION_HPP_
+#define _RIVE_NESTED_REMAP_ANIMATION_HPP_
+#include "rive/generated/animation/nested_remap_animation_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class NestedRemapAnimation : public NestedRemapAnimationBase
+{
+public:
+    void timeChanged() override;
+    bool advance(float elapsedSeconds) override;
+    void initializeAnimation(ArtboardInstance*) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/nested_simple_animation.hpp b/include/rive/animation/nested_simple_animation.hpp
new file mode 100644
index 0000000..ce04aa4
--- /dev/null
+++ b/include/rive/animation/nested_simple_animation.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_NESTED_SIMPLE_ANIMATION_HPP_
+#define _RIVE_NESTED_SIMPLE_ANIMATION_HPP_
+#include "rive/generated/animation/nested_simple_animation_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class NestedSimpleAnimation : public NestedSimpleAnimationBase
+{
+public:
+    bool advance(float elapsedSeconds) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/nested_state_machine.hpp b/include/rive/animation/nested_state_machine.hpp
new file mode 100644
index 0000000..f3e557c
--- /dev/null
+++ b/include/rive/animation/nested_state_machine.hpp
@@ -0,0 +1,44 @@
+#ifndef _RIVE_NESTED_STATE_MACHINE_HPP_
+#define _RIVE_NESTED_STATE_MACHINE_HPP_
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/generated/animation/nested_state_machine_base.hpp"
+#include "rive/hit_result.hpp"
+#include "rive/math/vec2d.hpp"
+#include <memory>
+
+namespace rive
+{
+class ArtboardInstance;
+class NestedInput;
+class StateMachineInstance;
+class NestedStateMachine : public NestedStateMachineBase
+{
+private:
+    std::unique_ptr<StateMachineInstance> m_StateMachineInstance;
+    std::vector<NestedInput*> m_nestedInputs;
+
+public:
+    NestedStateMachine();
+    ~NestedStateMachine() override;
+    bool advance(float elapsedSeconds) override;
+    void initializeAnimation(ArtboardInstance*) override;
+    StateMachineInstance* stateMachineInstance();
+
+    HitResult pointerMove(Vec2D position);
+    HitResult pointerDown(Vec2D position);
+    HitResult pointerUp(Vec2D position);
+    HitResult pointerExit(Vec2D position);
+#ifdef WITH_RIVE_TOOLS
+    bool hitTest(Vec2D position) const;
+#endif
+
+    void addNestedInput(NestedInput* input);
+    size_t inputCount() { return m_nestedInputs.size(); }
+    NestedInput* input(size_t index);
+    NestedInput* input(std::string name);
+    void dataContextFromInstance(ViewModelInstance* viewModelInstance);
+    void dataContext(DataContext* dataContext);
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/nested_trigger.hpp b/include/rive/animation/nested_trigger.hpp
new file mode 100644
index 0000000..989eac8
--- /dev/null
+++ b/include/rive/animation/nested_trigger.hpp
@@ -0,0 +1,15 @@
+#ifndef _RIVE_NESTED_TRIGGER_HPP_
+#define _RIVE_NESTED_TRIGGER_HPP_
+#include "rive/generated/animation/nested_trigger_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class NestedTrigger : public NestedTriggerBase
+{
+public:
+    void applyValue() override;
+    void fire(const CallbackData& value) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/state_instance.hpp b/include/rive/animation/state_instance.hpp
new file mode 100644
index 0000000..1dffd9d
--- /dev/null
+++ b/include/rive/animation/state_instance.hpp
@@ -0,0 +1,35 @@
+#ifndef _RIVE_STATE_INSTANCE_HPP_
+#define _RIVE_STATE_INSTANCE_HPP_
+
+#include <string>
+#include <stddef.h>
+#include "rive/rive_types.hpp"
+#include "rive/span.hpp"
+
+namespace rive
+{
+class LayerState;
+class StateMachineInstance;
+class ArtboardInstance;
+
+/// Represents an instance of a state tracked by the State Machine.
+class StateInstance
+{
+private:
+    const LayerState* m_LayerState;
+
+public:
+    StateInstance(const LayerState* layerState);
+    virtual ~StateInstance();
+    virtual void advance(float seconds, StateMachineInstance* stateMachineInstance) = 0;
+    virtual void apply(ArtboardInstance* instance, float mix) = 0;
+
+    /// Returns true when the State Machine needs to keep advancing this
+    /// state.
+    virtual bool keepGoing() const = 0;
+    virtual void clearSpilledTime() {}
+
+    const LayerState* state() const;
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/state_machine.hpp b/include/rive/animation/state_machine.hpp
new file mode 100644
index 0000000..258fe61
--- /dev/null
+++ b/include/rive/animation/state_machine.hpp
@@ -0,0 +1,52 @@
+#ifndef _RIVE_STATE_MACHINE_HPP_
+#define _RIVE_STATE_MACHINE_HPP_
+#include "rive/generated/animation/state_machine_base.hpp"
+#include <stdio.h>
+#include <vector>
+
+namespace rive
+{
+class StateMachineLayer;
+class StateMachineInput;
+class StateMachineListener;
+class StateMachineImporter;
+class DataBind;
+class StateMachine : public StateMachineBase
+{
+    friend class StateMachineImporter;
+
+private:
+    std::vector<std::unique_ptr<StateMachineLayer>> m_Layers;
+    std::vector<std::unique_ptr<StateMachineInput>> m_Inputs;
+    std::vector<std::unique_ptr<StateMachineListener>> m_Listeners;
+    std::vector<std::unique_ptr<DataBind>> m_dataBinds;
+
+    void addLayer(std::unique_ptr<StateMachineLayer>);
+    void addInput(std::unique_ptr<StateMachineInput>);
+    void addListener(std::unique_ptr<StateMachineListener>);
+    void addDataBind(std::unique_ptr<DataBind>);
+
+public:
+    StateMachine();
+    ~StateMachine() override;
+
+    StatusCode import(ImportStack& importStack) override;
+
+    size_t layerCount() const { return m_Layers.size(); }
+    size_t inputCount() const { return m_Inputs.size(); }
+    size_t listenerCount() const { return m_Listeners.size(); }
+    size_t dataBindCount() const { return m_dataBinds.size(); }
+
+    const StateMachineInput* input(std::string name) const;
+    const StateMachineInput* input(size_t index) const;
+    const StateMachineLayer* layer(std::string name) const;
+    const StateMachineLayer* layer(size_t index) const;
+    const DataBind* dataBind(size_t index) const;
+    const StateMachineListener* listener(size_t index) const;
+
+    StatusCode onAddedDirty(CoreContext* context) override;
+    StatusCode onAddedClean(CoreContext* context) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/state_machine_bool.hpp b/include/rive/animation/state_machine_bool.hpp
new file mode 100644
index 0000000..a441e55
--- /dev/null
+++ b/include/rive/animation/state_machine_bool.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_STATE_MACHINE_BOOL_HPP_
+#define _RIVE_STATE_MACHINE_BOOL_HPP_
+#include "rive/generated/animation/state_machine_bool_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class StateMachineBool : public StateMachineBoolBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/state_machine_component.hpp b/include/rive/animation/state_machine_component.hpp
new file mode 100644
index 0000000..8f6e340
--- /dev/null
+++ b/include/rive/animation/state_machine_component.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_STATE_MACHINE_COMPONENT_HPP_
+#define _RIVE_STATE_MACHINE_COMPONENT_HPP_
+#include "rive/generated/animation/state_machine_component_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class StateMachineComponent : public StateMachineComponentBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/state_machine_fire_event.hpp b/include/rive/animation/state_machine_fire_event.hpp
new file mode 100644
index 0000000..47526c9
--- /dev/null
+++ b/include/rive/animation/state_machine_fire_event.hpp
@@ -0,0 +1,23 @@
+#ifndef _RIVE_STATE_MACHINE_FIRE_EVENT_HPP_
+#define _RIVE_STATE_MACHINE_FIRE_EVENT_HPP_
+#include "rive/generated/animation/state_machine_fire_event_base.hpp"
+
+namespace rive
+{
+class StateMachineInstance;
+enum class StateMachineFireOccurance : int
+{
+    atStart = 0,
+    atEnd = 1
+};
+
+class StateMachineFireEvent : public StateMachineFireEventBase
+{
+public:
+    StatusCode import(ImportStack& importStack) override;
+    StateMachineFireOccurance occurs() const { return (StateMachineFireOccurance)occursValue(); }
+    void perform(StateMachineInstance* stateMachineInstance) const;
+};
+
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/state_machine_input.hpp b/include/rive/animation/state_machine_input.hpp
new file mode 100644
index 0000000..d7d29ae
--- /dev/null
+++ b/include/rive/animation/state_machine_input.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_STATE_MACHINE_INPUT_HPP_
+#define _RIVE_STATE_MACHINE_INPUT_HPP_
+#include "rive/generated/animation/state_machine_input_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class StateMachineInput : public StateMachineInputBase
+{
+public:
+    StatusCode onAddedDirty(CoreContext* context) override;
+    StatusCode onAddedClean(CoreContext* context) override;
+    StatusCode import(ImportStack& importStack) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/state_machine_input_instance.hpp b/include/rive/animation/state_machine_input_instance.hpp
new file mode 100644
index 0000000..8c82a71
--- /dev/null
+++ b/include/rive/animation/state_machine_input_instance.hpp
@@ -0,0 +1,92 @@
+#ifndef _RIVE_STATE_MACHINE_INPUT_INSTANCE_HPP_
+#define _RIVE_STATE_MACHINE_INPUT_INSTANCE_HPP_
+
+#include <string>
+#include <stdint.h>
+
+namespace rive
+{
+class StateMachineInstance;
+class StateMachineInput;
+class StateMachineBool;
+class StateMachineNumber;
+class StateMachineTrigger;
+class TransitionTriggerCondition;
+class StateMachineLayerInstance;
+
+class SMIInput
+{
+    friend class StateMachineInstance;
+    friend class StateMachineLayerInstance;
+
+private:
+    virtual void advanced() {}
+
+protected:
+    void valueChanged();
+
+    SMIInput(const StateMachineInput* input, StateMachineInstance* machineInstance);
+
+public:
+    virtual ~SMIInput() {}
+    const StateMachineInput* input() const { return m_input; }
+
+    const std::string& name() const;
+    uint16_t inputCoreType() const;
+
+private:
+    StateMachineInstance* m_machineInstance;
+    const StateMachineInput* m_input;
+#ifdef WITH_RIVE_TOOLS
+    uint64_t m_index = 0;
+#endif
+};
+
+class SMIBool : public SMIInput
+{
+    friend class StateMachineInstance;
+
+private:
+    bool m_Value;
+
+    SMIBool(const StateMachineBool* input, StateMachineInstance* machineInstance);
+
+public:
+    bool value() const { return m_Value; }
+    void value(bool newValue);
+};
+
+class SMINumber : public SMIInput
+{
+    friend class StateMachineInstance;
+
+private:
+    float m_Value;
+
+    SMINumber(const StateMachineNumber* input, StateMachineInstance* machineInstance);
+
+public:
+    float value() const { return m_Value; }
+    void value(float newValue);
+};
+
+class SMITrigger : public SMIInput
+{
+    friend class StateMachineInstance;
+    friend class TransitionTriggerCondition;
+
+private:
+    bool m_fired = false;
+
+    SMITrigger(const StateMachineTrigger* input, StateMachineInstance* machineInstance);
+    void advanced() override { m_fired = false; }
+
+public:
+    void fire();
+
+#ifdef TESTING
+    bool didFire() { return m_fired; }
+#endif
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/state_machine_instance.hpp b/include/rive/animation/state_machine_instance.hpp
new file mode 100644
index 0000000..5921adf
--- /dev/null
+++ b/include/rive/animation/state_machine_instance.hpp
@@ -0,0 +1,200 @@
+#ifndef _RIVE_STATE_MACHINE_INSTANCE_HPP_
+#define _RIVE_STATE_MACHINE_INSTANCE_HPP_
+
+#include <string>
+#include <stddef.h>
+#include <vector>
+#include <unordered_map>
+#include "rive/animation/linear_animation_instance.hpp"
+#include "rive/animation/state_instance.hpp"
+#include "rive/animation/state_transition.hpp"
+#include "rive/core/field_types/core_callback_type.hpp"
+#include "rive/hit_result.hpp"
+#include "rive/listener_type.hpp"
+#include "rive/nested_animation.hpp"
+#include "rive/scene.hpp"
+
+namespace rive
+{
+class StateMachine;
+class LayerState;
+class SMIInput;
+class ArtboardInstance;
+class SMIBool;
+class SMINumber;
+class SMITrigger;
+class Shape;
+class StateMachineLayerInstance;
+class HitComponent;
+class HitShape;
+class ListenerGroup;
+class NestedArtboard;
+class NestedEventListener;
+class NestedEventNotifier;
+class Event;
+class KeyedProperty;
+class EventReport;
+class DataBind;
+class BindableProperty;
+
+#ifdef WITH_RIVE_TOOLS
+class StateMachineInstance;
+typedef void (*InputChanged)(StateMachineInstance*, uint64_t);
+#endif
+
+class StateMachineInstance : public Scene, public NestedEventNotifier, public NestedEventListener
+{
+    friend class SMIInput;
+    friend class KeyedProperty;
+    friend class HitComponent;
+    friend class StateMachineLayerInstance;
+
+private:
+    /// Provide a hitListener if you want to process a down or an up for the pointer position
+    /// too.
+    HitResult updateListeners(Vec2D position, ListenerType hitListener);
+
+    template <typename SMType, typename InstType>
+    InstType* getNamedInput(const std::string& name) const;
+    void notifyEventListeners(const std::vector<EventReport>& events, NestedArtboard* source);
+    void sortHitComponents();
+    double randomValue();
+    StateTransition* findRandomTransition(StateInstance* stateFromInstance, bool ignoreTriggers);
+    StateTransition* findAllowedTransition(StateInstance* stateFromInstance, bool ignoreTriggers);
+    DataContext* m_DataContext;
+
+public:
+    StateMachineInstance(const StateMachine* machine, ArtboardInstance* instance);
+    StateMachineInstance(StateMachineInstance const&) = delete;
+    ~StateMachineInstance() override;
+
+    void markNeedsAdvance();
+    // Advance the state machine by the specified time. Returns true if the
+    // state machine will continue to animate after this advance.
+    bool advance(float seconds);
+
+    // Returns true when the StateMachineInstance has more data to process.
+    bool needsAdvance() const;
+
+    // Returns a pointer to the instance's stateMachine
+    const StateMachine* stateMachine() const { return m_machine; }
+
+    size_t inputCount() const override { return m_inputInstances.size(); }
+    SMIInput* input(size_t index) const override;
+    SMIBool* getBool(const std::string& name) const override;
+    SMINumber* getNumber(const std::string& name) const override;
+    SMITrigger* getTrigger(const std::string& name) const override;
+    void dataContextFromInstance(ViewModelInstance* viewModelInstance) override;
+    void dataContext(DataContext* dataContext);
+
+    size_t currentAnimationCount() const;
+    const LinearAnimationInstance* currentAnimationByIndex(size_t index) const;
+
+    // The number of state changes that occurred across all layers on the
+    // previous advance.
+    std::size_t stateChangedCount() const;
+
+    // Returns the state name for states that changed in layers on the
+    // previously called advance. If the index of out of range, it returns
+    // the empty string.
+    const LayerState* stateChangedByIndex(size_t index) const;
+
+    bool advanceAndApply(float secs) override;
+    std::string name() const override;
+    HitResult pointerMove(Vec2D position) override;
+    HitResult pointerDown(Vec2D position) override;
+    HitResult pointerUp(Vec2D position) override;
+    HitResult pointerExit(Vec2D position) override;
+#ifdef WITH_RIVE_TOOLS
+    bool hitTest(Vec2D position) const;
+#endif
+
+    float durationSeconds() const override { return -1; }
+    Loop loop() const override { return Loop::oneShot; }
+    bool isTranslucent() const override { return true; }
+
+    /// Allow anything referencing a concrete StateMachineInstace access to
+    /// the backing artboard (explicitly not allowed on Scenes).
+    Artboard* artboard() const { return m_artboardInstance; }
+
+    void setParentStateMachineInstance(StateMachineInstance* instance)
+    {
+        m_parentStateMachineInstance = instance;
+    }
+    StateMachineInstance* parentStateMachineInstance() { return m_parentStateMachineInstance; }
+
+    void setParentNestedArtboard(NestedArtboard* artboard) { m_parentNestedArtboard = artboard; }
+    NestedArtboard* parentNestedArtboard() { return m_parentNestedArtboard; }
+    void notify(const std::vector<EventReport>& events, NestedArtboard* context) override;
+
+    /// Tracks an event that reported, will be cleared at the end of the next advance.
+    void reportEvent(Event* event, float secondsDelay = 0.0f) override;
+
+    /// Gets the number of events that reported since the last advance.
+    std::size_t reportedEventCount() const;
+
+    /// Gets a reported event at an index < reportedEventCount().
+    const EventReport reportedEventAt(std::size_t index) const;
+    bool playsAudio() override { return true; }
+    BindableProperty* bindablePropertyInstance(BindableProperty* bindableProperty) const;
+    DataBind* bindableDataBind(BindableProperty* bindableProperty);
+#ifdef TESTING
+    size_t hitComponentsCount() { return m_hitComponents.size(); };
+    HitComponent* hitComponent(size_t index)
+    {
+        if (index < m_hitComponents.size())
+        {
+            return m_hitComponents[index].get();
+        }
+        return nullptr;
+    }
+    const LayerState* layerState(size_t index);
+#endif
+    void updateDataBinds();
+
+private:
+    std::vector<EventReport> m_reportedEvents;
+    const StateMachine* m_machine;
+    bool m_needsAdvance = false;
+    std::vector<SMIInput*> m_inputInstances; // we own each pointer
+    size_t m_layerCount;
+    StateMachineLayerInstance* m_layers;
+    std::vector<std::unique_ptr<HitComponent>> m_hitComponents;
+    std::vector<std::unique_ptr<ListenerGroup>> m_listenerGroups;
+    StateMachineInstance* m_parentStateMachineInstance = nullptr;
+    NestedArtboard* m_parentNestedArtboard = nullptr;
+    std::vector<DataBind*> m_dataBinds;
+    std::unordered_map<BindableProperty*, BindableProperty*> m_bindablePropertyInstances;
+    std::unordered_map<BindableProperty*, DataBind*> m_bindableDataBinds;
+
+#ifdef WITH_RIVE_TOOLS
+public:
+    void onInputChanged(InputChanged callback) { m_inputChangedCallback = callback; }
+    InputChanged m_inputChangedCallback = nullptr;
+#endif
+};
+
+class HitComponent
+{
+public:
+    Component* component() const { return m_component; }
+    HitComponent(Component* component, StateMachineInstance* stateMachineInstance) :
+        m_component(component), m_stateMachineInstance(stateMachineInstance)
+    {}
+    virtual ~HitComponent() {}
+    virtual HitResult processEvent(Vec2D position, ListenerType hitType, bool canHit) = 0;
+    virtual void prepareEvent(Vec2D position, ListenerType hitType) = 0;
+#ifdef WITH_RIVE_TOOLS
+    virtual bool hitTest(Vec2D position) const = 0;
+#endif
+#ifdef TESTING
+    int earlyOutCount = 0;
+#endif
+
+protected:
+    Component* m_component;
+    StateMachineInstance* m_stateMachineInstance;
+};
+
+} // namespace rive
+#endif
diff --git a/include/rive/animation/state_machine_layer.hpp b/include/rive/animation/state_machine_layer.hpp
new file mode 100644
index 0000000..74ba7e3
--- /dev/null
+++ b/include/rive/animation/state_machine_layer.hpp
@@ -0,0 +1,51 @@
+#ifndef _RIVE_STATE_MACHINE_LAYER_HPP_
+#define _RIVE_STATE_MACHINE_LAYER_HPP_
+#include "rive/generated/animation/state_machine_layer_base.hpp"
+#include <stdio.h>
+#include <vector>
+
+namespace rive
+{
+class LayerState;
+class StateMachineLayerImporter;
+class AnyState;
+class EntryState;
+class ExitState;
+class StateMachineLayer : public StateMachineLayerBase
+{
+    friend class StateMachineLayerImporter;
+
+private:
+    std::vector<LayerState*> m_States;
+    AnyState* m_Any = nullptr;
+    EntryState* m_Entry = nullptr;
+    ExitState* m_Exit = nullptr;
+
+    void addState(LayerState* state);
+
+public:
+    ~StateMachineLayer() override;
+    StatusCode onAddedDirty(CoreContext* context) override;
+    StatusCode onAddedClean(CoreContext* context) override;
+
+    StatusCode import(ImportStack& importStack) override;
+
+    const AnyState* anyState() const { return m_Any; }
+    const EntryState* entryState() const { return m_Entry; }
+    const ExitState* exitState() const { return m_Exit; }
+
+#ifdef TESTING
+    size_t stateCount() const { return m_States.size(); }
+    LayerState* state(size_t index) const
+    {
+        if (index < m_States.size())
+        {
+            return m_States[index];
+        }
+        return nullptr;
+    }
+#endif
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/state_machine_layer_component.hpp b/include/rive/animation/state_machine_layer_component.hpp
new file mode 100644
index 0000000..e8c6799
--- /dev/null
+++ b/include/rive/animation/state_machine_layer_component.hpp
@@ -0,0 +1,22 @@
+#ifndef _RIVE_STATE_MACHINE_LAYER_COMPONENT_HPP_
+#define _RIVE_STATE_MACHINE_LAYER_COMPONENT_HPP_
+#include "rive/generated/animation/state_machine_layer_component_base.hpp"
+#include <vector>
+
+namespace rive
+{
+class StateMachineFireEvent;
+class StateMachineLayerComponent : public StateMachineLayerComponentBase
+{
+    friend class StateMachineLayerComponentImporter;
+
+public:
+    const std::vector<StateMachineFireEvent*>& events() const { return m_events; }
+    ~StateMachineLayerComponent() override;
+
+private:
+    std::vector<StateMachineFireEvent*> m_events;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/state_machine_listener.hpp b/include/rive/animation/state_machine_listener.hpp
new file mode 100644
index 0000000..2867188
--- /dev/null
+++ b/include/rive/animation/state_machine_listener.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_STATE_MACHINE_LISTENER_HPP_
+#define _RIVE_STATE_MACHINE_LISTENER_HPP_
+#include "rive/generated/animation/state_machine_listener_base.hpp"
+#include "rive/listener_type.hpp"
+#include "rive/math/vec2d.hpp"
+
+namespace rive
+{
+class Shape;
+class StateMachineListenerImporter;
+class ListenerAction;
+class StateMachineInstance;
+class StateMachineListener : public StateMachineListenerBase
+{
+    friend class StateMachineListenerImporter;
+
+public:
+    StateMachineListener();
+    ~StateMachineListener() override;
+
+    ListenerType listenerType() const { return (ListenerType)listenerTypeValue(); }
+    size_t actionCount() const { return m_actions.size(); }
+
+    const ListenerAction* action(size_t index) const;
+    StatusCode import(ImportStack& importStack) override;
+
+    void performChanges(StateMachineInstance* stateMachineInstance,
+                        Vec2D position,
+                        Vec2D previousPosition) const;
+
+private:
+    void addAction(std::unique_ptr<ListenerAction>);
+    std::vector<std::unique_ptr<ListenerAction>> m_actions;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/state_machine_number.hpp b/include/rive/animation/state_machine_number.hpp
new file mode 100644
index 0000000..ca3e4e1
--- /dev/null
+++ b/include/rive/animation/state_machine_number.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_STATE_MACHINE_NUMBER_HPP_
+#define _RIVE_STATE_MACHINE_NUMBER_HPP_
+#include "rive/generated/animation/state_machine_number_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class StateMachineNumber : public StateMachineNumberBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/state_machine_trigger.hpp b/include/rive/animation/state_machine_trigger.hpp
new file mode 100644
index 0000000..c556c57
--- /dev/null
+++ b/include/rive/animation/state_machine_trigger.hpp
@@ -0,0 +1,11 @@
+#ifndef _RIVE_STATE_MACHINE_TRIGGER_HPP_
+#define _RIVE_STATE_MACHINE_TRIGGER_HPP_
+#include "rive/generated/animation/state_machine_trigger_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class StateMachineTrigger : public StateMachineTriggerBase
+{};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/state_transition.hpp b/include/rive/animation/state_transition.hpp
new file mode 100644
index 0000000..c021c2c
--- /dev/null
+++ b/include/rive/animation/state_transition.hpp
@@ -0,0 +1,131 @@
+#ifndef _RIVE_STATE_TRANSITION_HPP_
+#define _RIVE_STATE_TRANSITION_HPP_
+#include "rive/animation/cubic_interpolator.hpp"
+#include "rive/animation/state_transition_flags.hpp"
+#include "rive/generated/animation/state_transition_base.hpp"
+#include <stdio.h>
+#include <vector>
+
+namespace rive
+{
+class LayerState;
+class StateMachineLayerImporter;
+class StateTransitionImporter;
+class TransitionCondition;
+class StateInstance;
+class StateMachineInstance;
+class LinearAnimation;
+class LinearAnimationInstance;
+
+enum class AllowTransition : unsigned char
+{
+    no,
+    waitingForExit,
+    yes
+};
+
+class StateTransition : public StateTransitionBase
+{
+    friend class StateMachineLayerImporter;
+    friend class StateTransitionImporter;
+
+private:
+    StateTransitionFlags transitionFlags() const
+    {
+        return static_cast<StateTransitionFlags>(flags());
+    }
+    LayerState* m_StateTo = nullptr;
+    uint32_t m_EvaluatedRandomWeight = 1;
+    CubicInterpolator* m_Interpolator = nullptr;
+
+    std::vector<TransitionCondition*> m_Conditions;
+    void addCondition(TransitionCondition* condition);
+
+public:
+    ~StateTransition() override;
+    const LayerState* stateTo() const { return m_StateTo; }
+    inline CubicInterpolator* interpolator() const { return m_Interpolator; }
+
+    inline uint32_t evaluatedRandomWeight() const { return m_EvaluatedRandomWeight; }
+    void evaluatedRandomWeight(uint32_t value) { m_EvaluatedRandomWeight = value; }
+
+    StatusCode onAddedDirty(CoreContext* context) override;
+    StatusCode onAddedClean(CoreContext* context) override;
+
+    /// Whether the transition is marked disabled (usually done in the
+    /// editor).
+    bool isDisabled() const
+    {
+        return (transitionFlags() & StateTransitionFlags::Disabled) ==
+               StateTransitionFlags::Disabled;
+    }
+
+    /// Returns AllowTransition::yes when this transition can be taken from
+    /// stateFrom with the given inputs.
+    AllowTransition allowed(StateInstance* stateFrom,
+                            StateMachineInstance* stateMachineInstance,
+                            bool ignoreTriggers) const;
+
+    /// Whether the animation is held at exit or if it keeps advancing
+    /// during mixing.
+    bool pauseOnExit() const
+    {
+        return (transitionFlags() & StateTransitionFlags::PauseOnExit) ==
+               StateTransitionFlags::PauseOnExit;
+    }
+
+    /// Whether exit time is enabled. All other conditions still apply, the
+    /// exit time is effectively an AND with the rest of the conditions.
+    bool enableExitTime() const
+    {
+        return (transitionFlags() & StateTransitionFlags::EnableExitTime) ==
+               StateTransitionFlags::EnableExitTime;
+    }
+
+    /// Whether the transition can be interrupted.
+    bool enableEarlyExit() const
+    {
+        return (transitionFlags() & StateTransitionFlags::EnableEarlyExit) ==
+               StateTransitionFlags::EnableEarlyExit;
+    }
+
+    StatusCode import(ImportStack& importStack) override;
+
+    size_t conditionCount() const { return m_Conditions.size(); }
+    TransitionCondition* condition(size_t index) const
+    {
+        if (index < m_Conditions.size())
+        {
+            return m_Conditions[index];
+        }
+        return nullptr;
+    }
+
+    /// The amount of time to mix the outgoing animation onto the incoming
+    /// one when changing state. Only applies when going out from an
+    /// AnimationState.
+    float mixTime(const LayerState* stateFrom) const;
+
+    /// Computes the exit time in seconds of the stateFrom. Set absolute to
+    /// true if you want the returned time to be relative to the entire
+    /// animation. Set absolute to false if you want it relative to the work
+    /// area.
+    float exitTimeSeconds(const LayerState* stateFrom, bool absolute = false) const;
+
+    /// Provide the animation instance to use for computing percentage
+    /// durations for exit time.
+    virtual const LinearAnimationInstance* exitTimeAnimationInstance(
+        const StateInstance* from) const;
+
+    /// Provide the animation to use for computing percentage durations for
+    /// exit time.
+    virtual const LinearAnimation* exitTimeAnimation(const LayerState* from) const;
+
+    /// Retruns true when we need to hold the exit time, also applies the
+    /// correct time to the animation instance in the stateFrom, when
+    /// applicable (when it's an AnimationState).
+    bool applyExitCondition(StateInstance* stateFrom) const;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/state_transition_flags.hpp b/include/rive/animation/state_transition_flags.hpp
new file mode 100644
index 0000000..87d718e
--- /dev/null
+++ b/include/rive/animation/state_transition_flags.hpp
@@ -0,0 +1,87 @@
+#ifndef _RIVE_STATE_TRANSITION_FLAGS_HPP_
+#define _RIVE_STATE_TRANSITION_FLAGS_HPP_
+
+#include <type_traits>
+
+namespace rive
+{
+enum class StateTransitionFlags : unsigned char
+{
+    None = 0,
+
+    /// Whether the transition is disabled.
+    Disabled = 1 << 0,
+
+    /// Whether the transition duration is a percentage or time in ms.
+    DurationIsPercentage = 1 << 1,
+
+    /// Whether exit time is enabled.
+    EnableExitTime = 1 << 2,
+
+    /// Whether the exit time is a percentage or time in ms.
+    ExitTimeIsPercentage = 1 << 3,
+
+    /// Whether the animation is held at exit or if it keeps advancing
+    /// during mixing.
+    PauseOnExit = 1 << 4,
+
+    /// Whether the transition can exit before it is complete.
+    EnableEarlyExit = 1 << 5
+
+};
+
+inline constexpr StateTransitionFlags operator&(StateTransitionFlags lhs, StateTransitionFlags rhs)
+{
+    return static_cast<StateTransitionFlags>(
+        static_cast<std::underlying_type<StateTransitionFlags>::type>(lhs) &
+        static_cast<std::underlying_type<StateTransitionFlags>::type>(rhs));
+}
+
+inline constexpr StateTransitionFlags operator^(StateTransitionFlags lhs, StateTransitionFlags rhs)
+{
+    return static_cast<StateTransitionFlags>(
+        static_cast<std::underlying_type<StateTransitionFlags>::type>(lhs) ^
+        static_cast<std::underlying_type<StateTransitionFlags>::type>(rhs));
+}
+
+inline constexpr StateTransitionFlags operator|(StateTransitionFlags lhs, StateTransitionFlags rhs)
+{
+    return static_cast<StateTransitionFlags>(
+        static_cast<std::underlying_type<StateTransitionFlags>::type>(lhs) |
+        static_cast<std::underlying_type<StateTransitionFlags>::type>(rhs));
+}
+
+inline constexpr StateTransitionFlags operator~(StateTransitionFlags rhs)
+{
+    return static_cast<StateTransitionFlags>(
+        ~static_cast<std::underlying_type<StateTransitionFlags>::type>(rhs));
+}
+
+inline StateTransitionFlags& operator|=(StateTransitionFlags& lhs, StateTransitionFlags rhs)
+{
+    lhs = static_cast<StateTransitionFlags>(
+        static_cast<std::underlying_type<StateTransitionFlags>::type>(lhs) |
+        static_cast<std::underlying_type<StateTransitionFlags>::type>(rhs));
+
+    return lhs;
+}
+
+inline StateTransitionFlags& operator&=(StateTransitionFlags& lhs, StateTransitionFlags rhs)
+{
+    lhs = static_cast<StateTransitionFlags>(
+        static_cast<std::underlying_type<StateTransitionFlags>::type>(lhs) &
+        static_cast<std::underlying_type<StateTransitionFlags>::type>(rhs));
+
+    return lhs;
+}
+
+inline StateTransitionFlags& operator^=(StateTransitionFlags& lhs, StateTransitionFlags rhs)
+{
+    lhs = static_cast<StateTransitionFlags>(
+        static_cast<std::underlying_type<StateTransitionFlags>::type>(lhs) ^
+        static_cast<std::underlying_type<StateTransitionFlags>::type>(rhs));
+
+    return lhs;
+}
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/system_state_instance.hpp b/include/rive/animation/system_state_instance.hpp
new file mode 100644
index 0000000..49f64b0
--- /dev/null
+++ b/include/rive/animation/system_state_instance.hpp
@@ -0,0 +1,25 @@
+#ifndef _RIVE_SYSTEM_STATE_INSTANCE_HPP_
+#define _RIVE_SYSTEM_STATE_INSTANCE_HPP_
+
+#include <string>
+#include "rive/animation/state_instance.hpp"
+
+namespace rive
+{
+class StateMachineInstance;
+
+/// Represents an instance of a system state machine. Basically a
+/// placeholder that may have meaning to the state machine itself, or is
+/// just a no-op state (perhaps an unknown to this runtime state-type).
+class SystemStateInstance : public StateInstance
+{
+public:
+    SystemStateInstance(const LayerState* layerState, ArtboardInstance* instance);
+
+    void advance(float seconds, StateMachineInstance* stateMachineInstance) override;
+    void apply(ArtboardInstance* artboard, float mix) override;
+
+    bool keepGoing() const override;
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/transition_artboard_condition.hpp b/include/rive/animation/transition_artboard_condition.hpp
new file mode 100644
index 0000000..7892537
--- /dev/null
+++ b/include/rive/animation/transition_artboard_condition.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_TRANSITION_ARTBOARD_CONDITION_HPP_
+#define _RIVE_TRANSITION_ARTBOARD_CONDITION_HPP_
+#include "rive/generated/animation/transition_artboard_condition_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TransitionArtboardCondition : public TransitionArtboardConditionBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/transition_bool_condition.hpp b/include/rive/animation/transition_bool_condition.hpp
new file mode 100644
index 0000000..fa5a03e
--- /dev/null
+++ b/include/rive/animation/transition_bool_condition.hpp
@@ -0,0 +1,17 @@
+#ifndef _RIVE_TRANSITION_BOOL_CONDITION_HPP_
+#define _RIVE_TRANSITION_BOOL_CONDITION_HPP_
+#include "rive/generated/animation/transition_bool_condition_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TransitionBoolCondition : public TransitionBoolConditionBase
+{
+public:
+    bool evaluate(const StateMachineInstance* stateMachineInstance) const override;
+
+protected:
+    bool validateInputType(const StateMachineInput* input) const override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/transition_comparator.hpp b/include/rive/animation/transition_comparator.hpp
new file mode 100644
index 0000000..38aab79
--- /dev/null
+++ b/include/rive/animation/transition_comparator.hpp
@@ -0,0 +1,28 @@
+#ifndef _RIVE_TRANSITION_COMPARATOR_HPP_
+#define _RIVE_TRANSITION_COMPARATOR_HPP_
+#include "rive/generated/animation/transition_comparator_base.hpp"
+#include "rive/animation/transition_condition_op.hpp"
+#include "rive/viewmodel/viewmodel_instance.hpp"
+#include "rive/viewmodel/viewmodel_instance_value.hpp"
+#include <stdio.h>
+namespace rive
+{
+class StateMachineInstance;
+class TransitionComparator : public TransitionComparatorBase
+{
+public:
+    StatusCode import(ImportStack& importStack) override;
+    virtual bool compare(TransitionComparator* comparand,
+                         TransitionConditionOp operation,
+                         const StateMachineInstance* stateMachineInstance);
+
+protected:
+    bool compareNumbers(float left, float right, TransitionConditionOp op);
+    bool compareBooleans(bool left, bool right, TransitionConditionOp op);
+    bool compareEnums(uint16_t left, uint16_t right, TransitionConditionOp op);
+    bool compareColors(int left, int right, TransitionConditionOp op);
+    bool compareStrings(std::string left, std::string right, TransitionConditionOp op);
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/transition_condition.hpp b/include/rive/animation/transition_condition.hpp
new file mode 100644
index 0000000..9fc8db6
--- /dev/null
+++ b/include/rive/animation/transition_condition.hpp
@@ -0,0 +1,26 @@
+#ifndef _RIVE_TRANSITION_CONDITION_HPP_
+#define _RIVE_TRANSITION_CONDITION_HPP_
+#include "rive/generated/animation/transition_condition_base.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+
+namespace rive
+{
+class StateMachineInput;
+class SMIInput;
+
+class TransitionCondition : public TransitionConditionBase
+{
+public:
+    StatusCode onAddedDirty(CoreContext* context) override;
+    StatusCode onAddedClean(CoreContext* context) override;
+
+    StatusCode import(ImportStack& importStack) override;
+
+    virtual bool evaluate(const StateMachineInstance* stateMachineInstance) const { return true; }
+
+protected:
+    virtual bool validateInputType(const StateMachineInput* input) const { return true; }
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/transition_condition_op.hpp b/include/rive/animation/transition_condition_op.hpp
new file mode 100644
index 0000000..cf2ef7c
--- /dev/null
+++ b/include/rive/animation/transition_condition_op.hpp
@@ -0,0 +1,17 @@
+#ifndef _RIVE_TRANSITION_CONDITION_OP_HPP_
+#define _RIVE_TRANSITION_CONDITION_OP_HPP_
+
+namespace rive
+{
+enum class TransitionConditionOp : int
+{
+    equal = 0,
+    notEqual = 1,
+    lessThanOrEqual = 2,
+    greaterThanOrEqual = 3,
+    lessThan = 4,
+    greaterThan = 5
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/transition_input_condition.hpp b/include/rive/animation/transition_input_condition.hpp
new file mode 100644
index 0000000..90ab05a
--- /dev/null
+++ b/include/rive/animation/transition_input_condition.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_TRANSITION_INPUT_CONDITION_HPP_
+#define _RIVE_TRANSITION_INPUT_CONDITION_HPP_
+#include "rive/generated/animation/transition_input_condition_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TransitionInputCondition : public TransitionInputConditionBase
+{
+public:
+    StatusCode import(ImportStack& importStack) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/transition_number_condition.hpp b/include/rive/animation/transition_number_condition.hpp
new file mode 100644
index 0000000..d794097
--- /dev/null
+++ b/include/rive/animation/transition_number_condition.hpp
@@ -0,0 +1,17 @@
+#ifndef _RIVE_TRANSITION_NUMBER_CONDITION_HPP_
+#define _RIVE_TRANSITION_NUMBER_CONDITION_HPP_
+#include "rive/generated/animation/transition_number_condition_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TransitionNumberCondition : public TransitionNumberConditionBase
+{
+protected:
+    bool validateInputType(const StateMachineInput* input) const override;
+
+public:
+    bool evaluate(const StateMachineInstance* stateMachineInstance) const override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/transition_property_artboard_comparator.hpp b/include/rive/animation/transition_property_artboard_comparator.hpp
new file mode 100644
index 0000000..184b649
--- /dev/null
+++ b/include/rive/animation/transition_property_artboard_comparator.hpp
@@ -0,0 +1,20 @@
+#ifndef _RIVE_TRANSITION_PROPERTY_ARTBOARD_COMPARATOR_HPP_
+#define _RIVE_TRANSITION_PROPERTY_ARTBOARD_COMPARATOR_HPP_
+#include "rive/generated/animation/transition_property_artboard_comparator_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class Artboard;
+class TransitionPropertyArtboardComparator : public TransitionPropertyArtboardComparatorBase
+{
+public:
+    bool compare(TransitionComparator* comparand,
+                 TransitionConditionOp operation,
+                 const StateMachineInstance* stateMachineInstance) override;
+
+private:
+    float propertyValue(const StateMachineInstance* stateMachineInstance);
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/transition_property_comparator.hpp b/include/rive/animation/transition_property_comparator.hpp
new file mode 100644
index 0000000..e2d9914
--- /dev/null
+++ b/include/rive/animation/transition_property_comparator.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_TRANSITION_PROPERTY_COMPARATOR_HPP_
+#define _RIVE_TRANSITION_PROPERTY_COMPARATOR_HPP_
+#include "rive/generated/animation/transition_property_comparator_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TransitionPropertyComparator : public TransitionPropertyComparatorBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/transition_property_viewmodel_comparator.hpp b/include/rive/animation/transition_property_viewmodel_comparator.hpp
new file mode 100644
index 0000000..6c5bd87
--- /dev/null
+++ b/include/rive/animation/transition_property_viewmodel_comparator.hpp
@@ -0,0 +1,33 @@
+#ifndef _RIVE_TRANSITION_PROPERTY_VIEW_MODEL_COMPARATOR_HPP_
+#define _RIVE_TRANSITION_PROPERTY_VIEW_MODEL_COMPARATOR_HPP_
+#include "rive/generated/animation/transition_property_viewmodel_comparator_base.hpp"
+#include "rive/data_bind/bindable_property.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TransitionPropertyViewModelComparator : public TransitionPropertyViewModelComparatorBase
+{
+public:
+    StatusCode import(ImportStack& importStack) override;
+    bool compare(TransitionComparator* comparand,
+                 TransitionConditionOp operation,
+                 const StateMachineInstance* stateMachineInstance) override;
+    template <typename T = BindableProperty, typename U>
+    U value(const StateMachineInstance* stateMachineInstance)
+    {
+        if (m_bindableProperty->is<T>())
+        {
+            auto bindableInstance =
+                stateMachineInstance->bindablePropertyInstance(m_bindableProperty);
+            return bindableInstance->as<T>()->propertyValue();
+        }
+        return (new T())->propertyValue();
+    };
+
+protected:
+    BindableProperty* m_bindableProperty;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/transition_trigger_condition.hpp b/include/rive/animation/transition_trigger_condition.hpp
new file mode 100644
index 0000000..7ad999f
--- /dev/null
+++ b/include/rive/animation/transition_trigger_condition.hpp
@@ -0,0 +1,17 @@
+#ifndef _RIVE_TRANSITION_TRIGGER_CONDITION_HPP_
+#define _RIVE_TRANSITION_TRIGGER_CONDITION_HPP_
+#include "rive/generated/animation/transition_trigger_condition_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TransitionTriggerCondition : public TransitionTriggerConditionBase
+{
+public:
+    bool evaluate(const StateMachineInstance* stateMachineInstance) const override;
+
+protected:
+    bool validateInputType(const StateMachineInput* input) const override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/transition_value_boolean_comparator.hpp b/include/rive/animation/transition_value_boolean_comparator.hpp
new file mode 100644
index 0000000..fc8b51c
--- /dev/null
+++ b/include/rive/animation/transition_value_boolean_comparator.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_TRANSITION_VALUE_BOOLEAN_COMPARATOR_HPP_
+#define _RIVE_TRANSITION_VALUE_BOOLEAN_COMPARATOR_HPP_
+#include "rive/generated/animation/transition_value_boolean_comparator_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TransitionValueBooleanComparator : public TransitionValueBooleanComparatorBase
+{
+public:
+    bool compare(TransitionComparator* comparand,
+                 TransitionConditionOp operation,
+                 const StateMachineInstance* stateMachineInstance) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/transition_value_color_comparator.hpp b/include/rive/animation/transition_value_color_comparator.hpp
new file mode 100644
index 0000000..c75c062
--- /dev/null
+++ b/include/rive/animation/transition_value_color_comparator.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_TRANSITION_VALUE_COLOR_COMPARATOR_HPP_
+#define _RIVE_TRANSITION_VALUE_COLOR_COMPARATOR_HPP_
+#include "rive/generated/animation/transition_value_color_comparator_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TransitionValueColorComparator : public TransitionValueColorComparatorBase
+{
+public:
+    bool compare(TransitionComparator* comparand,
+                 TransitionConditionOp operation,
+                 const StateMachineInstance* stateMachineInstance) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/transition_value_comparator.hpp b/include/rive/animation/transition_value_comparator.hpp
new file mode 100644
index 0000000..4ebba61
--- /dev/null
+++ b/include/rive/animation/transition_value_comparator.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_TRANSITION_VALUE_COMPARATOR_HPP_
+#define _RIVE_TRANSITION_VALUE_COMPARATOR_HPP_
+#include "rive/generated/animation/transition_value_comparator_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TransitionValueComparator : public TransitionValueComparatorBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/transition_value_condition.hpp b/include/rive/animation/transition_value_condition.hpp
new file mode 100644
index 0000000..6f79a02
--- /dev/null
+++ b/include/rive/animation/transition_value_condition.hpp
@@ -0,0 +1,15 @@
+#ifndef _RIVE_TRANSITION_VALUE_CONDITION_HPP_
+#define _RIVE_TRANSITION_VALUE_CONDITION_HPP_
+#include "rive/generated/animation/transition_value_condition_base.hpp"
+#include "rive/animation/transition_condition_op.hpp"
+
+namespace rive
+{
+class TransitionValueCondition : public TransitionValueConditionBase
+{
+public:
+    TransitionConditionOp op() const { return (TransitionConditionOp)opValue(); }
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/transition_value_enum_comparator.hpp b/include/rive/animation/transition_value_enum_comparator.hpp
new file mode 100644
index 0000000..62bd13d
--- /dev/null
+++ b/include/rive/animation/transition_value_enum_comparator.hpp
@@ -0,0 +1,11 @@
+#ifndef _RIVE_TRANSITION_VALUE_ENUM_COMPARATOR_HPP_
+#define _RIVE_TRANSITION_VALUE_ENUM_COMPARATOR_HPP_
+#include "rive/generated/animation/transition_value_enum_comparator_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TransitionValueEnumComparator : public TransitionValueEnumComparatorBase
+{};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/transition_value_number_comparator.hpp b/include/rive/animation/transition_value_number_comparator.hpp
new file mode 100644
index 0000000..daf644c
--- /dev/null
+++ b/include/rive/animation/transition_value_number_comparator.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_TRANSITION_VALUE_NUMBER_COMPARATOR_HPP_
+#define _RIVE_TRANSITION_VALUE_NUMBER_COMPARATOR_HPP_
+#include "rive/generated/animation/transition_value_number_comparator_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TransitionValueNumberComparator : public TransitionValueNumberComparatorBase
+{
+public:
+    bool compare(TransitionComparator* comparand,
+                 TransitionConditionOp operation,
+                 const StateMachineInstance* stateMachineInstance) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/transition_value_string_comparator.hpp b/include/rive/animation/transition_value_string_comparator.hpp
new file mode 100644
index 0000000..c634db8
--- /dev/null
+++ b/include/rive/animation/transition_value_string_comparator.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_TRANSITION_VALUE_STRING_COMPARATOR_HPP_
+#define _RIVE_TRANSITION_VALUE_STRING_COMPARATOR_HPP_
+#include "rive/generated/animation/transition_value_string_comparator_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TransitionValueStringComparator : public TransitionValueStringComparatorBase
+{
+public:
+    bool compare(TransitionComparator* comparand,
+                 TransitionConditionOp operation,
+                 const StateMachineInstance* stateMachineInstance) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/transition_viewmodel_condition.hpp b/include/rive/animation/transition_viewmodel_condition.hpp
new file mode 100644
index 0000000..508807d
--- /dev/null
+++ b/include/rive/animation/transition_viewmodel_condition.hpp
@@ -0,0 +1,35 @@
+#ifndef _RIVE_TRANSITION_VIEW_MODEL_CONDITION_HPP_
+#define _RIVE_TRANSITION_VIEW_MODEL_CONDITION_HPP_
+#include "rive/generated/animation/transition_viewmodel_condition_base.hpp"
+#include "rive/animation/transition_comparator.hpp"
+#include "rive/animation/transition_condition_op.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TransitionViewModelCondition : public TransitionViewModelConditionBase
+{
+protected:
+    TransitionComparator* m_leftComparator;
+    TransitionComparator* m_rightComparator;
+
+public:
+    bool evaluate(const StateMachineInstance* stateMachineInstance) const override;
+    TransitionComparator* leftComparator() const { return m_leftComparator; };
+    TransitionComparator* rightComparator() const { return m_rightComparator; };
+    void comparator(TransitionComparator* value)
+    {
+        if (m_leftComparator == nullptr)
+        {
+            m_leftComparator = value;
+        }
+        else
+        {
+            m_rightComparator = value;
+        }
+    };
+    TransitionConditionOp op() const { return (TransitionConditionOp)opValue(); }
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/artboard.hpp b/include/rive/artboard.hpp
new file mode 100644
index 0000000..b054943
--- /dev/null
+++ b/include/rive/artboard.hpp
@@ -0,0 +1,415 @@
+#ifndef _RIVE_ARTBOARD_HPP_
+#define _RIVE_ARTBOARD_HPP_
+
+#include "rive/animation/linear_animation.hpp"
+#include "rive/animation/state_machine.hpp"
+#include "rive/core_context.hpp"
+#include "rive/data_bind/data_bind.hpp"
+#include "rive/data_bind/data_context.hpp"
+#include "rive/data_bind/data_bind_context.hpp"
+#include "rive/viewmodel/viewmodel_instance_value.hpp"
+#include "rive/viewmodel/viewmodel_instance_viewmodel.hpp"
+#include "rive/generated/artboard_base.hpp"
+#include "rive/hit_info.hpp"
+#include "rive/math/aabb.hpp"
+#include "rive/renderer.hpp"
+#include "rive/text/text_value_run.hpp"
+#include "rive/event.hpp"
+#include "rive/audio/audio_engine.hpp"
+#include "rive/math/raw_path.hpp"
+
+#include <queue>
+#include <unordered_set>
+#include <vector>
+
+namespace rive
+{
+class File;
+class Drawable;
+class Factory;
+class Node;
+class DrawTarget;
+class ArtboardImporter;
+class NestedArtboard;
+class ArtboardInstance;
+class LinearAnimationInstance;
+class Scene;
+class StateMachineInstance;
+class Joystick;
+class TextValueRun;
+class Event;
+class SMIBool;
+class SMIInput;
+class SMINumber;
+class SMITrigger;
+
+#ifdef WITH_RIVE_TOOLS
+typedef void (*ArtboardCallback)(Artboard*);
+#endif
+
+class Artboard : public ArtboardBase, public CoreContext
+{
+    friend class File;
+    friend class ArtboardImporter;
+    friend class Component;
+
+private:
+    std::vector<Core*> m_Objects;
+    std::vector<LinearAnimation*> m_Animations;
+    std::vector<StateMachine*> m_StateMachines;
+    std::vector<Component*> m_DependencyOrder;
+    std::vector<Drawable*> m_Drawables;
+    std::vector<DrawTarget*> m_DrawTargets;
+    std::vector<NestedArtboard*> m_NestedArtboards;
+    std::vector<Joystick*> m_Joysticks;
+    std::vector<DataBind*> m_DataBinds;
+    std::vector<DataBind*> m_AllDataBinds;
+    DataContext* m_DataContext = nullptr;
+    bool m_JoysticksApplyBeforeUpdate = true;
+    bool m_HasChangedDrawOrderInLastUpdate = false;
+
+    unsigned int m_DirtDepth = 0;
+    RawPath m_backgroundRawPath;
+    Factory* m_Factory = nullptr;
+    Drawable* m_FirstDrawable = nullptr;
+    bool m_IsInstance = false;
+    bool m_FrameOrigin = true;
+    std::unordered_set<LayoutComponent*> m_dirtyLayout;
+    float m_originalWidth = 0;
+    float m_originalHeight = 0;
+    bool m_updatesOwnLayout = true;
+    Artboard* parentArtboard() const;
+    NestedArtboard* m_host = nullptr;
+    bool sharesLayoutWithHost() const;
+
+#ifdef EXTERNAL_RIVE_AUDIO_ENGINE
+    rcp<AudioEngine> m_audioEngine;
+#endif
+
+    void sortDependencies();
+    void sortDrawOrder();
+    void updateDataBinds();
+    void updateRenderPath() override;
+    void update(ComponentDirt value) override;
+
+public:
+    void host(NestedArtboard* nestedArtboard);
+    NestedArtboard* host() const;
+
+private:
+#ifdef TESTING
+public:
+    Artboard(Factory* factory) : m_Factory(factory) {}
+#endif
+    void addObject(Core* object);
+    void addAnimation(LinearAnimation* object);
+    void addStateMachine(StateMachine* object);
+
+public:
+    Artboard();
+    ~Artboard() override;
+    StatusCode initialize();
+
+    Core* resolve(uint32_t id) const override;
+
+    /// Find the id of a component in the artboard the object in the artboard. The artboard
+    /// itself has id 0 so we use that as a flag for not found.
+    uint32_t idOf(Core* object) const;
+
+    Factory* factory() const { return m_Factory; }
+
+    // EXPERIMENTAL -- for internal testing only for now.
+    // DO NOT RELY ON THIS as it may change/disappear in the future.
+    Core* hitTest(HitInfo*, const Mat2D&) override;
+
+    void onComponentDirty(Component* component);
+
+    /// Update components that depend on each other in DAG order.
+    bool updateComponents();
+    void onDirty(ComponentDirt dirt) override;
+
+    // Artboards don't update their world transforms in the same way
+    // as other TransformComponents so we override this.
+    // This is because LayoutComponent extends Drawable, but
+    // Artboard is a special type of LayoutComponent
+    void updateWorldTransform() override {}
+
+    void markLayoutDirty(LayoutComponent* layoutComponent);
+
+    void* takeLayoutNode();
+    bool syncStyleChanges();
+    bool canHaveOverrides() override { return true; }
+
+    bool advance(double elapsedSeconds, bool nested = true);
+    bool advanceInternal(double elapsedSeconds, bool isRoot, bool nested = true);
+    bool hasChangedDrawOrderInLastUpdate() { return m_HasChangedDrawOrderInLastUpdate; };
+    Drawable* firstDrawable() { return m_FirstDrawable; };
+
+    enum class DrawOption
+    {
+        kNormal,
+        kHideBG,
+        kHideFG,
+    };
+    void draw(Renderer* renderer, DrawOption option);
+    void draw(Renderer* renderer) override;
+    void addToRenderPath(RenderPath* path, const Mat2D& transform);
+
+#ifdef TESTING
+    RenderPath* clipPath() const { return m_clipPath.get(); }
+    RenderPath* backgroundPath() const { return m_backgroundPath.get(); }
+#endif
+
+    const std::vector<Core*>& objects() const { return m_Objects; }
+    const std::vector<NestedArtboard*> nestedArtboards() const { return m_NestedArtboards; }
+    const std::vector<DataBind*> dataBinds() const { return m_DataBinds; }
+    DataContext* dataContext() { return m_DataContext; }
+    NestedArtboard* nestedArtboard(const std::string& name) const;
+    NestedArtboard* nestedArtboardAtPath(const std::string& path) const;
+
+    float originalWidth() const { return m_originalWidth; }
+    float originalHeight() const { return m_originalHeight; }
+    float layoutWidth() const;
+    float layoutHeight() const;
+    float layoutX() const;
+    float layoutY() const;
+    AABB bounds() const;
+    Vec2D origin() const;
+
+    // Can we hide these from the public? (they use playable)
+    bool isTranslucent() const;
+    bool isTranslucent(const LinearAnimation*) const;
+    bool isTranslucent(const LinearAnimationInstance*) const;
+    void dataContext(DataContext* dataContext, DataContext* parent);
+    void internalDataContext(DataContext* dataContext, DataContext* parent, bool isRoot);
+    void dataContextFromInstance(ViewModelInstance* viewModelInstance, DataContext* parent);
+    void dataContextFromInstance(ViewModelInstance* viewModelInstance,
+                                 DataContext* parent,
+                                 bool isRoot);
+    void dataContextFromInstance(ViewModelInstance* viewModelInstance);
+    void addDataBind(DataBind* dataBind);
+    void populateDataBinds(std::vector<DataBind*>* dataBinds);
+    void sortDataBinds(std::vector<DataBind*> dataBinds);
+    bool hasAudio() const;
+
+    template <typename T = Component> T* find(const std::string& name)
+    {
+        for (auto object : m_Objects)
+        {
+            if (object != nullptr && object->is<T>() && object->as<T>()->name() == name)
+            {
+                return static_cast<T*>(object);
+            }
+        }
+        return nullptr;
+    }
+
+    template <typename T = Component> size_t count()
+    {
+        size_t count = 0;
+        for (auto object : m_Objects)
+        {
+            if (object != nullptr && object->is<T>())
+            {
+                count++;
+            }
+        }
+        return count;
+    }
+
+    template <typename T = Component> T* objectAt(size_t index)
+    {
+        size_t count = 0;
+        for (auto object : m_Objects)
+        {
+            if (object != nullptr && object->is<T>())
+            {
+                if (count++ == index)
+                {
+                    return static_cast<T*>(object);
+                }
+            }
+        }
+        return nullptr;
+    }
+
+    template <typename T = Component> std::vector<T*> find()
+    {
+        std::vector<T*> results;
+        for (auto object : m_Objects)
+        {
+            if (object != nullptr && object->is<T>())
+            {
+                results.push_back(static_cast<T*>(object));
+            }
+        }
+        return results;
+    }
+
+    size_t animationCount() const { return m_Animations.size(); }
+    std::string animationNameAt(size_t index) const;
+
+    size_t stateMachineCount() const { return m_StateMachines.size(); }
+    std::string stateMachineNameAt(size_t index) const;
+
+    LinearAnimation* firstAnimation() const { return animation(0); }
+    LinearAnimation* animation(const std::string& name) const;
+    LinearAnimation* animation(size_t index) const;
+
+    StateMachine* firstStateMachine() const { return stateMachine(0); }
+    StateMachine* stateMachine(const std::string& name) const;
+    StateMachine* stateMachine(size_t index) const;
+
+    /// When provided, the designer has specified that this artboard should
+    /// always autoplay this StateMachine. Returns -1 if it was not
+    // provided.
+    int defaultStateMachineIndex() const;
+
+    /// Make an instance of this artboard.
+    template <typename T = ArtboardInstance> std::unique_ptr<T> instance() const
+    {
+        std::unique_ptr<T> artboardClone(new T);
+        artboardClone->copy(*this);
+
+        artboardClone->m_Factory = m_Factory;
+        artboardClone->m_FrameOrigin = m_FrameOrigin;
+        artboardClone->m_DataContext = m_DataContext;
+        artboardClone->m_IsInstance = true;
+        artboardClone->m_originalWidth = m_originalWidth;
+        artboardClone->m_originalHeight = m_originalHeight;
+
+        std::vector<Core*>& cloneObjects = artboardClone->m_Objects;
+        cloneObjects.push_back(artboardClone.get());
+
+        if (!m_Objects.empty())
+        {
+            // Skip first object (artboard).
+            auto itr = m_Objects.begin();
+            while (++itr != m_Objects.end())
+            {
+                auto object = *itr;
+                cloneObjects.push_back(object == nullptr ? nullptr : object->clone());
+                // For each object, clone its data bind objects and target their clones
+                for (auto dataBind : m_DataBinds)
+                {
+                    if (dataBind->target() == object)
+                    {
+                        auto dataBindClone = static_cast<DataBind*>(dataBind->clone());
+                        dataBindClone->target(cloneObjects.back());
+                        dataBindClone->converter(dataBind->converter());
+                        artboardClone->m_DataBinds.push_back(dataBindClone);
+                    }
+                }
+            }
+        }
+
+        for (auto animation : m_Animations)
+        {
+            artboardClone->m_Animations.push_back(animation);
+        }
+        for (auto stateMachine : m_StateMachines)
+        {
+            artboardClone->m_StateMachines.push_back(stateMachine);
+        }
+
+        if (artboardClone->initialize() != StatusCode::Ok)
+        {
+            artboardClone = nullptr;
+        }
+
+        assert(artboardClone->isInstance());
+        return artboardClone;
+    }
+
+    /// Returns true if the artboard is an instance of another
+    bool isInstance() const { return m_IsInstance; }
+
+    /// Returns true when the artboard will shift the origin from the top
+    /// left to the relative width/height of the artboard itself. This is
+    /// what the editor does visually when you change the origin value to
+    /// give context as to where the origin lies within the framed bounds.
+    bool frameOrigin() const { return m_FrameOrigin; }
+    /// When composing multiple artboards together in a common world-space,
+    /// it may be desireable to have them share the same space regardless of
+    /// origin offset from the bounding artboard. Set frameOrigin to false
+    /// to move the bounds relative to the origin instead of the origin
+    /// relative to the bounds.
+    void frameOrigin(bool value);
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        bool result = ArtboardBase::deserialize(propertyKey, reader);
+        switch (propertyKey)
+        {
+            case widthPropertyKey:
+                m_originalWidth = width();
+                break;
+            case heightPropertyKey:
+                m_originalHeight = height();
+                break;
+            default:
+                break;
+        }
+        return result;
+    }
+
+    StatusCode import(ImportStack& importStack) override;
+
+    float volume() const;
+    void volume(float value);
+
+#ifdef EXTERNAL_RIVE_AUDIO_ENGINE
+    rcp<AudioEngine> audioEngine() const;
+    void audioEngine(rcp<AudioEngine> audioEngine);
+#endif
+
+#ifdef WITH_RIVE_LAYOUT
+    void propagateSize() override;
+#endif
+private:
+    float m_volume = 1.0f;
+#ifdef WITH_RIVE_TOOLS
+    ArtboardCallback m_layoutChangedCallback = nullptr;
+    ArtboardCallback m_layoutDirtyCallback = nullptr;
+
+public:
+    void onLayoutChanged(ArtboardCallback callback) { m_layoutChangedCallback = callback; }
+    void onLayoutDirty(ArtboardCallback callback) { m_layoutDirtyCallback = callback; }
+#endif
+};
+
+class ArtboardInstance : public Artboard
+{
+public:
+    ArtboardInstance();
+    ~ArtboardInstance() override;
+
+    std::unique_ptr<LinearAnimationInstance> animationAt(size_t index);
+    std::unique_ptr<LinearAnimationInstance> animationNamed(const std::string& name);
+
+    std::unique_ptr<StateMachineInstance> stateMachineAt(size_t index);
+    std::unique_ptr<StateMachineInstance> stateMachineNamed(const std::string& name);
+
+    /// When provided, the designer has specified that this artboard should
+    /// always autoplay this StateMachine instance. If it was not specified,
+    /// this returns nullptr.
+    std::unique_ptr<StateMachineInstance> defaultStateMachine();
+
+    // This attemps to always return *something*, in this search order:
+    // 1. default statemachine instance
+    // 2. first statemachine instance
+    // 3. first animation instance
+    // 4. nullptr
+    std::unique_ptr<Scene> defaultScene();
+
+    SMIInput* input(const std::string& name, const std::string& path);
+    template <typename InstType>
+    InstType* getNamedInput(const std::string& name, const std::string& path);
+    SMIBool* getBool(const std::string& name, const std::string& path);
+    SMINumber* getNumber(const std::string& name, const std::string& path);
+    SMITrigger* getTrigger(const std::string& name, const std::string& path);
+    TextValueRun* getTextRun(const std::string& name, const std::string& path);
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/assets/asset.hpp b/include/rive/assets/asset.hpp
new file mode 100644
index 0000000..16f8cef
--- /dev/null
+++ b/include/rive/assets/asset.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_ASSET_HPP_
+#define _RIVE_ASSET_HPP_
+#include "rive/generated/assets/asset_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class Asset : public AssetBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/assets/audio_asset.hpp b/include/rive/assets/audio_asset.hpp
new file mode 100644
index 0000000..bc8d89a
--- /dev/null
+++ b/include/rive/assets/audio_asset.hpp
@@ -0,0 +1,30 @@
+#ifndef _RIVE_AUDIO_ASSET_HPP_
+#define _RIVE_AUDIO_ASSET_HPP_
+#include "rive/generated/assets/audio_asset_base.hpp"
+#include "rive/audio/audio_source.hpp"
+#include "rive/audio/audio_engine.hpp"
+
+namespace rive
+{
+class AudioAsset : public AudioAssetBase
+{
+public:
+    AudioAsset();
+    ~AudioAsset() override;
+    bool decode(SimpleArray<uint8_t>&, Factory*) override;
+    std::string fileExtension() const override;
+
+#ifdef TESTING
+    bool hasAudioSource() { return m_audioSource != nullptr; }
+#endif
+
+    rcp<AudioSource> audioSource() { return m_audioSource; }
+    void audioSource(rcp<AudioSource> source) { m_audioSource = source; }
+    void stop(rcp<AudioEngine> engine);
+
+private:
+    rcp<AudioSource> m_audioSource;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/assets/drawable_asset.hpp b/include/rive/assets/drawable_asset.hpp
new file mode 100644
index 0000000..ce2ddaf
--- /dev/null
+++ b/include/rive/assets/drawable_asset.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_DRAWABLE_ASSET_HPP_
+#define _RIVE_DRAWABLE_ASSET_HPP_
+#include "rive/generated/assets/drawable_asset_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class DrawableAsset : public DrawableAssetBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/assets/export_audio.hpp b/include/rive/assets/export_audio.hpp
new file mode 100644
index 0000000..7642863
--- /dev/null
+++ b/include/rive/assets/export_audio.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_EXPORT_AUDIO_HPP_
+#define _RIVE_EXPORT_AUDIO_HPP_
+#include "rive/generated/assets/export_audio_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ExportAudio : public ExportAudioBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/assets/file_asset.hpp b/include/rive/assets/file_asset.hpp
new file mode 100644
index 0000000..22915f4
--- /dev/null
+++ b/include/rive/assets/file_asset.hpp
@@ -0,0 +1,58 @@
+#ifndef _RIVE_FILE_ASSET_HPP_
+#define _RIVE_FILE_ASSET_HPP_
+#include "rive/assets/file_asset_referencer.hpp"
+#include "rive/generated/assets/file_asset_base.hpp"
+#include "rive/span.hpp"
+#include "rive/simple_array.hpp"
+#include <string>
+
+namespace rive
+{
+class Factory;
+class FileAsset : public FileAssetBase
+{
+private:
+    std::vector<uint8_t> m_cdnUuid;
+    std::vector<FileAssetReferencer*> m_fileAssetReferencers;
+
+public:
+    Span<const uint8_t> cdnUuid() const;
+    std::string cdnUuidStr() const;
+
+    void decodeCdnUuid(Span<const uint8_t> value) override;
+    void copyCdnUuid(const FileAssetBase& object) override;
+    virtual bool decode(SimpleArray<uint8_t>&, Factory*) = 0;
+    virtual std::string fileExtension() const = 0;
+    StatusCode import(ImportStack& importStack) override;
+    const std::vector<FileAssetReferencer*> fileAssetReferencers()
+    {
+        return m_fileAssetReferencers;
+    }
+
+    void addFileAssetReferencer(FileAssetReferencer* referencer)
+    {
+        m_fileAssetReferencers.push_back(referencer);
+    }
+
+    void removeFileAssetReferencer(FileAssetReferencer* referencer)
+    {
+        auto itr = m_fileAssetReferencers.begin();
+        while (itr != m_fileAssetReferencers.end())
+        {
+            if (*itr == referencer)
+            {
+                itr = m_fileAssetReferencers.erase(itr);
+            }
+            else
+            {
+                itr++;
+            }
+        }
+    }
+
+    std::string uniqueName() const;
+    std::string uniqueFilename() const;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/assets/file_asset_contents.hpp b/include/rive/assets/file_asset_contents.hpp
new file mode 100644
index 0000000..67457c0
--- /dev/null
+++ b/include/rive/assets/file_asset_contents.hpp
@@ -0,0 +1,22 @@
+#ifndef _RIVE_FILE_ASSET_CONTENTS_HPP_
+#define _RIVE_FILE_ASSET_CONTENTS_HPP_
+#include "rive/generated/assets/file_asset_contents_base.hpp"
+#include <cstdint>
+#include "rive/simple_array.hpp"
+
+namespace rive
+{
+class FileAssetContents : public FileAssetContentsBase
+{
+public:
+    SimpleArray<uint8_t>& bytes();
+    StatusCode import(ImportStack& importStack) override;
+    void decodeBytes(Span<const uint8_t> value) override;
+    void copyBytes(const FileAssetContentsBase& object) override;
+
+private:
+    SimpleArray<uint8_t> m_bytes;
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/assets/file_asset_referencer.hpp b/include/rive/assets/file_asset_referencer.hpp
new file mode 100644
index 0000000..1a82cfc
--- /dev/null
+++ b/include/rive/assets/file_asset_referencer.hpp
@@ -0,0 +1,23 @@
+#ifndef _RIVE_FILE_ASSET_REFERENCER_HPP_
+#define _RIVE_FILE_ASSET_REFERENCER_HPP_
+
+#include <vector>
+#include "rive/importers/import_stack.hpp"
+
+namespace rive
+{
+class FileAsset;
+class FileAssetReferencer
+{
+protected:
+    FileAsset* m_fileAsset = nullptr;
+
+public:
+    virtual ~FileAssetReferencer() = 0;
+    virtual void setAsset(FileAsset* asset);
+    virtual uint32_t assetId() = 0;
+    StatusCode registerReferencer(ImportStack& importStack);
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/assets/folder.hpp b/include/rive/assets/folder.hpp
new file mode 100644
index 0000000..d51c359
--- /dev/null
+++ b/include/rive/assets/folder.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_FOLDER_HPP_
+#define _RIVE_FOLDER_HPP_
+#include "rive/generated/assets/folder_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class Folder : public FolderBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/assets/font_asset.hpp b/include/rive/assets/font_asset.hpp
new file mode 100644
index 0000000..29fad8d
--- /dev/null
+++ b/include/rive/assets/font_asset.hpp
@@ -0,0 +1,22 @@
+#ifndef _RIVE_FONT_ASSET_HPP_
+#define _RIVE_FONT_ASSET_HPP_
+#include "rive/generated/assets/font_asset_base.hpp"
+#include "rive/text_engine.hpp"
+#include "rive/refcnt.hpp"
+
+namespace rive
+{
+class FontAsset : public FontAssetBase
+{
+public:
+    bool decode(SimpleArray<uint8_t>&, Factory*) override;
+    std::string fileExtension() const override;
+    const rcp<Font> font() const { return m_font; }
+    void font(rcp<Font> font);
+
+private:
+    rcp<Font> m_font;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/assets/image_asset.hpp b/include/rive/assets/image_asset.hpp
new file mode 100644
index 0000000..54a3856
--- /dev/null
+++ b/include/rive/assets/image_asset.hpp
@@ -0,0 +1,30 @@
+#ifndef _RIVE_IMAGE_ASSET_HPP_
+#define _RIVE_IMAGE_ASSET_HPP_
+
+#include "rive/generated/assets/image_asset_base.hpp"
+#include "rive/renderer.hpp"
+#include "rive/simple_array.hpp"
+#include <string>
+
+namespace rive
+{
+class ImageAsset : public ImageAssetBase
+{
+private:
+    rcp<RenderImage> m_RenderImage;
+
+public:
+    ImageAsset() {}
+    ~ImageAsset() override;
+
+#ifdef TESTING
+    std::size_t decodedByteSize = 0;
+#endif
+    bool decode(SimpleArray<uint8_t>&, Factory*) override;
+    std::string fileExtension() const override;
+    RenderImage* renderImage() const { return m_RenderImage.get(); }
+    void renderImage(rcp<RenderImage> renderImage);
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/audio/audio_engine.hpp b/include/rive/audio/audio_engine.hpp
new file mode 100644
index 0000000..e709ec7
--- /dev/null
+++ b/include/rive/audio/audio_engine.hpp
@@ -0,0 +1,96 @@
+#ifdef WITH_RIVE_AUDIO
+#ifndef _RIVE_AUDIO_ENGINE_HPP_
+#define _RIVE_AUDIO_ENGINE_HPP_
+
+#include "rive/refcnt.hpp"
+#include "rive/span.hpp"
+#include <vector>
+#include <stdio.h>
+#include <cstdint>
+#include <mutex>
+
+typedef struct ma_engine ma_engine;
+typedef struct ma_sound ma_sound;
+typedef struct ma_device ma_device;
+typedef struct ma_node_base ma_node_base;
+typedef struct ma_context ma_context;
+
+namespace rive
+{
+class AudioSound;
+class AudioSource;
+class LevelsNode;
+class Artboard;
+class AudioEngine : public RefCnt<AudioEngine>
+{
+    friend class AudioSound;
+    friend class AudioSource;
+    friend class LevelsNode;
+
+public:
+    static const uint32_t defaultNumChannels = 2;
+    static const uint32_t defaultSampleRate = 48000;
+
+    static rcp<AudioEngine> Make(uint32_t numChannels, uint32_t sampleRate);
+
+    ma_device* device() { return m_device; }
+    ma_engine* engine() { return m_engine; }
+
+    uint32_t channels() const;
+    uint32_t sampleRate() const;
+    uint64_t timeInFrames();
+
+    ~AudioEngine();
+    rcp<AudioSound> play(rcp<AudioSource> source,
+                         uint64_t startTime,
+                         uint64_t endTime,
+                         uint64_t soundStartTime,
+                         Artboard* artboard = nullptr);
+
+    static rcp<AudioEngine> RuntimeEngine(bool makeWhenNecessary = true);
+
+#ifdef EXTERNAL_RIVE_AUDIO_ENGINE
+    bool readAudioFrames(float* frames, uint64_t numFrames, uint64_t* framesRead = nullptr);
+    bool sumAudioFrames(float* frames, uint64_t numFrames);
+#endif
+
+#ifdef WITH_RIVE_AUDIO_TOOLS
+    void initLevelMonitor();
+    void levels(Span<float> levels);
+    float level(uint32_t channel);
+#endif
+
+    void start();
+    void stop();
+    void stop(Artboard* artboard);
+
+#ifdef TESTING
+    size_t playingSoundCount();
+#endif
+private:
+    AudioEngine(ma_engine* engine, ma_context* context);
+    ma_device* m_device;
+    ma_engine* m_engine;
+    ma_context* m_context;
+    std::mutex m_mutex;
+
+    void soundCompleted(rcp<AudioSound> sound);
+    void unlinkSound(rcp<AudioSound> sound);
+
+    std::vector<rcp<AudioSound>> m_completedSounds;
+    rcp<AudioSound> m_playingSoundsHead;
+    static void SoundCompleted(void* pUserData, ma_sound* pSound);
+
+#ifdef WITH_RIVE_AUDIO_TOOLS
+    void measureLevels(const float* frames, uint32_t frameCount);
+    std::vector<float> m_levels;
+    LevelsNode* m_levelMonitor = nullptr;
+#endif
+#ifdef EXTERNAL_RIVE_AUDIO_ENGINE
+    std::vector<float> m_readFrames;
+#endif
+};
+} // namespace rive
+
+#endif
+#endif
\ No newline at end of file
diff --git a/include/rive/audio/audio_format.hpp b/include/rive/audio/audio_format.hpp
new file mode 100644
index 0000000..330dffe
--- /dev/null
+++ b/include/rive/audio/audio_format.hpp
@@ -0,0 +1,15 @@
+#ifndef _RIVE_AUDIO_FORMAT_HPP_
+#define _RIVE_AUDIO_FORMAT_HPP_
+namespace rive
+{
+enum class AudioFormat : unsigned int
+{
+    unknown = 0,
+    wav,
+    flac,
+    mp3,
+    vorbis,
+    buffered
+};
+}
+#endif
\ No newline at end of file
diff --git a/include/rive/audio/audio_reader.hpp b/include/rive/audio/audio_reader.hpp
new file mode 100644
index 0000000..6243fd7
--- /dev/null
+++ b/include/rive/audio/audio_reader.hpp
@@ -0,0 +1,37 @@
+#ifdef WITH_RIVE_AUDIO
+#ifndef _RIVE_AUDIO_READER_HPP_
+#define _RIVE_AUDIO_READER_HPP_
+
+#include "miniaudio.h"
+#include "rive/refcnt.hpp"
+#include "rive/span.hpp"
+#include <vector>
+
+namespace rive
+{
+class AudioSource;
+class AudioReader : public RefCnt<AudioReader>
+{
+    friend class AudioSource;
+
+public:
+    uint64_t lengthInFrames();
+    Span<float> read(uint64_t frameCount);
+    ~AudioReader();
+    uint32_t channels() const;
+    uint32_t sampleRate() const;
+
+private:
+    AudioReader(rcp<AudioSource> audioSource, uint32_t channels);
+    ma_decoder* decoder();
+
+    rcp<AudioSource> m_source;
+    uint32_t m_channels;
+    ma_decoder m_decoder;
+    std::vector<float> m_readBuffer;
+};
+
+} // namespace rive
+
+#endif
+#endif
\ No newline at end of file
diff --git a/include/rive/audio/audio_sound.hpp b/include/rive/audio/audio_sound.hpp
new file mode 100644
index 0000000..96d63f3
--- /dev/null
+++ b/include/rive/audio/audio_sound.hpp
@@ -0,0 +1,133 @@
+#ifdef WITH_RIVE_AUDIO
+#ifndef _RIVE_AUDIO_SOUND_HPP_
+#define _RIVE_AUDIO_SOUND_HPP_
+
+#include "miniaudio.h"
+#include "rive/refcnt.hpp"
+#include "rive/audio/audio_source.hpp"
+
+namespace rive
+{
+class AudioEngine;
+class Artboard;
+
+struct ma_end_clipped_decoder
+{
+    ma_data_source_base base;
+    ma_decoder decoder;
+    ma_uint64 frameCursor;
+    ma_uint64 endFrame;
+};
+
+static ma_result ma_end_clipped_decoder_read(ma_data_source* pDataSource,
+                                             void* pFramesOut,
+                                             ma_uint64 frameCount,
+                                             ma_uint64* pFramesRead)
+{
+    ma_end_clipped_decoder* clipped = (ma_end_clipped_decoder*)pDataSource;
+
+    ma_result result =
+        ma_decoder_read_pcm_frames(&clipped->decoder, pFramesOut, frameCount, pFramesRead);
+
+    clipped->frameCursor += *pFramesRead;
+    if (clipped->frameCursor >= clipped->endFrame)
+    {
+        ma_uint64 overflow = clipped->frameCursor - clipped->endFrame;
+        if (*pFramesRead > overflow)
+        {
+            *pFramesRead -= overflow;
+        }
+        else
+        {
+            *pFramesRead = 0;
+            return MA_AT_END;
+        }
+    }
+
+    return result;
+}
+
+static ma_result ma_end_clipped_decoder_seek(ma_data_source* pDataSource, ma_uint64 frameIndex)
+{
+    ma_end_clipped_decoder* clipped = (ma_end_clipped_decoder*)pDataSource;
+    ma_result result = ma_decoder_seek_to_pcm_frame(&clipped->decoder, frameIndex);
+    if (result != MA_SUCCESS)
+    {
+        return result;
+    }
+
+    clipped->frameCursor = frameIndex;
+    return result;
+}
+
+static ma_result ma_end_clipped_decoder_get_data_format(ma_data_source* pDataSource,
+                                                        ma_format* pFormat,
+                                                        ma_uint32* pChannels,
+                                                        ma_uint32* pSampleRate,
+                                                        ma_channel* pChannelMap,
+                                                        size_t channelMapCap)
+{
+    ma_end_clipped_decoder* clipped = (ma_end_clipped_decoder*)pDataSource;
+    return ma_decoder_get_data_format(&clipped->decoder,
+                                      pFormat,
+                                      pChannels,
+                                      pSampleRate,
+                                      pChannelMap,
+                                      channelMapCap);
+}
+
+static ma_result ma_end_clipped_decoder_get_cursor(ma_data_source* pDataSource, ma_uint64* pCursor)
+{
+    ma_end_clipped_decoder* clipped = (ma_end_clipped_decoder*)pDataSource;
+    *pCursor = clipped->frameCursor;
+    return MA_SUCCESS;
+}
+
+static ma_result ma_end_clipped_decoder_get_length(ma_data_source* pDataSource, ma_uint64* pLength)
+{
+    ma_end_clipped_decoder* clipped = (ma_end_clipped_decoder*)pDataSource;
+    return ma_decoder_get_length_in_pcm_frames(&clipped->decoder, pLength);
+}
+
+static ma_data_source_vtable g_ma_end_clipped_decoder_vtable = {
+    ma_end_clipped_decoder_read,
+    ma_end_clipped_decoder_seek,
+    ma_end_clipped_decoder_get_data_format,
+    ma_end_clipped_decoder_get_cursor,
+    ma_end_clipped_decoder_get_length};
+
+class AudioSound : public RefCnt<AudioSound>
+{
+    friend class AudioEngine;
+
+public:
+    bool seek(uint64_t timeInFrames);
+    ~AudioSound();
+    void stop(uint64_t fadeTimeInFrames = 0);
+    float volume();
+    void volume(float value);
+    bool completed() const;
+
+private:
+    AudioSound(AudioEngine* engine, rcp<AudioSource> source, Artboard* artboard);
+    ma_end_clipped_decoder* clippedDecoder() { return &m_decoder; }
+    ma_audio_buffer* buffer() { return &m_buffer; }
+    ma_sound* sound() { return &m_sound; }
+    void dispose();
+
+    ma_end_clipped_decoder m_decoder;
+    ma_audio_buffer m_buffer;
+    ma_sound m_sound;
+    rcp<AudioSource> m_source;
+
+    // This is storage used by the AudioEngine.
+    bool m_isDisposed;
+    rcp<AudioSound> m_nextPlaying;
+    rcp<AudioSound> m_prevPlaying;
+    AudioEngine* m_engine;
+    Artboard* m_artboard;
+};
+} // namespace rive
+
+#endif
+#endif
\ No newline at end of file
diff --git a/include/rive/audio/audio_source.hpp b/include/rive/audio/audio_source.hpp
new file mode 100644
index 0000000..d6153a3
--- /dev/null
+++ b/include/rive/audio/audio_source.hpp
@@ -0,0 +1,65 @@
+#ifndef _RIVE_AUDIO_SOURCE_HPP_
+#define _RIVE_AUDIO_SOURCE_HPP_
+
+#include "rive/refcnt.hpp"
+#include "rive/span.hpp"
+#include "rive/simple_array.hpp"
+#include "rive/audio/audio_format.hpp"
+
+namespace rive
+{
+class AudioEngine;
+class AudioReader;
+class AudioSound;
+class AudioSource : public RefCnt<AudioSource>
+{
+public:
+    // Makes an audio source that does not own it's backing bytes.
+    AudioSource(rive::Span<uint8_t> fileBytes);
+
+    // Makes an audio source whose backing bytes will be deleted when the
+    // AudioSource deletes.
+    AudioSource(rive::SimpleArray<uint8_t> fileBytes);
+
+    // Makes a buffered audio source whose backing bytes will be deleted when
+    // the AudioSource deletes.
+    AudioSource(rive::Span<float> samples, uint32_t numChannels, uint32_t sampleRate);
+
+#ifdef WITH_RIVE_AUDIO
+    rcp<AudioReader> makeReader(uint32_t numChannels, uint32_t sampleRate);
+#endif
+
+    uint32_t channels();
+    uint32_t sampleRate();
+    AudioFormat format() const;
+    const rive::Span<uint8_t> bytes() const
+    {
+#ifdef WITH_RIVE_AUDIO
+        return m_fileBytes;
+#else
+        return rive::Span<uint8_t>(nullptr, 0);
+#endif
+    }
+
+    const rive::Span<float> bufferedSamples() const;
+    bool isBuffered() const
+    {
+#ifdef WITH_RIVE_AUDIO
+        return m_isBuffered;
+#else
+        return false;
+#endif
+    }
+
+private:
+#ifdef WITH_RIVE_AUDIO
+    bool m_isBuffered;
+    uint32_t m_channels;
+    uint32_t m_sampleRate;
+    rive::Span<uint8_t> m_fileBytes;
+    rive::SimpleArray<uint8_t> m_ownedBytes;
+#endif
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/audio_event.hpp b/include/rive/audio_event.hpp
new file mode 100644
index 0000000..2844e9c
--- /dev/null
+++ b/include/rive/audio_event.hpp
@@ -0,0 +1,25 @@
+#ifndef _RIVE_AUDIO_EVENT_HPP_
+#define _RIVE_AUDIO_EVENT_HPP_
+#include "rive/generated/audio_event_base.hpp"
+#include "rive/assets/file_asset_referencer.hpp"
+
+namespace rive
+{
+class AudioAsset;
+class AudioEvent : public AudioEventBase, public FileAssetReferencer
+{
+public:
+    StatusCode import(ImportStack& importStack) override;
+    void setAsset(FileAsset* asset) override;
+    uint32_t assetId() override;
+    void trigger(const CallbackData& value) override;
+    void play();
+
+#ifdef TESTING
+    AudioAsset* asset() const { return (AudioAsset*)m_fileAsset; }
+#endif
+    Core* clone() const override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/backboard.hpp b/include/rive/backboard.hpp
new file mode 100644
index 0000000..475011c
--- /dev/null
+++ b/include/rive/backboard.hpp
@@ -0,0 +1,10 @@
+#ifndef _RIVE_BACKBOARD_HPP_
+#define _RIVE_BACKBOARD_HPP_
+#include "rive/generated/backboard_base.hpp"
+namespace rive
+{
+class Backboard : public BackboardBase
+{};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/bones/bone.hpp b/include/rive/bones/bone.hpp
new file mode 100644
index 0000000..d5d2a16
--- /dev/null
+++ b/include/rive/bones/bone.hpp
@@ -0,0 +1,34 @@
+#ifndef _RIVE_BONE_HPP_
+#define _RIVE_BONE_HPP_
+#include "rive/generated/bones/bone_base.hpp"
+#include <stdio.h>
+#include <vector>
+
+namespace rive
+{
+class Constraint;
+class Bone : public BoneBase
+{
+
+private:
+    std::vector<Bone*> m_ChildBones;
+    std::vector<Constraint*> m_PeerConstraints;
+
+public:
+    StatusCode onAddedClean(CoreContext* context) override;
+    float x() const override;
+    float y() const override;
+
+    inline const std::vector<Bone*> childBones() { return m_ChildBones; }
+
+    void addChildBone(Bone* bone);
+    Vec2D tipWorldTranslation() const;
+    void addPeerConstraint(Constraint* peer);
+    const std::vector<Constraint*>& peerConstraints() const { return m_PeerConstraints; }
+
+private:
+    void lengthChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/bones/cubic_weight.hpp b/include/rive/bones/cubic_weight.hpp
new file mode 100644
index 0000000..23af090
--- /dev/null
+++ b/include/rive/bones/cubic_weight.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_CUBIC_WEIGHT_HPP_
+#define _RIVE_CUBIC_WEIGHT_HPP_
+#include "rive/generated/bones/cubic_weight_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class CubicWeight : public CubicWeightBase
+{
+private:
+    Vec2D m_InTranslation;
+    Vec2D m_OutTranslation;
+
+public:
+    Vec2D& inTranslation() { return m_InTranslation; }
+    Vec2D& outTranslation() { return m_OutTranslation; }
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/bones/root_bone.hpp b/include/rive/bones/root_bone.hpp
new file mode 100644
index 0000000..b6f4aeb
--- /dev/null
+++ b/include/rive/bones/root_bone.hpp
@@ -0,0 +1,18 @@
+#ifndef _RIVE_ROOT_BONE_HPP_
+#define _RIVE_ROOT_BONE_HPP_
+#include "rive/generated/bones/root_bone_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class RootBone : public RootBoneBase
+{
+public:
+    StatusCode onAddedClean(CoreContext* context) override;
+
+protected:
+    void xChanged() override;
+    void yChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/bones/skeletal_component.hpp b/include/rive/bones/skeletal_component.hpp
new file mode 100644
index 0000000..03b49ef
--- /dev/null
+++ b/include/rive/bones/skeletal_component.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_SKELETAL_COMPONENT_HPP_
+#define _RIVE_SKELETAL_COMPONENT_HPP_
+#include "rive/generated/bones/skeletal_component_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class SkeletalComponent : public SkeletalComponentBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/bones/skin.hpp b/include/rive/bones/skin.hpp
new file mode 100644
index 0000000..862b60e
--- /dev/null
+++ b/include/rive/bones/skin.hpp
@@ -0,0 +1,44 @@
+#ifndef _RIVE_SKIN_HPP_
+#define _RIVE_SKIN_HPP_
+#include "rive/generated/bones/skin_base.hpp"
+#include "rive/math/mat2d.hpp"
+#include "rive/span.hpp"
+#include <stdio.h>
+#include <vector>
+
+namespace rive
+{
+class Tendon;
+class Vertex;
+class Skinnable;
+
+class Skin : public SkinBase
+{
+    friend class Tendon;
+
+public:
+    ~Skin() override;
+
+private:
+    Mat2D m_WorldTransform;
+    std::vector<Tendon*> m_Tendons;
+    float* m_BoneTransforms = nullptr;
+    Skinnable* m_Skinnable;
+
+protected:
+    void addTendon(Tendon* tendon);
+
+public:
+    StatusCode onAddedDirty(CoreContext* context) override;
+    void buildDependencies() override;
+    void deform(Span<Vertex*> vertices);
+    void onDirty(ComponentDirt dirt) override;
+    void update(ComponentDirt value) override;
+
+#ifdef TESTING
+    std::vector<Tendon*>& tendons() { return m_Tendons; }
+#endif
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/bones/skinnable.hpp b/include/rive/bones/skinnable.hpp
new file mode 100644
index 0000000..345dbf9
--- /dev/null
+++ b/include/rive/bones/skinnable.hpp
@@ -0,0 +1,31 @@
+#ifndef _RIVE_SKINNABLE_HPP_
+#define _RIVE_SKINNABLE_HPP_
+
+#include "rive/rive_types.hpp"
+
+namespace rive
+{
+class Skin;
+class Component;
+
+class Skinnable
+{
+    friend class Skin;
+
+private:
+    Skin* m_Skin = nullptr;
+
+protected:
+    void skin(Skin* skin);
+
+public:
+    virtual ~Skinnable() {}
+
+    Skin* skin() const { return m_Skin; }
+    virtual void markSkinDirty() = 0;
+
+    static Skinnable* from(Component* component);
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/bones/tendon.hpp b/include/rive/bones/tendon.hpp
new file mode 100644
index 0000000..6a8f456
--- /dev/null
+++ b/include/rive/bones/tendon.hpp
@@ -0,0 +1,25 @@
+#ifndef _RIVE_TENDON_HPP_
+#define _RIVE_TENDON_HPP_
+
+#include "rive/generated/bones/tendon_base.hpp"
+#include "rive/math/mat2d.hpp"
+#include <stdio.h>
+
+namespace rive
+{
+class Bone;
+class Tendon : public TendonBase
+{
+private:
+    Mat2D m_InverseBind;
+    Bone* m_Bone = nullptr;
+
+public:
+    Bone* bone() const { return m_Bone; }
+    const Mat2D& inverseBind() const { return m_InverseBind; }
+    StatusCode onAddedDirty(CoreContext* context) override;
+    StatusCode onAddedClean(CoreContext* context) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/bones/weight.hpp b/include/rive/bones/weight.hpp
new file mode 100644
index 0000000..4b8658f
--- /dev/null
+++ b/include/rive/bones/weight.hpp
@@ -0,0 +1,27 @@
+#ifndef _RIVE_WEIGHT_HPP_
+#define _RIVE_WEIGHT_HPP_
+#include "rive/generated/bones/weight_base.hpp"
+#include "rive/math/vec2d.hpp"
+#include <stdio.h>
+
+namespace rive
+{
+class Weight : public WeightBase
+{
+private:
+    Vec2D m_Translation;
+
+public:
+    Vec2D& translation() { return m_Translation; }
+
+    StatusCode onAddedDirty(CoreContext* context) override;
+
+    static Vec2D deform(Vec2D inPoint,
+                        unsigned int indices,
+                        unsigned int weights,
+                        const Mat2D& world,
+                        const float* boneTransforms);
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/bounds_provider.hpp b/include/rive/bounds_provider.hpp
new file mode 100644
index 0000000..7f3ca83
--- /dev/null
+++ b/include/rive/bounds_provider.hpp
@@ -0,0 +1,17 @@
+#ifndef _RIVE_BOUNDS_PROVIDER_HPP_
+#define _RIVE_BOUNDS_PROVIDER_HPP_
+
+#include "rive/math/aabb.hpp"
+#include "rive/math/mat2d.hpp"
+
+namespace rive
+{
+
+class BoundsProvider
+{
+public:
+    virtual ~BoundsProvider() {}
+    virtual AABB computeBounds(Mat2D toParent);
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/clip_result.hpp b/include/rive/clip_result.hpp
new file mode 100644
index 0000000..7b9b7dc
--- /dev/null
+++ b/include/rive/clip_result.hpp
@@ -0,0 +1,12 @@
+#ifndef _RIVE_CLIP_RESULT_HPP_
+#define _RIVE_CLIP_RESULT_HPP_
+namespace rive
+{
+enum class ClipResult : unsigned char
+{
+    noClip = 0,
+    clip = 1,
+    emptyClip = 2
+};
+}
+#endif
\ No newline at end of file
diff --git a/include/rive/command_path.hpp b/include/rive/command_path.hpp
new file mode 100644
index 0000000..c0ae7b6
--- /dev/null
+++ b/include/rive/command_path.hpp
@@ -0,0 +1,44 @@
+#ifndef _RIVE_COMMAND_PATH_HPP_
+#define _RIVE_COMMAND_PATH_HPP_
+
+#include "rive/math/mat2d.hpp"
+#include "rive/math/path_types.hpp"
+#include "rive/refcnt.hpp"
+
+namespace rive
+{
+class RenderPath;
+
+/// Abstract path used to build up commands used for rendering.
+class CommandPath : public RefCnt<CommandPath>
+{
+public:
+    virtual ~CommandPath() {}
+    virtual void rewind() = 0;
+    virtual void fillRule(FillRule value) = 0;
+    virtual void addPath(CommandPath* path, const Mat2D& transform) = 0;
+
+    virtual void moveTo(float x, float y) = 0;
+    virtual void lineTo(float x, float y) = 0;
+    virtual void cubicTo(float ox, float oy, float ix, float iy, float x, float y) = 0;
+    virtual void close() = 0;
+
+    virtual RenderPath* renderPath() = 0;
+
+    // non-virtual helpers
+
+    void addRect(float x, float y, float width, float height)
+    {
+        moveTo(x, y);
+        lineTo(x + width, y);
+        lineTo(x + width, y + height);
+        lineTo(x, y + height);
+        close();
+    }
+
+    void move(Vec2D v) { this->moveTo(v.x, v.y); }
+    void line(Vec2D v) { this->lineTo(v.x, v.y); }
+    void cubic(Vec2D a, Vec2D b, Vec2D c) { this->cubicTo(a.x, a.y, b.x, b.y, c.x, c.y); }
+};
+} // namespace rive
+#endif
diff --git a/include/rive/component.hpp b/include/rive/component.hpp
new file mode 100644
index 0000000..7488dca
--- /dev/null
+++ b/include/rive/component.hpp
@@ -0,0 +1,63 @@
+#ifndef _RIVE_COMPONENT_HPP_
+#define _RIVE_COMPONENT_HPP_
+#include "rive/component_dirt.hpp"
+#include "rive/generated/component_base.hpp"
+#include "rive/dependency_helper.hpp"
+
+#include <vector>
+#include <functional>
+
+namespace rive
+{
+class ContainerComponent;
+class Artboard;
+
+class Component : public ComponentBase
+{
+    friend class Artboard;
+
+private:
+    ContainerComponent* m_Parent = nullptr;
+
+    unsigned int m_GraphOrder;
+    Artboard* m_Artboard = nullptr;
+
+protected:
+    ComponentDirt m_Dirt = ComponentDirt::Filthy;
+
+public:
+    DependencyHelper<Artboard, Component> m_DependencyHelper;
+    virtual bool collapse(bool value);
+    inline Artboard* artboard() const { return m_Artboard; }
+    StatusCode onAddedDirty(CoreContext* context) override;
+    inline ContainerComponent* parent() const { return m_Parent; }
+    const std::vector<Component*>& dependents() const { return m_DependencyHelper.dependents(); }
+
+    void addDependent(Component* component);
+
+    // TODO: re-evaluate when more of the lib is complete...
+    // These could be pure virtual but we define them empty here to avoid
+    // having to implement them in a bunch of concrete classes that
+    // currently don't use this logic.
+    virtual void buildDependencies() {}
+    virtual void onDirty(ComponentDirt dirt) {}
+    virtual void update(ComponentDirt value) {}
+
+    unsigned int graphOrder() const { return m_GraphOrder; }
+    bool addDirt(ComponentDirt value, bool recurse = false);
+    inline bool hasDirt(ComponentDirt flag) const { return (m_Dirt & flag) == flag; }
+    static inline bool hasDirt(ComponentDirt value, ComponentDirt flag)
+    {
+        return (value & flag) != ComponentDirt::None;
+    }
+
+    StatusCode import(ImportStack& importStack) override;
+
+    bool isCollapsed() const
+    {
+        return (m_Dirt & ComponentDirt::Collapsed) == ComponentDirt::Collapsed;
+    }
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/component_dirt.hpp b/include/rive/component_dirt.hpp
new file mode 100644
index 0000000..ccd64c0
--- /dev/null
+++ b/include/rive/component_dirt.hpp
@@ -0,0 +1,73 @@
+#ifndef _RIVE_DIRTY_FLAGS_HPP_
+#define _RIVE_DIRTY_FLAGS_HPP_
+
+#include "rive/enum_bitset.hpp"
+
+namespace rive
+{
+enum class ComponentDirt : unsigned short
+{
+    None = 0,
+
+    /// Used to mark that a component (and subsequently it's children) do not
+    /// update with the Artboard update cycle and that any drawable should also
+    /// be hidden. Used by Solos.
+    Collapsed = 1 << 0,
+
+    Dependents = 1 << 1,
+
+    /// General flag for components are dirty (if this is up, the update
+    /// cycle runs). It gets automatically applied with any other dirt.
+    Components = 1 << 2,
+
+    /// Draw order needs to be re-computed.
+    DrawOrder = 1 << 3,
+
+    /// Path is dirty and needs to be rebuilt.
+    Path = 1 << 4,
+
+    /// TextShape is dirty and needs to be rebuilt.
+    TextShape = 1 << 4,
+
+    /// Skin needs to recompute bone transformations.
+    Skin = 1 << 4,
+
+    /// Vertices have changed, re-order cached lists.
+    Vertices = 1 << 5,
+
+    /// Text modifier coverage is dirty and needs to be rebuilt.
+    TextCoverage = 1 << 5,
+
+    /// Used by any component that needs to recompute their local transform.
+    /// Usually components that have their transform dirty will also have
+    /// their worldTransform dirty.
+    Transform = 1 << 6,
+
+    /// Used by any component that needs to update its world transform.
+    WorldTransform = 1 << 7,
+
+    /// Marked when the stored render opacity needs to be updated.
+    RenderOpacity = 1 << 8,
+
+    /// Dirt used to mark some stored paint needs to be rebuilt or that we
+    /// just want to trigger an update cycle so painting occurs.
+    Paint = 1 << 9,
+
+    /// Used by the gradients track when the stops need to be re-ordered.
+    Stops = 1 << 10,
+
+    /// Used by data binds to track  the value has changed.
+    Bindings = 1 << 11,
+
+    /// Blend modes need to be updated
+    // TODO: do we need this?
+    // BlendMode = 1 << 9,
+
+    LayoutStyle = 1 << 11,
+
+    /// All dirty. Every flag (apart from Collapsed) is set.
+    Filthy = 0xFFFE
+};
+RIVE_MAKE_ENUM_BITSET(ComponentDirt)
+} // namespace rive
+#endif
diff --git a/include/rive/constraints/constraint.hpp b/include/rive/constraints/constraint.hpp
new file mode 100644
index 0000000..bb29fb2
--- /dev/null
+++ b/include/rive/constraints/constraint.hpp
@@ -0,0 +1,24 @@
+#ifndef _RIVE_CONSTRAINT_HPP_
+#define _RIVE_CONSTRAINT_HPP_
+#include "rive/generated/constraints/constraint_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TransformComponent;
+class Mat2D;
+
+class Constraint : public ConstraintBase
+{
+public:
+    void strengthChanged() override;
+    StatusCode onAddedClean(CoreContext* context) override;
+    virtual void markConstraintDirty();
+    virtual void constrain(TransformComponent* component) = 0;
+    void buildDependencies() override;
+    void onDirty(ComponentDirt dirt) override;
+};
+
+const Mat2D& getParentWorld(const TransformComponent& component);
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/constraints/distance_constraint.hpp b/include/rive/constraints/distance_constraint.hpp
new file mode 100644
index 0000000..5af1a70
--- /dev/null
+++ b/include/rive/constraints/distance_constraint.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_DISTANCE_CONSTRAINT_HPP_
+#define _RIVE_DISTANCE_CONSTRAINT_HPP_
+#include "rive/generated/constraints/distance_constraint_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class DistanceConstraint : public DistanceConstraintBase
+{
+public:
+    void constrain(TransformComponent* component) override;
+    void distanceChanged() override;
+    void modeValueChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/constraints/follow_path_constraint.hpp b/include/rive/constraints/follow_path_constraint.hpp
new file mode 100644
index 0000000..a59b0f6
--- /dev/null
+++ b/include/rive/constraints/follow_path_constraint.hpp
@@ -0,0 +1,27 @@
+#ifndef _RIVE_FOLLOW_PATH_CONSTRAINT_HPP_
+#define _RIVE_FOLLOW_PATH_CONSTRAINT_HPP_
+#include "rive/generated/constraints/follow_path_constraint_base.hpp"
+#include "rive/math/transform_components.hpp"
+#include "rive/math/contour_measure.hpp"
+namespace rive
+{
+class FollowPathConstraint : public FollowPathConstraintBase
+{
+public:
+    void distanceChanged() override;
+    void orientChanged() override;
+    StatusCode onAddedClean(CoreContext* context) override;
+    const Mat2D targetTransform() const;
+    void constrain(TransformComponent* component) override;
+    void update(ComponentDirt value) override;
+    void buildDependencies() override;
+
+private:
+    RawPath m_rawPath;
+    std::vector<rcp<ContourMeasure>> m_contours;
+    TransformComponents m_ComponentsA;
+    TransformComponents m_ComponentsB;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/constraints/ik_constraint.hpp b/include/rive/constraints/ik_constraint.hpp
new file mode 100644
index 0000000..80a9892
--- /dev/null
+++ b/include/rive/constraints/ik_constraint.hpp
@@ -0,0 +1,35 @@
+#ifndef _RIVE_I_KCONSTRAINT_HPP_
+#define _RIVE_I_KCONSTRAINT_HPP_
+#include "rive/generated/constraints/ik_constraint_base.hpp"
+#include "rive/math/mat2d.hpp"
+#include "rive/math/transform_components.hpp"
+#include <vector>
+
+namespace rive
+{
+class Bone;
+class IKConstraint : public IKConstraintBase
+{
+private:
+    struct BoneChainLink
+    {
+        int index;
+        Bone* bone;
+        float angle;
+        TransformComponents transformComponents;
+        Mat2D parentWorldInverse;
+    };
+    std::vector<BoneChainLink> m_FkChain;
+    void solve1(BoneChainLink* fk1, const Vec2D& worldTargetTranslation);
+    void solve2(BoneChainLink* fk1, BoneChainLink* fk2, const Vec2D& worldTargetTranslation);
+    void constrainRotation(BoneChainLink& fk, float rotation);
+
+public:
+    void markConstraintDirty() override;
+    StatusCode onAddedClean(CoreContext* context) override;
+    void constrain(TransformComponent* component) override;
+    void buildDependencies() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/constraints/rotation_constraint.hpp b/include/rive/constraints/rotation_constraint.hpp
new file mode 100644
index 0000000..596ade3
--- /dev/null
+++ b/include/rive/constraints/rotation_constraint.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_ROTATION_CONSTRAINT_HPP_
+#define _RIVE_ROTATION_CONSTRAINT_HPP_
+#include "rive/generated/constraints/rotation_constraint_base.hpp"
+#include "rive/math/transform_components.hpp"
+#include <stdio.h>
+namespace rive
+{
+class RotationConstraint : public RotationConstraintBase
+{
+private:
+    TransformComponents m_ComponentsA;
+    TransformComponents m_ComponentsB;
+
+public:
+    void constrain(TransformComponent* component) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/constraints/scale_constraint.hpp b/include/rive/constraints/scale_constraint.hpp
new file mode 100644
index 0000000..10d5030
--- /dev/null
+++ b/include/rive/constraints/scale_constraint.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_SCALE_CONSTRAINT_HPP_
+#define _RIVE_SCALE_CONSTRAINT_HPP_
+#include "rive/generated/constraints/scale_constraint_base.hpp"
+#include "rive/math/transform_components.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ScaleConstraint : public ScaleConstraintBase
+{
+private:
+    TransformComponents m_ComponentsA;
+    TransformComponents m_ComponentsB;
+
+public:
+    void constrain(TransformComponent* component) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/constraints/targeted_constraint.hpp b/include/rive/constraints/targeted_constraint.hpp
new file mode 100644
index 0000000..7d08846
--- /dev/null
+++ b/include/rive/constraints/targeted_constraint.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_TARGETED_CONSTRAINT_HPP_
+#define _RIVE_TARGETED_CONSTRAINT_HPP_
+#include "rive/generated/constraints/targeted_constraint_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TransformComponent;
+class TargetedConstraint : public TargetedConstraintBase
+{
+protected:
+    TransformComponent* m_Target = nullptr;
+
+public:
+    void buildDependencies() override;
+    StatusCode onAddedDirty(CoreContext* context) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/constraints/transform_component_constraint.hpp b/include/rive/constraints/transform_component_constraint.hpp
new file mode 100644
index 0000000..c890ca1
--- /dev/null
+++ b/include/rive/constraints/transform_component_constraint.hpp
@@ -0,0 +1,15 @@
+#ifndef _RIVE_TRANSFORM_COMPONENT_CONSTRAINT_HPP_
+#define _RIVE_TRANSFORM_COMPONENT_CONSTRAINT_HPP_
+#include "rive/generated/constraints/transform_component_constraint_base.hpp"
+#include "rive/transform_space.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TransformComponentConstraint : public TransformComponentConstraintBase
+{
+public:
+    TransformSpace minMaxSpace() const { return (TransformSpace)minMaxSpaceValue(); }
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/constraints/transform_component_constraint_y.hpp b/include/rive/constraints/transform_component_constraint_y.hpp
new file mode 100644
index 0000000..05aaf3d
--- /dev/null
+++ b/include/rive/constraints/transform_component_constraint_y.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_TRANSFORM_COMPONENT_CONSTRAINT_Y_HPP_
+#define _RIVE_TRANSFORM_COMPONENT_CONSTRAINT_Y_HPP_
+#include "rive/generated/constraints/transform_component_constraint_y_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TransformComponentConstraintY : public TransformComponentConstraintYBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/constraints/transform_constraint.hpp b/include/rive/constraints/transform_constraint.hpp
new file mode 100644
index 0000000..7aae972
--- /dev/null
+++ b/include/rive/constraints/transform_constraint.hpp
@@ -0,0 +1,21 @@
+#ifndef _RIVE_TRANSFORM_CONSTRAINT_HPP_
+#define _RIVE_TRANSFORM_CONSTRAINT_HPP_
+#include "rive/generated/constraints/transform_constraint_base.hpp"
+#include "rive/math/transform_components.hpp"
+
+#include <stdio.h>
+namespace rive
+{
+class TransformConstraint : public TransformConstraintBase
+{
+private:
+    TransformComponents m_ComponentsA;
+    TransformComponents m_ComponentsB;
+
+public:
+    virtual const Mat2D targetTransform() const;
+    void constrain(TransformComponent* component) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/constraints/transform_space_constraint.hpp b/include/rive/constraints/transform_space_constraint.hpp
new file mode 100644
index 0000000..fe7be1a
--- /dev/null
+++ b/include/rive/constraints/transform_space_constraint.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_TRANSFORM_SPACE_CONSTRAINT_HPP_
+#define _RIVE_TRANSFORM_SPACE_CONSTRAINT_HPP_
+#include "rive/generated/constraints/transform_space_constraint_base.hpp"
+#include "rive/transform_space.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TransformSpaceConstraint : public TransformSpaceConstraintBase
+{
+public:
+    TransformSpace sourceSpace() const { return (TransformSpace)sourceSpaceValue(); }
+    TransformSpace destSpace() const { return (TransformSpace)destSpaceValue(); }
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/constraints/translation_constraint.hpp b/include/rive/constraints/translation_constraint.hpp
new file mode 100644
index 0000000..08c2cf3
--- /dev/null
+++ b/include/rive/constraints/translation_constraint.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_TRANSLATION_CONSTRAINT_HPP_
+#define _RIVE_TRANSLATION_CONSTRAINT_HPP_
+#include "rive/generated/constraints/translation_constraint_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TranslationConstraint : public TranslationConstraintBase
+{
+public:
+    void constrain(TransformComponent* component) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/container_component.hpp b/include/rive/container_component.hpp
new file mode 100644
index 0000000..04cf246
--- /dev/null
+++ b/include/rive/container_component.hpp
@@ -0,0 +1,26 @@
+#ifndef _RIVE_CONTAINER_COMPONENT_HPP_
+#define _RIVE_CONTAINER_COMPONENT_HPP_
+#include "rive/generated/container_component_base.hpp"
+#include <vector>
+#include <functional>
+
+namespace rive
+{
+class ContainerComponent : public ContainerComponentBase
+{
+public:
+    const std::vector<Component*>& children() const { return m_children; }
+    virtual void addChild(Component* component);
+    bool collapse(bool value) override;
+
+    // Returns true if it searched through all of its children. predicate can
+    // return false to stop searching.
+    bool forAll(std::function<bool(Component*)> predicate);
+    bool forEachChild(std::function<bool(Component*)> predicate);
+
+private:
+    std::vector<Component*> m_children;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/core.hpp b/include/rive/core.hpp
new file mode 100644
index 0000000..8a94797
--- /dev/null
+++ b/include/rive/core.hpp
@@ -0,0 +1,56 @@
+#ifndef _RIVE_CORE_HPP_
+#define _RIVE_CORE_HPP_
+
+#include "rive/rive_types.hpp"
+#include "rive/core/binary_reader.hpp"
+#include "rive/status_code.hpp"
+
+namespace rive
+{
+class CoreContext;
+class ImportStack;
+class Core
+{
+public:
+    Core() = default;
+    Core(const Core&) = default;
+    const uint32_t emptyId = -1;
+    static const int invalidPropertyKey = 0;
+    virtual ~Core() {}
+    virtual uint16_t coreType() const = 0;
+    virtual bool isTypeOf(uint16_t typeKey) const = 0;
+    virtual bool deserialize(uint16_t propertyKey, BinaryReader& reader) = 0;
+
+    template <typename T> inline bool is() const { return isTypeOf(T::typeKey); }
+    template <typename T> inline T* as()
+    {
+        assert(is<T>());
+        return static_cast<T*>(this);
+    }
+
+    /// Make a shallow copy of the object.
+    virtual Core* clone() const { return nullptr; }
+
+    template <typename T> inline const T* as() const
+    {
+        assert(is<T>());
+        return static_cast<const T*>(this);
+    }
+
+    /// Called when the object is first added to the context, other objects
+    /// may not have resolved their dependencies yet. This is an opportunity
+    /// to look up objects referenced by id, but not assume that they in
+    /// turn have resolved their references yet. Called during
+    /// load/instance.
+    virtual StatusCode onAddedDirty(CoreContext* context) { return StatusCode::Ok; }
+
+    /// Called when all the objects in the context have had onAddedDirty
+    /// called. This is an opportunity to reference things referenced by
+    /// dependencies. (A path should be able to find a Shape somewhere in
+    /// its hierarchy, which may be multiple levels up).
+    virtual StatusCode onAddedClean(CoreContext* context) { return StatusCode::Ok; }
+
+    virtual StatusCode import(ImportStack& importStack) { return StatusCode::Ok; }
+};
+} // namespace rive
+#endif
diff --git a/include/rive/core/binary_data_reader.hpp b/include/rive/core/binary_data_reader.hpp
new file mode 100644
index 0000000..beae449
--- /dev/null
+++ b/include/rive/core/binary_data_reader.hpp
@@ -0,0 +1,38 @@
+#ifndef _RIVE_CORE_BINARY_DATA_READER_HPP_
+#define _RIVE_CORE_BINARY_DATA_READER_HPP_
+
+#include <string>
+#include <stdint.h>
+
+namespace rive
+{
+class BinaryDataReader
+{
+private:
+    uint8_t* m_Position;
+    uint8_t* m_End;
+    bool m_Overflowed;
+    size_t m_Length;
+
+    void overflow();
+
+public:
+    BinaryDataReader(uint8_t* bytes, size_t length);
+    bool didOverflow() const;
+    bool isEOF() const { return m_Position >= m_End; }
+    const uint8_t* position() const { return m_Position; }
+
+    size_t lengthInBytes() const;
+
+    uint64_t readVarUint();
+    uint32_t readVarUint32();
+    double readFloat64();
+    float readFloat32();
+    uint8_t readByte();
+    uint32_t readUint32();
+    void complete(uint8_t* bytes, size_t length);
+    void reset(uint8_t* bytes);
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/core/binary_reader.hpp b/include/rive/core/binary_reader.hpp
new file mode 100644
index 0000000..00687da
--- /dev/null
+++ b/include/rive/core/binary_reader.hpp
@@ -0,0 +1,56 @@
+
+#ifndef _RIVE_CORE_BINARY_READER_HPP_
+#define _RIVE_CORE_BINARY_READER_HPP_
+
+#include <string>
+#include <vector>
+#include "rive/span.hpp"
+#include "rive/core/type_conversions.hpp"
+
+namespace rive
+{
+class BinaryReader
+{
+private:
+    Span<const uint8_t> m_Bytes;
+    const uint8_t* m_Position;
+    bool m_Overflowed;
+    bool m_IntRangeError;
+
+    void overflow();
+    void intRangeError();
+
+public:
+    explicit BinaryReader(Span<const uint8_t>);
+    bool didOverflow() const;
+    bool didIntRangeError() const;
+    bool hasError() const { return m_Overflowed || m_IntRangeError; }
+    bool reachedEnd() const;
+
+    size_t lengthInBytes() const;
+    const uint8_t* position() const;
+
+    std::string readString();
+    Span<const uint8_t> readBytes();
+    float readFloat32();
+    uint8_t readByte();
+    uint32_t readUint32();
+    uint64_t readVarUint64(); // Reads a LEB128 encoded uint64_t
+
+    // This will cast the uint read to the requested size, but if the
+    // raw value was out-of-range, instead returns 0 and sets the IntRangeError.
+    template <typename T> T readVarUintAs()
+    {
+        auto value = this->readVarUint64();
+        if (!fitsIn<T>(value))
+        {
+            value = 0;
+            this->intRangeError();
+        }
+        return static_cast<T>(value);
+    }
+    void reset();
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/core/binary_stream.hpp b/include/rive/core/binary_stream.hpp
new file mode 100644
index 0000000..f582ccf
--- /dev/null
+++ b/include/rive/core/binary_stream.hpp
@@ -0,0 +1,20 @@
+#ifndef _RIVE_CORE_BINARY_STREAM_HPP_
+#define _RIVE_CORE_BINARY_STREAM_HPP_
+
+#include <cstdint>
+#include <cstddef>
+
+namespace rive
+{
+// Used to write binary chunks to an underlying stream, makes no assumptions
+// regarding storage/streaming it can flush the contents as it needs.
+class BinaryStream
+{
+public:
+    virtual void write(const uint8_t* bytes, std::size_t length) = 0;
+    virtual void flush() = 0;
+    virtual void clear() = 0;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/core/binary_writer.hpp b/include/rive/core/binary_writer.hpp
new file mode 100644
index 0000000..8d87cee
--- /dev/null
+++ b/include/rive/core/binary_writer.hpp
@@ -0,0 +1,32 @@
+#ifndef _RIVE_CORE_BINARY_WRITER_HPP_
+#define _RIVE_CORE_BINARY_WRITER_HPP_
+
+#include <cstddef>
+#include <cstdint>
+
+namespace rive
+{
+class BinaryStream;
+class BinaryWriter
+{
+private:
+    BinaryStream* m_Stream;
+
+public:
+    BinaryWriter(BinaryStream* stream);
+    ~BinaryWriter();
+    void write(float value);
+    void writeFloat(float value);
+    void write(double value);
+    void writeVarUint(uint64_t value);
+    void writeVarUint(uint32_t value);
+    void write(const uint8_t* bytes, std::size_t length);
+    void write(uint8_t value);
+    void writeDouble(double value);
+    void write(uint16_t value);
+    void write(uint32_t value);
+    void clear();
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/core/field_types/core_bool_type.hpp b/include/rive/core/field_types/core_bool_type.hpp
new file mode 100644
index 0000000..c6b0fcc
--- /dev/null
+++ b/include/rive/core/field_types/core_bool_type.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_CORE_BOOL_TYPE_HPP_
+#define _RIVE_CORE_BOOL_TYPE_HPP_
+
+namespace rive
+{
+class BinaryReader;
+class CoreBoolType
+{
+public:
+    static const int id = 0;
+    static bool deserialize(BinaryReader& reader);
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/core/field_types/core_bytes_type.hpp b/include/rive/core/field_types/core_bytes_type.hpp
new file mode 100644
index 0000000..90f1886
--- /dev/null
+++ b/include/rive/core/field_types/core_bytes_type.hpp
@@ -0,0 +1,17 @@
+#ifndef _RIVE_CORE_BYTES_TYPE_HPP_
+#define _RIVE_CORE_BYTES_TYPE_HPP_
+
+#include "rive/span.hpp"
+#include <cstdint>
+
+namespace rive
+{
+class BinaryReader;
+class CoreBytesType
+{
+public:
+    static const int id = 1;
+    static Span<const uint8_t> deserialize(BinaryReader& reader);
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/core/field_types/core_callback_type.hpp b/include/rive/core/field_types/core_callback_type.hpp
new file mode 100644
index 0000000..a6bb93f
--- /dev/null
+++ b/include/rive/core/field_types/core_callback_type.hpp
@@ -0,0 +1,29 @@
+#ifndef _RIVE_CORE_CALLBACK_TYPE_HPP_
+#define _RIVE_CORE_CALLBACK_TYPE_HPP_
+
+namespace rive
+{
+class Event;
+class CallbackContext
+{
+public:
+    virtual ~CallbackContext() {}
+    virtual void reportEvent(Event* event, float secondsDelay = 0.0f) {}
+    virtual bool playsAudio() { return false; }
+};
+
+class CallbackData
+{
+public:
+    CallbackContext* context() const { return m_context; }
+    float delaySeconds() const { return m_delaySeconds; }
+    CallbackData(CallbackContext* context, float delaySeconds) :
+        m_context(context), m_delaySeconds(delaySeconds)
+    {}
+
+private:
+    CallbackContext* m_context;
+    float m_delaySeconds;
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/core/field_types/core_color_type.hpp b/include/rive/core/field_types/core_color_type.hpp
new file mode 100644
index 0000000..afacba9
--- /dev/null
+++ b/include/rive/core/field_types/core_color_type.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_CORE_COLOR_TYPE_HPP_
+#define _RIVE_CORE_COLOR_TYPE_HPP_
+
+namespace rive
+{
+class BinaryReader;
+class CoreColorType
+{
+public:
+    static const int id = 3;
+    static int deserialize(BinaryReader& reader);
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/core/field_types/core_double_type.hpp b/include/rive/core/field_types/core_double_type.hpp
new file mode 100644
index 0000000..6351572
--- /dev/null
+++ b/include/rive/core/field_types/core_double_type.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_CORE_DOUBLE_TYPE_HPP_
+#define _RIVE_CORE_DOUBLE_TYPE_HPP_
+
+namespace rive
+{
+class BinaryReader;
+class CoreDoubleType
+{
+public:
+    static const int id = 2;
+    static float deserialize(BinaryReader& reader);
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/core/field_types/core_string_type.hpp b/include/rive/core/field_types/core_string_type.hpp
new file mode 100644
index 0000000..5962545
--- /dev/null
+++ b/include/rive/core/field_types/core_string_type.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_CORE_STRING_TYPE_HPP_
+#define _RIVE_CORE_STRING_TYPE_HPP_
+
+#include <string>
+
+namespace rive
+{
+class BinaryReader;
+class CoreStringType
+{
+public:
+    static const int id = 1;
+    static std::string deserialize(BinaryReader& reader);
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/core/field_types/core_uint_type.hpp b/include/rive/core/field_types/core_uint_type.hpp
new file mode 100644
index 0000000..af9c399
--- /dev/null
+++ b/include/rive/core/field_types/core_uint_type.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_CORE_UINT_TYPE_HPP_
+#define _RIVE_CORE_UINT_TYPE_HPP_
+
+namespace rive
+{
+class BinaryReader;
+class CoreUintType
+{
+public:
+    static const int id = 0;
+    static unsigned int deserialize(BinaryReader& reader);
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/core/reader.h b/include/rive/core/reader.h
new file mode 100644
index 0000000..45c8ffd
--- /dev/null
+++ b/include/rive/core/reader.h
@@ -0,0 +1,157 @@
+#include <stdlib.h>
+#include <string.h>
+
+static bool is_big_endian(void)
+{
+    union
+    {
+        uint32_t i;
+        char c[4];
+    } bint = {0x01020304};
+
+    return bint.c[0] == 1;
+}
+
+/* Decode an unsigned int LEB128 at buf into r, returning the nr of bytes read.
+ */
+inline size_t decode_uint_leb(const uint8_t* buf, const uint8_t* buf_end, uint64_t* r)
+{
+    const uint8_t* p = buf;
+    uint8_t shift = 0;
+    uint64_t result = 0;
+    uint8_t byte;
+
+    do
+    {
+        if (p >= buf_end)
+        {
+            return 0;
+        }
+        byte = *p++;
+        result |= ((uint64_t)(byte & 0x7f)) << shift;
+        shift += 7;
+    } while ((byte & 0x80) != 0);
+    *r = result;
+    return p - buf;
+}
+
+/* Decode an unsigned int LEB128 at buf into r, returning the nr of bytes read.
+ */
+inline size_t decode_uint_leb32(const uint8_t* buf, const uint8_t* buf_end, uint32_t* r)
+{
+    const uint8_t* p = buf;
+    uint8_t shift = 0;
+    uint32_t result = 0;
+    uint8_t byte;
+
+    do
+    {
+        if (p >= buf_end)
+        {
+            return 0;
+        }
+        byte = *p++;
+        result |= ((uint32_t)(byte & 0x7f)) << shift;
+        shift += 7;
+    } while ((byte & 0x80) != 0);
+    *r = result;
+    return p - buf;
+}
+
+/* Decodes a string
+ */
+inline uint64_t decode_string(uint64_t str_len,
+                              const uint8_t* buf,
+                              const uint8_t* buf_end,
+                              char* char_buf)
+{
+    // Return zero bytes read on buffer overflow
+    if (buf_end < buf + str_len)
+    {
+        return 0;
+    }
+    const uint8_t* p = buf;
+    for (int i = 0; i < str_len; i++)
+    {
+        char_buf[i] = *p++;
+    }
+    // Add the null terminator
+    char_buf[str_len] = '\0';
+    return str_len;
+}
+
+/* Decodes a double (8 bytes)
+ */
+inline size_t decode_double(const uint8_t* buf, const uint8_t* buf_end, double* r)
+{
+    // Return zero bytes read on buffer overflow
+    if (buf_end - buf < sizeof(double))
+    {
+        return 0;
+    }
+    if (is_big_endian())
+    {
+        uint8_t inverted[8] = {buf[7], buf[6], buf[5], buf[4], buf[3], buf[2], buf[1], buf[0]};
+        memcpy(r, inverted, sizeof(double));
+    }
+    else
+    {
+        memcpy(r, buf, sizeof(double));
+    }
+    return sizeof(double);
+}
+
+/* Decodes a float (4 bytes)
+ */
+inline size_t decode_float(const uint8_t* buf, const uint8_t* buf_end, float* r)
+{
+    // Return zero bytes read on buffer overflow
+    if (buf_end - buf < (unsigned)sizeof(float))
+    {
+        return 0;
+    }
+    if (is_big_endian())
+    {
+        uint8_t inverted[4] = {buf[3], buf[2], buf[1], buf[0]};
+        memcpy(r, inverted, sizeof(float));
+    }
+    else
+    {
+        memcpy(r, buf, sizeof(float));
+    }
+    return sizeof(float);
+}
+
+/* Decodes a single byte
+ */
+inline size_t decode_uint_8(const uint8_t* buf, const uint8_t* buf_end, uint8_t* r)
+{
+    // Return zero bytes read on buffer overflow
+    if (buf_end - buf < (unsigned)sizeof(uint8_t))
+    {
+        return 0;
+    }
+    memcpy(r, buf, sizeof(uint8_t));
+    return sizeof(uint8_t);
+}
+
+/* Decodes a 32 bit unsigned integer.
+ */
+inline size_t decode_uint_32(const uint8_t* buf, const uint8_t* buf_end, uint32_t* r)
+{
+    // Return zero bytes read on buffer overflow
+    if (buf_end - buf < (unsigned)sizeof(uint32_t))
+    {
+        return 0;
+    }
+    if (is_big_endian())
+    {
+        uint8_t inverted[4] = {buf[3], buf[2], buf[1], buf[0]};
+        memcpy(r, inverted, sizeof(uint32_t));
+    }
+    else
+    {
+        memcpy(r, buf, sizeof(uint32_t));
+    }
+    return sizeof(uint32_t);
+}
\ No newline at end of file
diff --git a/include/rive/core/type_conversions.hpp b/include/rive/core/type_conversions.hpp
new file mode 100644
index 0000000..406d9fc
--- /dev/null
+++ b/include/rive/core/type_conversions.hpp
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_TYPE_CONVERSIONS_HPP_
+#define _RIVE_TYPE_CONVERSIONS_HPP_
+
+#include "rive/rive_types.hpp"
+#include <limits>
+
+namespace rive
+{
+
+template <typename T> bool fitsIn(intmax_t x)
+{
+    return x >= std::numeric_limits<T>::min() && x <= std::numeric_limits<T>::max();
+}
+
+template <typename T> T castTo(intmax_t x)
+{
+    assert(sizeof(T) <= 4); // don't cast to 64bit types
+    assert(fitsIn<T>(x));
+    return static_cast<T>(x);
+}
+
+} // namespace rive
+
+#endif
diff --git a/include/rive/core/vector_binary_writer.hpp b/include/rive/core/vector_binary_writer.hpp
new file mode 100644
index 0000000..34ba649
--- /dev/null
+++ b/include/rive/core/vector_binary_writer.hpp
@@ -0,0 +1,43 @@
+#ifndef _RIVE_CORE_VECTOR_BINARY_WRITER_HPP_
+#define _RIVE_CORE_VECTOR_BINARY_WRITER_HPP_
+
+#include "rive/core/binary_stream.hpp"
+#include "rive/core/binary_writer.hpp"
+#include <cstring>
+
+namespace rive
+{
+class VectorBinaryWriter : public BinaryStream, public BinaryWriter
+{
+private:
+    std::vector<uint8_t>* m_WriteBuffer;
+    std::size_t m_Start;
+    size_t m_pos = 0;
+
+public:
+    VectorBinaryWriter(std::vector<uint8_t>* buffer) :
+        BinaryWriter(this), m_WriteBuffer(buffer), m_Start(m_WriteBuffer->size())
+    {}
+
+    uint8_t* buffer() const { return &(*m_WriteBuffer)[m_Start]; }
+    std::size_t bufferSize() const { return m_WriteBuffer->size() - m_Start; }
+
+    std::size_t start() const { return m_Start; }
+    size_t size() const { return m_pos; }
+
+    using BinaryWriter::write;
+    void write(const uint8_t* bytes, std::size_t length) override
+    {
+        auto end = m_pos;
+        if (m_WriteBuffer->size() < end + length)
+        {
+            m_WriteBuffer->resize(end + length);
+        }
+        std::memcpy(&((*m_WriteBuffer)[end]), bytes, length);
+        m_pos += length;
+    }
+    void flush() override {}
+    void clear() override { m_pos = 0; }
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/core_context.hpp b/include/rive/core_context.hpp
new file mode 100644
index 0000000..655c751
--- /dev/null
+++ b/include/rive/core_context.hpp
@@ -0,0 +1,17 @@
+#ifndef _RIVE_CORE_CONTEXT_HPP_
+#define _RIVE_CORE_CONTEXT_HPP_
+
+#include "rive/rive_types.hpp"
+
+namespace rive
+{
+class Artboard;
+class Core;
+class CoreContext
+{
+public:
+    virtual ~CoreContext() {}
+    virtual Core* resolve(uint32_t id) const = 0;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/custom_property.hpp b/include/rive/custom_property.hpp
new file mode 100644
index 0000000..2863962
--- /dev/null
+++ b/include/rive/custom_property.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_CUSTOM_PROPERTY_HPP_
+#define _RIVE_CUSTOM_PROPERTY_HPP_
+#include "rive/generated/custom_property_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class CustomProperty : public CustomPropertyBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/custom_property_boolean.hpp b/include/rive/custom_property_boolean.hpp
new file mode 100644
index 0000000..260bc0f
--- /dev/null
+++ b/include/rive/custom_property_boolean.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_CUSTOM_PROPERTY_BOOLEAN_HPP_
+#define _RIVE_CUSTOM_PROPERTY_BOOLEAN_HPP_
+#include "rive/generated/custom_property_boolean_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class CustomPropertyBoolean : public CustomPropertyBooleanBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/custom_property_number.hpp b/include/rive/custom_property_number.hpp
new file mode 100644
index 0000000..192351b
--- /dev/null
+++ b/include/rive/custom_property_number.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_CUSTOM_PROPERTY_NUMBER_HPP_
+#define _RIVE_CUSTOM_PROPERTY_NUMBER_HPP_
+#include "rive/generated/custom_property_number_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class CustomPropertyNumber : public CustomPropertyNumberBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/custom_property_string.hpp b/include/rive/custom_property_string.hpp
new file mode 100644
index 0000000..7efc20e
--- /dev/null
+++ b/include/rive/custom_property_string.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_CUSTOM_PROPERTY_STRING_HPP_
+#define _RIVE_CUSTOM_PROPERTY_STRING_HPP_
+#include "rive/generated/custom_property_string_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class CustomPropertyString : public CustomPropertyStringBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/bindable_property.hpp b/include/rive/data_bind/bindable_property.hpp
new file mode 100644
index 0000000..cbfb148
--- /dev/null
+++ b/include/rive/data_bind/bindable_property.hpp
@@ -0,0 +1,12 @@
+#ifndef _RIVE_BINDABLE_PROPERTY_HPP_
+#define _RIVE_BINDABLE_PROPERTY_HPP_
+#include "rive/generated/data_bind/bindable_property_base.hpp"
+#include "rive/data_bind/data_bind.hpp"
+#include <stdio.h>
+namespace rive
+{
+class BindableProperty : public BindablePropertyBase
+{};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/bindable_property_boolean.hpp b/include/rive/data_bind/bindable_property_boolean.hpp
new file mode 100644
index 0000000..18037b6
--- /dev/null
+++ b/include/rive/data_bind/bindable_property_boolean.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_BINDABLE_PROPERTY_BOOLEAN_HPP_
+#define _RIVE_BINDABLE_PROPERTY_BOOLEAN_HPP_
+#include "rive/generated/data_bind/bindable_property_boolean_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class BindablePropertyBoolean : public BindablePropertyBooleanBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/bindable_property_color.hpp b/include/rive/data_bind/bindable_property_color.hpp
new file mode 100644
index 0000000..dd128ca
--- /dev/null
+++ b/include/rive/data_bind/bindable_property_color.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_BINDABLE_PROPERTY_COLOR_HPP_
+#define _RIVE_BINDABLE_PROPERTY_COLOR_HPP_
+#include "rive/generated/data_bind/bindable_property_color_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class BindablePropertyColor : public BindablePropertyColorBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/bindable_property_enum.hpp b/include/rive/data_bind/bindable_property_enum.hpp
new file mode 100644
index 0000000..48692c2
--- /dev/null
+++ b/include/rive/data_bind/bindable_property_enum.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_BINDABLE_PROPERTY_ENUM_HPP_
+#define _RIVE_BINDABLE_PROPERTY_ENUM_HPP_
+#include "rive/generated/data_bind/bindable_property_enum_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class BindablePropertyEnum : public BindablePropertyEnumBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/bindable_property_number.hpp b/include/rive/data_bind/bindable_property_number.hpp
new file mode 100644
index 0000000..38a72fd
--- /dev/null
+++ b/include/rive/data_bind/bindable_property_number.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_BINDABLE_PROPERTY_NUMBER_HPP_
+#define _RIVE_BINDABLE_PROPERTY_NUMBER_HPP_
+#include "rive/generated/data_bind/bindable_property_number_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class BindablePropertyNumber : public BindablePropertyNumberBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/bindable_property_string.hpp b/include/rive/data_bind/bindable_property_string.hpp
new file mode 100644
index 0000000..302caf0
--- /dev/null
+++ b/include/rive/data_bind/bindable_property_string.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_BINDABLE_PROPERTY_STRING_HPP_
+#define _RIVE_BINDABLE_PROPERTY_STRING_HPP_
+#include "rive/generated/data_bind/bindable_property_string_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class BindablePropertyString : public BindablePropertyStringBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/context/context_value.hpp b/include/rive/data_bind/context/context_value.hpp
new file mode 100644
index 0000000..c26730b
--- /dev/null
+++ b/include/rive/data_bind/context/context_value.hpp
@@ -0,0 +1,49 @@
+#ifndef _RIVE_DATA_BIND_CONTEXT_VALUE_HPP_
+#define _RIVE_DATA_BIND_CONTEXT_VALUE_HPP_
+#include "rive/viewmodel/viewmodel_instance_value.hpp"
+#include "rive/data_bind/converters/data_converter.hpp"
+#include <stdio.h>
+namespace rive
+{
+class DataBindContextValue
+{
+protected:
+    ViewModelInstanceValue* m_source;
+    DataConverter* m_converter;
+    DataValue* m_dataValue;
+
+public:
+    DataBindContextValue(ViewModelInstanceValue* source, DataConverter* converter);
+    virtual ~DataBindContextValue(){};
+    virtual void applyToSource(Core* component, uint32_t propertyKey, bool isMainDirection);
+    virtual void apply(Core* component, uint32_t propertyKey, bool isMainDirection){};
+    virtual void update(Core* component){};
+    virtual DataValue* getTargetValue(Core* target, uint32_t propertyKey) { return nullptr; };
+    void updateSourceValue();
+    template <typename T = DataValue, typename U> U getDataValue(DataValue* input)
+    {
+        auto dataValue = m_converter != nullptr ? m_converter->convert(input) : input;
+        if (dataValue->is<T>())
+        {
+            return dataValue->as<T>()->value();
+        }
+        return (new T())->value();
+    };
+    template <typename T = DataValue, typename U> U getReverseDataValue(DataValue* input)
+    {
+        auto dataValue = m_converter != nullptr ? m_converter->reverseConvert(input) : input;
+        if (dataValue->is<T>())
+        {
+            return dataValue->as<T>()->value();
+        }
+        return (new T())->value();
+    };
+    template <typename T = DataValue, typename U>
+    U calculateValue(DataValue* input, bool isMainDirection)
+    {
+        return isMainDirection ? getDataValue<T, U>(input) : getReverseDataValue<T, U>(input);
+    };
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/context/context_value_boolean.hpp b/include/rive/data_bind/context/context_value_boolean.hpp
new file mode 100644
index 0000000..678e7fc
--- /dev/null
+++ b/include/rive/data_bind/context/context_value_boolean.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_DATA_BIND_CONTEXT_VALUE_BOOLEAN_HPP_
+#define _RIVE_DATA_BIND_CONTEXT_VALUE_BOOLEAN_HPP_
+#include "rive/data_bind/context/context_value.hpp"
+namespace rive
+{
+class DataBindContextValueBoolean : public DataBindContextValue
+{
+
+public:
+    DataBindContextValueBoolean(ViewModelInstanceValue* source, DataConverter* converter);
+    void apply(Core* component, uint32_t propertyKey, bool isMainDirection) override;
+    DataValue* getTargetValue(Core* target, uint32_t propertyKey) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/context/context_value_color.hpp b/include/rive/data_bind/context/context_value_color.hpp
new file mode 100644
index 0000000..207062a
--- /dev/null
+++ b/include/rive/data_bind/context/context_value_color.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_DATA_BIND_CONTEXT_VALUE_COLOR_HPP_
+#define _RIVE_DATA_BIND_CONTEXT_VALUE_COLOR_HPP_
+#include "rive/data_bind/context/context_value.hpp"
+namespace rive
+{
+class DataBindContextValueColor : public DataBindContextValue
+{
+
+public:
+    DataBindContextValueColor(ViewModelInstanceValue* source, DataConverter* converter);
+    void apply(Core* component, uint32_t propertyKey, bool isMainDirection) override;
+    DataValue* getTargetValue(Core* target, uint32_t propertyKey) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/context/context_value_enum.hpp b/include/rive/data_bind/context/context_value_enum.hpp
new file mode 100644
index 0000000..dd097b2
--- /dev/null
+++ b/include/rive/data_bind/context/context_value_enum.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_DATA_BIND_CONTEXT_VALUE_ENUM_HPP_
+#define _RIVE_DATA_BIND_CONTEXT_VALUE_ENUM_HPP_
+#include "rive/data_bind/context/context_value.hpp"
+namespace rive
+{
+class DataBindContextValueEnum : public DataBindContextValue
+{
+
+public:
+    DataBindContextValueEnum(ViewModelInstanceValue* source, DataConverter* converter);
+    void apply(Core* component, uint32_t propertyKey, bool isMainDirection) override;
+    DataValue* getTargetValue(Core* target, uint32_t propertyKey) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/context/context_value_list.hpp b/include/rive/data_bind/context/context_value_list.hpp
new file mode 100644
index 0000000..5dd9586
--- /dev/null
+++ b/include/rive/data_bind/context/context_value_list.hpp
@@ -0,0 +1,33 @@
+#ifndef _RIVE_DATA_BIND_CONTEXT_VALUE_LIST_HPP_
+#define _RIVE_DATA_BIND_CONTEXT_VALUE_LIST_HPP_
+#include "rive/data_bind/context/context_value.hpp"
+#include "rive/data_bind/context/context_value_list_item.hpp"
+#include "rive/artboard.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/viewmodel/viewmodel_instance_list_item.hpp"
+namespace rive
+{
+class DataBindContextValueList : public DataBindContextValue
+{
+
+public:
+    DataBindContextValueList(ViewModelInstanceValue* source, DataConverter* converter);
+    void apply(Core* component, uint32_t propertyKey, bool isMainDirection) override;
+    void update(Core* target) override;
+    virtual void applyToSource(Core* component,
+                               uint32_t propertyKey,
+                               bool isMainDirection) override;
+
+private:
+    std::vector<std::unique_ptr<DataBindContextValueListItem>> m_ListItemsCache;
+    void insertItem(Core* target, ViewModelInstanceListItem* viewModelInstanceListItem, int index);
+    void swapItems(Core* target, int index1, int index2);
+    void popItem(Core* target);
+    std::unique_ptr<ArtboardInstance> createArtboard(Component* target,
+                                                     Artboard* artboard,
+                                                     ViewModelInstanceListItem* listItem) const;
+    std::unique_ptr<StateMachineInstance> createStateMachineInstance(ArtboardInstance* artboard);
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/context/context_value_list_item.hpp b/include/rive/data_bind/context/context_value_list_item.hpp
new file mode 100644
index 0000000..f15493b
--- /dev/null
+++ b/include/rive/data_bind/context/context_value_list_item.hpp
@@ -0,0 +1,25 @@
+#ifndef _RIVE_DATA_BIND_CONTEXT_VALUE_LIST_ITEM_HPP_
+#define _RIVE_DATA_BIND_CONTEXT_VALUE_LIST_ITEM_HPP_
+#include "rive/data_bind/context/context_value.hpp"
+#include "rive/artboard.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/viewmodel/viewmodel_instance_list_item.hpp"
+namespace rive
+{
+class DataBindContextValueListItem
+{
+
+private:
+    std::unique_ptr<ArtboardInstance> m_Artboard;
+    std::unique_ptr<StateMachineInstance> m_StateMachine;
+    ViewModelInstanceListItem* m_ListItem;
+
+public:
+    DataBindContextValueListItem(std::unique_ptr<ArtboardInstance> artboard,
+                                 std::unique_ptr<StateMachineInstance> stateMachine,
+                                 ViewModelInstanceListItem* listItem);
+    ViewModelInstanceListItem* listItem() { return m_ListItem; };
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/context/context_value_number.hpp b/include/rive/data_bind/context/context_value_number.hpp
new file mode 100644
index 0000000..f3a56fa
--- /dev/null
+++ b/include/rive/data_bind/context/context_value_number.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_DATA_BIND_CONTEXT_VALUE_NUMBER_HPP_
+#define _RIVE_DATA_BIND_CONTEXT_VALUE_NUMBER_HPP_
+#include "rive/data_bind/context/context_value.hpp"
+namespace rive
+{
+class DataBindContextValueNumber : public DataBindContextValue
+{
+
+public:
+    DataBindContextValueNumber(ViewModelInstanceValue* source, DataConverter* converter);
+    void apply(Core* component, uint32_t propertyKey, bool isMainDirection) override;
+    DataValue* getTargetValue(Core* target, uint32_t propertyKey) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/context/context_value_string.hpp b/include/rive/data_bind/context/context_value_string.hpp
new file mode 100644
index 0000000..fb8f3cf
--- /dev/null
+++ b/include/rive/data_bind/context/context_value_string.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_DATA_BIND_CONTEXT_VALUE_STRING_HPP_
+#define _RIVE_DATA_BIND_CONTEXT_VALUE_STRING_HPP_
+#include "rive/data_bind/context/context_value.hpp"
+namespace rive
+{
+class DataBindContextValueString : public DataBindContextValue
+{
+
+public:
+    DataBindContextValueString(ViewModelInstanceValue* source, DataConverter* converter);
+    void apply(Core* component, uint32_t propertyKey, bool isMainDirection) override;
+    DataValue* getTargetValue(Core* target, uint32_t propertyKey) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/converters/data_converter.hpp b/include/rive/data_bind/converters/data_converter.hpp
new file mode 100644
index 0000000..bf6deb6
--- /dev/null
+++ b/include/rive/data_bind/converters/data_converter.hpp
@@ -0,0 +1,18 @@
+#ifndef _RIVE_DATA_CONVERTER_HPP_
+#define _RIVE_DATA_CONVERTER_HPP_
+#include "rive/generated/data_bind/converters/data_converter_base.hpp"
+#include "rive/data_bind/data_values/data_value.hpp"
+#include <stdio.h>
+namespace rive
+{
+class DataConverter : public DataConverterBase
+{
+public:
+    virtual DataValue* convert(DataValue* value) { return value; };
+    virtual DataValue* reverseConvert(DataValue* value) { return value; };
+    virtual DataType outputType() { return DataType::none; };
+    StatusCode import(ImportStack& importStack) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/converters/data_converter_group.hpp b/include/rive/data_bind/converters/data_converter_group.hpp
new file mode 100644
index 0000000..4056085
--- /dev/null
+++ b/include/rive/data_bind/converters/data_converter_group.hpp
@@ -0,0 +1,28 @@
+#ifndef _RIVE_DATA_CONVERTER_GROUP_HPP_
+#define _RIVE_DATA_CONVERTER_GROUP_HPP_
+#include "rive/generated/data_bind/converters/data_converter_group_base.hpp"
+#include "rive/data_bind/converters/data_converter_group_item.hpp"
+#include <stdio.h>
+namespace rive
+{
+class DataConverterGroup : public DataConverterGroupBase
+{
+public:
+    DataValue* convert(DataValue* value) override;
+    DataValue* reverseConvert(DataValue* value) override;
+    void addItem(DataConverterGroupItem* item);
+    DataType outputType() override
+    {
+        if (m_items.size() > 0)
+        {
+            return m_items.back()->converter()->outputType();
+        };
+        return Super::outputType();
+    }
+
+private:
+    std::vector<DataConverterGroupItem*> m_items;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/converters/data_converter_group_item.hpp b/include/rive/data_bind/converters/data_converter_group_item.hpp
new file mode 100644
index 0000000..0a6202d
--- /dev/null
+++ b/include/rive/data_bind/converters/data_converter_group_item.hpp
@@ -0,0 +1,20 @@
+#ifndef _RIVE_DATA_CONVERTER_GROUP_ITEM_HPP_
+#define _RIVE_DATA_CONVERTER_GROUP_ITEM_HPP_
+#include "rive/generated/data_bind/converters/data_converter_group_item_base.hpp"
+#include "rive/data_bind/converters/data_converter.hpp"
+#include <stdio.h>
+namespace rive
+{
+class DataConverterGroupItem : public DataConverterGroupItemBase
+{
+public:
+    StatusCode import(ImportStack& importStack) override;
+    DataConverter* converter() const { return m_dataConverter; };
+    void converter(DataConverter* value) { m_dataConverter = value; };
+
+protected:
+    DataConverter* m_dataConverter;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/converters/data_converter_operation.hpp b/include/rive/data_bind/converters/data_converter_operation.hpp
new file mode 100644
index 0000000..9bbb2b4
--- /dev/null
+++ b/include/rive/data_bind/converters/data_converter_operation.hpp
@@ -0,0 +1,18 @@
+#ifndef _RIVE_DATA_CONVERTER_OPERATION_HPP_
+#define _RIVE_DATA_CONVERTER_OPERATION_HPP_
+#include "rive/generated/data_bind/converters/data_converter_operation_base.hpp"
+#include "rive/animation/arithmetic_operation.hpp"
+#include <stdio.h>
+namespace rive
+{
+class DataConverterOperation : public DataConverterOperationBase
+{
+public:
+    DataValue* convert(DataValue* value) override;
+    DataValue* reverseConvert(DataValue* value) override;
+    DataType outputType() override { return DataType::number; };
+    ArithmeticOperation op() const { return (ArithmeticOperation)operationType(); }
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/converters/data_converter_rounder.hpp b/include/rive/data_bind/converters/data_converter_rounder.hpp
new file mode 100644
index 0000000..063d38c
--- /dev/null
+++ b/include/rive/data_bind/converters/data_converter_rounder.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_DATA_CONVERTER_ROUND_HPP_
+#define _RIVE_DATA_CONVERTER_ROUND_HPP_
+#include "rive/generated/data_bind/converters/data_converter_rounder_base.hpp"
+#include "rive/data_bind/data_values/data_value.hpp"
+#include <stdio.h>
+namespace rive
+{
+class DataConverterRounder : public DataConverterRounderBase
+{
+public:
+    DataValue* convert(DataValue* value) override;
+    DataType outputType() override { return DataType::number; };
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/converters/data_converter_to_string.hpp b/include/rive/data_bind/converters/data_converter_to_string.hpp
new file mode 100644
index 0000000..0757e60
--- /dev/null
+++ b/include/rive/data_bind/converters/data_converter_to_string.hpp
@@ -0,0 +1,15 @@
+#ifndef _RIVE_DATA_CONVERTER_TO_STRING_HPP_
+#define _RIVE_DATA_CONVERTER_TO_STRING_HPP_
+#include "rive/generated/data_bind/converters/data_converter_to_string_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class DataConverterToString : public DataConverterToStringBase
+{
+public:
+    DataValue* convert(DataValue* value) override;
+    DataType outputType() override { return DataType::string; };
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/data_bind.hpp b/include/rive/data_bind/data_bind.hpp
new file mode 100644
index 0000000..a62cfd6
--- /dev/null
+++ b/include/rive/data_bind/data_bind.hpp
@@ -0,0 +1,39 @@
+#ifndef _RIVE_DATA_BIND_HPP_
+#define _RIVE_DATA_BIND_HPP_
+#include "rive/component_dirt.hpp"
+#include "rive/generated/data_bind/data_bind_base.hpp"
+#include "rive/viewmodel/viewmodel_instance_value.hpp"
+#include "rive/data_bind/context/context_value.hpp"
+#include "rive/data_bind/data_context.hpp"
+#include "rive/data_bind/converters/data_converter.hpp"
+#include "rive/data_bind/data_values/data_type.hpp"
+#include <stdio.h>
+namespace rive
+{
+class DataBind : public DataBindBase
+{
+public:
+    StatusCode onAddedDirty(CoreContext* context) override;
+    StatusCode import(ImportStack& importStack) override;
+    virtual void updateSourceBinding();
+    virtual void update(ComponentDirt value);
+    Core* target() const { return m_target; };
+    void target(Core* value) { m_target = value; };
+    virtual void bind();
+    ComponentDirt dirt() { return m_Dirt; };
+    void dirt(ComponentDirt value) { m_Dirt = value; };
+    bool addDirt(ComponentDirt value, bool recurse);
+    DataConverter* converter() const { return m_dataConverter; };
+    void converter(DataConverter* value) { m_dataConverter = value; };
+
+protected:
+    ComponentDirt m_Dirt = ComponentDirt::Filthy;
+    Core* m_target;
+    ViewModelInstanceValue* m_Source;
+    std::unique_ptr<DataBindContextValue> m_ContextValue;
+    DataConverter* m_dataConverter;
+    DataType outputType();
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/data_bind_context.hpp b/include/rive/data_bind/data_bind_context.hpp
new file mode 100644
index 0000000..b7e2ad4
--- /dev/null
+++ b/include/rive/data_bind/data_bind_context.hpp
@@ -0,0 +1,24 @@
+#ifndef _RIVE_DATA_BIND_CONTEXT_HPP_
+#define _RIVE_DATA_BIND_CONTEXT_HPP_
+#include "rive/generated/data_bind/data_bind_context_base.hpp"
+#include "rive/viewmodel/viewmodel_instance_value.hpp"
+#include "rive/data_bind/context/context_value.hpp"
+#include "rive/data_bind/data_context.hpp"
+#include "rive/refcnt.hpp"
+#include <stdio.h>
+namespace rive
+{
+class DataBindContext : public DataBindContextBase
+{
+protected:
+    std::vector<uint32_t> m_SourcePathIdsBuffer;
+
+public:
+    void decodeSourcePathIds(Span<const uint8_t> value) override;
+    void copySourcePathIds(const DataBindContextBase& object) override;
+    void bindFromContext(DataContext* dataContext);
+    ViewModelInstanceValue* source() { return m_Source; };
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/data_context.hpp b/include/rive/data_bind/data_context.hpp
new file mode 100644
index 0000000..5bc6528
--- /dev/null
+++ b/include/rive/data_bind/data_context.hpp
@@ -0,0 +1,38 @@
+#ifndef _RIVE_DATA_CONTEXT_HPP_
+#define _RIVE_DATA_CONTEXT_HPP_
+#include "rive/viewmodel/viewmodel_instance_value.hpp"
+#include "rive/viewmodel/viewmodel_instance.hpp"
+
+namespace rive
+{
+class DataContext
+{
+private:
+    DataContext* m_Parent = nullptr;
+    std::vector<ViewModelInstance*> m_ViewModelInstances;
+    ViewModelInstance* m_ViewModelInstance;
+
+public:
+    DataContext();
+    DataContext(ViewModelInstance* viewModelInstance);
+    ~DataContext();
+
+    DataContext* parent() { return m_Parent; }
+    void parent(DataContext* value) { m_Parent = value; }
+    void addViewModelInstance(ViewModelInstance* value);
+    ViewModelInstanceValue* getViewModelProperty(const std::vector<uint32_t> path) const;
+    ViewModelInstance* getViewModelInstance(const std::vector<uint32_t> path) const;
+    void viewModelInstance(ViewModelInstance* value);
+    ViewModelInstance* viewModelInstance() { return m_ViewModelInstance; };
+
+    ViewModelInstanceValue* viewModelValue()
+    {
+        if (m_Parent)
+        {
+            return m_Parent->viewModelValue();
+        }
+        return nullptr;
+    }
+};
+} // namespace rive
+#endif
diff --git a/include/rive/data_bind/data_values/data_type.hpp b/include/rive/data_bind/data_values/data_type.hpp
new file mode 100644
index 0000000..c1740b5
--- /dev/null
+++ b/include/rive/data_bind/data_values/data_type.hpp
@@ -0,0 +1,30 @@
+#ifndef _RIVE_DATA_TYPE_HPP_
+#define _RIVE_DATA_TYPE_HPP_
+namespace rive
+{
+/// Data types used for converters.
+enum class DataType : unsigned int
+{
+    /// None.
+    none = 0,
+
+    /// String.
+    string = 1,
+
+    /// Number.
+    number = 2,
+
+    /// Bool.
+    boolean = 3,
+
+    /// Color.
+    color = 4,
+
+    /// List.
+    list = 5,
+
+    /// Enum.
+    enumType = 6
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/data_values/data_value.hpp b/include/rive/data_bind/data_values/data_value.hpp
new file mode 100644
index 0000000..e412183
--- /dev/null
+++ b/include/rive/data_bind/data_values/data_value.hpp
@@ -0,0 +1,21 @@
+#ifndef _RIVE_DATA_VALUE_HPP_
+#define _RIVE_DATA_VALUE_HPP_
+#include "rive/data_bind/data_values/data_type.hpp"
+
+#include <stdio.h>
+namespace rive
+{
+class DataValue
+{
+public:
+    virtual bool isTypeOf(DataType dataType) const { return false; }
+    template <typename T> inline bool is() const { return isTypeOf(T::typeKey); }
+    template <typename T> inline T* as()
+    {
+        assert(is<T>());
+        return static_cast<T*>(this);
+    }
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/data_values/data_value_boolean.hpp b/include/rive/data_bind/data_values/data_value_boolean.hpp
new file mode 100644
index 0000000..4fd8575
--- /dev/null
+++ b/include/rive/data_bind/data_values/data_value_boolean.hpp
@@ -0,0 +1,23 @@
+#ifndef _RIVE_DATA_VALUE_BOOLEAN_HPP_
+#define _RIVE_DATA_VALUE_BOOLEAN_HPP_
+#include "rive/data_bind/data_values/data_value.hpp"
+
+#include <stdio.h>
+namespace rive
+{
+class DataValueBoolean : public DataValue
+{
+private:
+    bool m_value = false;
+
+public:
+    DataValueBoolean(bool value) : m_value(value){};
+    DataValueBoolean(){};
+    static const DataType typeKey = DataType::boolean;
+    bool isTypeOf(DataType typeKey) const override { return typeKey == DataType::boolean; }
+    bool value() { return m_value; };
+    void value(bool value) { m_value = value; };
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/data_values/data_value_color.hpp b/include/rive/data_bind/data_values/data_value_color.hpp
new file mode 100644
index 0000000..fdc88b5
--- /dev/null
+++ b/include/rive/data_bind/data_values/data_value_color.hpp
@@ -0,0 +1,23 @@
+#ifndef _RIVE_DATA_VALUE_COLOR_HPP_
+#define _RIVE_DATA_VALUE_COLOR_HPP_
+#include "rive/data_bind/data_values/data_value.hpp"
+
+#include <stdio.h>
+namespace rive
+{
+class DataValueColor : public DataValue
+{
+private:
+    int m_value = false;
+
+public:
+    DataValueColor(int value) : m_value(value){};
+    DataValueColor(){};
+    static const DataType typeKey = DataType::color;
+    bool isTypeOf(DataType typeKey) const override { return typeKey == DataType::color; }
+    int value() { return m_value; };
+    void value(int value) { m_value = value; };
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/data_values/data_value_enum.hpp b/include/rive/data_bind/data_values/data_value_enum.hpp
new file mode 100644
index 0000000..9c2dbe5
--- /dev/null
+++ b/include/rive/data_bind/data_values/data_value_enum.hpp
@@ -0,0 +1,26 @@
+#ifndef _RIVE_DATA_VALUE_ENUM_HPP_
+#define _RIVE_DATA_VALUE_ENUM_HPP_
+#include "rive/data_bind/data_values/data_value.hpp"
+#include "rive/viewmodel/data_enum.hpp"
+
+#include <iostream>
+namespace rive
+{
+class DataValueEnum : public DataValue
+{
+private:
+    uint32_t m_value = 0;
+    DataEnum* m_dataEnum;
+
+public:
+    DataValueEnum(uint32_t value, DataEnum* dataEnum) : m_value(value), m_dataEnum(dataEnum){};
+    DataValueEnum(){};
+    static const DataType typeKey = DataType::enumType;
+    bool isTypeOf(DataType typeKey) const override { return typeKey == DataType::enumType; };
+    uint32_t value() { return m_value; };
+    void value(uint32_t value) { m_value = value; };
+    DataEnum* dataEnum() { return m_dataEnum; };
+    void dataEnum(DataEnum* value) { m_dataEnum = value; };
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/data_values/data_value_number.hpp b/include/rive/data_bind/data_values/data_value_number.hpp
new file mode 100644
index 0000000..90a3b5d
--- /dev/null
+++ b/include/rive/data_bind/data_values/data_value_number.hpp
@@ -0,0 +1,23 @@
+#ifndef _RIVE_DATA_VALUE_NUMBER_HPP_
+#define _RIVE_DATA_VALUE_NUMBER_HPP_
+#include "rive/data_bind/data_values/data_value.hpp"
+
+#include <stdio.h>
+namespace rive
+{
+class DataValueNumber : public DataValue
+{
+private:
+    float m_value = 0;
+
+public:
+    DataValueNumber(float value) : m_value(value){};
+    DataValueNumber(){};
+    static const DataType typeKey = DataType::number;
+    bool isTypeOf(DataType typeKey) const override { return typeKey == DataType::number; }
+    float value() { return m_value; };
+    void value(float value) { m_value = value; };
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind/data_values/data_value_string.hpp b/include/rive/data_bind/data_values/data_value_string.hpp
new file mode 100644
index 0000000..7bf2676
--- /dev/null
+++ b/include/rive/data_bind/data_values/data_value_string.hpp
@@ -0,0 +1,22 @@
+#ifndef _RIVE_DATA_VALUE_STRING_HPP_
+#define _RIVE_DATA_VALUE_STRING_HPP_
+#include "rive/data_bind/data_values/data_value.hpp"
+
+#include <iostream>
+namespace rive
+{
+class DataValueString : public DataValue
+{
+private:
+    std::string m_value = "";
+
+public:
+    DataValueString(std::string value) : m_value(value){};
+    DataValueString(){};
+    static const DataType typeKey = DataType::string;
+    bool isTypeOf(DataType typeKey) const override { return typeKey == DataType::string; };
+    std::string value() { return m_value; };
+    void value(std::string value) { m_value = value; };
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/data_bind_flags.hpp b/include/rive/data_bind_flags.hpp
new file mode 100644
index 0000000..2d8561f
--- /dev/null
+++ b/include/rive/data_bind_flags.hpp
@@ -0,0 +1,29 @@
+#ifndef _RIVE_DATA_BIND_FLAGS_HPP_
+#define _RIVE_DATA_BIND_FLAGS_HPP_
+
+#include "rive/enum_bitset.hpp"
+
+namespace rive
+{
+enum class DataBindFlags : unsigned short
+{
+    /// Whether the main binding direction is to source (0) or to target (1)
+    Direction = 1 << 0,
+
+    /// Whether the binding direction is twoWay
+    TwoWay = 1 << 1,
+
+    /// Whether the binding happens only once
+    Once = 1 << 2,
+
+    /// Flag if set to target
+    ToTarget = 0,
+
+    /// Flag if set to source
+    ToSource = 1 << 0,
+
+};
+
+RIVE_MAKE_ENUM_BITSET(DataBindFlags)
+} // namespace rive
+#endif
diff --git a/include/rive/dependency_helper.hpp b/include/rive/dependency_helper.hpp
new file mode 100644
index 0000000..59ef805
--- /dev/null
+++ b/include/rive/dependency_helper.hpp
@@ -0,0 +1,46 @@
+#ifndef _RIVE_DEPENDENCY_HELPER_HPP_
+#define _RIVE_DEPENDENCY_HELPER_HPP_
+
+#include "rive/component_dirt.hpp"
+
+namespace rive
+{
+class Component;
+// class DependencyRoot
+// {
+// public:
+//     virtual void onComponentDirty(Component* component) {};
+// };
+
+template <typename T, typename U> class DependencyHelper
+{
+    std::vector<U*> m_Dependents;
+    T* m_dependecyRoot;
+
+public:
+    void dependecyRoot(T* value) { m_dependecyRoot = value; }
+    DependencyHelper(T* value) : m_dependecyRoot(value) {}
+    DependencyHelper() {}
+    void addDependent(U* component)
+    {
+        // Make it's not already a dependent.
+        if (std::find(m_Dependents.begin(), m_Dependents.end(), component) != m_Dependents.end())
+        {
+            return;
+        }
+        m_Dependents.push_back(component);
+    }
+    void addDirt(ComponentDirt value)
+    {
+        for (auto d : m_Dependents)
+        {
+            d->addDirt(value, true);
+        }
+    }
+
+    void onComponentDirty(U* component) { m_dependecyRoot->onComponentDirty(component); }
+
+    const std::vector<U*>& dependents() const { return m_Dependents; }
+};
+} // namespace rive
+#endif
diff --git a/include/rive/dependency_sorter.hpp b/include/rive/dependency_sorter.hpp
new file mode 100644
index 0000000..17ac825
--- /dev/null
+++ b/include/rive/dependency_sorter.hpp
@@ -0,0 +1,23 @@
+#ifndef _RIVE_DEPENDENCYSORTER_HPP_
+#define _RIVE_DEPENDENCYSORTER_HPP_
+
+#include <unordered_set>
+#include <vector>
+
+namespace rive
+{
+class Component;
+class DependencySorter
+{
+private:
+    std::unordered_set<Component*> m_Perm;
+    std::unordered_set<Component*> m_Temp;
+
+public:
+    void sort(Component* root, std::vector<Component*>& order);
+    void sort(std::vector<Component*> roots, std::vector<Component*>& order);
+    bool visit(Component* component, std::vector<Component*>& order);
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/draw_rules.hpp b/include/rive/draw_rules.hpp
new file mode 100644
index 0000000..7ff58e5
--- /dev/null
+++ b/include/rive/draw_rules.hpp
@@ -0,0 +1,24 @@
+#ifndef _RIVE_DRAW_RULES_HPP_
+#define _RIVE_DRAW_RULES_HPP_
+#include "rive/generated/draw_rules_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class DrawTarget;
+class DrawRules : public DrawRulesBase
+{
+private:
+    DrawTarget* m_ActiveTarget = nullptr;
+
+public:
+    DrawTarget* activeTarget() const { return m_ActiveTarget; }
+
+    StatusCode onAddedDirty(CoreContext* context) override;
+    StatusCode onAddedClean(CoreContext* context) override;
+
+protected:
+    void drawTargetIdChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/draw_target.hpp b/include/rive/draw_target.hpp
new file mode 100644
index 0000000..b37113c
--- /dev/null
+++ b/include/rive/draw_target.hpp
@@ -0,0 +1,35 @@
+#ifndef _RIVE_DRAW_TARGET_HPP_
+#define _RIVE_DRAW_TARGET_HPP_
+
+#include "rive/draw_target_placement.hpp"
+#include "rive/generated/draw_target_base.hpp"
+#include <stdio.h>
+
+namespace rive
+{
+class Drawable;
+class Artboard;
+class DrawTarget : public DrawTargetBase
+{
+    friend class Artboard;
+
+private:
+    Drawable* m_Drawable = nullptr;
+
+    // Controlled by the artboard.
+    Drawable* first = nullptr;
+    Drawable* last = nullptr;
+
+public:
+    Drawable* drawable() const { return m_Drawable; }
+    StatusCode onAddedDirty(CoreContext* context) override;
+    StatusCode onAddedClean(CoreContext* context) override;
+
+    DrawTargetPlacement placement() const { return (DrawTargetPlacement)placementValue(); }
+
+protected:
+    void placementValueChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/draw_target_placement.hpp b/include/rive/draw_target_placement.hpp
new file mode 100644
index 0000000..b2e45cd
--- /dev/null
+++ b/include/rive/draw_target_placement.hpp
@@ -0,0 +1,11 @@
+#ifndef _RIVE_DRAW_TARGET_PLACEMENT_HPP_
+#define _RIVE_DRAW_TARGET_PLACEMENT_HPP_
+namespace rive
+{
+enum class DrawTargetPlacement : unsigned char
+{
+    before = 0,
+    after = 1
+};
+}
+#endif
\ No newline at end of file
diff --git a/include/rive/drawable.hpp b/include/rive/drawable.hpp
new file mode 100644
index 0000000..28220df
--- /dev/null
+++ b/include/rive/drawable.hpp
@@ -0,0 +1,76 @@
+#ifndef _RIVE_DRAWABLE_HPP_
+#define _RIVE_DRAWABLE_HPP_
+#include "rive/generated/drawable_base.hpp"
+#include "rive/hit_info.hpp"
+#include "rive/renderer.hpp"
+#include "rive/clip_result.hpp"
+#include "rive/drawable_flag.hpp"
+#include <vector>
+
+namespace rive
+{
+class ClippingShape;
+class Artboard;
+class DrawRules;
+class LayoutComponent;
+
+class Drawable : public DrawableBase
+{
+    friend class Artboard;
+    friend class StateMachineInstance;
+
+private:
+    std::vector<ClippingShape*> m_ClippingShapes;
+
+    /// Used exclusively by the artboard;
+    DrawRules* flattenedDrawRules = nullptr;
+    Drawable* prev = nullptr;
+    Drawable* next = nullptr;
+
+public:
+    BlendMode blendMode() const { return (BlendMode)blendModeValue(); }
+    ClipResult applyClip(Renderer* renderer) const;
+    virtual void draw(Renderer* renderer) = 0;
+    virtual Core* hitTest(HitInfo*, const Mat2D&) = 0;
+    void addClippingShape(ClippingShape* shape);
+    inline const std::vector<ClippingShape*>& clippingShapes() const { return m_ClippingShapes; }
+
+    inline bool isHidden() const
+    {
+        return (static_cast<DrawableFlag>(drawableFlags()) & DrawableFlag::Hidden) ==
+                   DrawableFlag::Hidden ||
+               hasDirt(ComponentDirt::Collapsed);
+    }
+
+    inline bool isTargetOpaque() const
+    {
+        return (static_cast<DrawableFlag>(drawableFlags()) & DrawableFlag::Opaque) ==
+               DrawableFlag::Opaque;
+    }
+
+    bool isChildOfLayout(LayoutComponent* layout);
+
+    StatusCode onAddedDirty(CoreContext* context) override;
+};
+
+class ProxyDrawing
+{
+public:
+    virtual void drawProxy(Renderer* renderer) = 0;
+};
+
+class DrawableProxy : public Drawable
+{
+private:
+    ProxyDrawing* m_proxyDrawing;
+
+public:
+    DrawableProxy(ProxyDrawing* proxy) : m_proxyDrawing(proxy) {}
+
+    void draw(Renderer* renderer) override { m_proxyDrawing->drawProxy(renderer); }
+
+    Core* hitTest(HitInfo*, const Mat2D&) override { return nullptr; }
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/drawable_flag.hpp b/include/rive/drawable_flag.hpp
new file mode 100644
index 0000000..c21604d
--- /dev/null
+++ b/include/rive/drawable_flag.hpp
@@ -0,0 +1,30 @@
+#ifndef _RIVE_DRAWABLE_FLAGS_HPP_
+#define _RIVE_DRAWABLE_FLAGS_HPP_
+
+#include "rive/enum_bitset.hpp"
+
+namespace rive
+{
+enum class DrawableFlag : unsigned short
+{
+    None = 0,
+
+    /// Whether the component should be drawn
+    Hidden = 1 << 0,
+
+    /// Editor only
+    Locked = 1 << 1,
+
+    /// Editor only
+    Disconnected = 1 << 2,
+
+    /// Whether this Component lets hit events pass through to components behind it
+    Opaque = 1 << 3,
+
+    /// Whether the computed world bounds for a shape need to be recalculated
+    /// Using Clean instead of dirty so it doesn't need to be initialized to 1
+    WorldBoundsClean = 1 << 4,
+};
+RIVE_MAKE_ENUM_BITSET(DrawableFlag)
+} // namespace rive
+#endif
diff --git a/include/rive/enum_bitset.hpp b/include/rive/enum_bitset.hpp
new file mode 100644
index 0000000..6ba8609
--- /dev/null
+++ b/include/rive/enum_bitset.hpp
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2023 Rive
+ */
+
+#ifndef _RIVE_ENUM_BITSET_HPP_
+#define _RIVE_ENUM_BITSET_HPP_
+
+#include <type_traits>
+
+namespace rive
+{
+// Wraps an enum containing a bitfield so that we can do implicit bool conversions.
+template <typename T> class EnumBitset
+{
+public:
+    using UnderlyingType = typename std::underlying_type<T>::type;
+    constexpr EnumBitset() = default;
+    constexpr EnumBitset(T value) : m_bits(static_cast<UnderlyingType>(value)) {}
+    constexpr UnderlyingType bits() const { return m_bits; }
+    constexpr T value() const { return static_cast<T>(m_bits); }
+    constexpr operator T() const { return value(); }
+    constexpr operator bool() const { return m_bits != 0; }
+
+private:
+    UnderlyingType m_bits = 0;
+};
+} // namespace rive
+
+#define RIVE_MAKE_ENUM_BITSET(ENUM)                                                                \
+    constexpr inline ::rive::EnumBitset<ENUM> operator&(::rive::EnumBitset<ENUM> lhs,              \
+                                                        ::rive::EnumBitset<ENUM> rhs)              \
+    {                                                                                              \
+        return static_cast<ENUM>(lhs.bits() & rhs.bits());                                         \
+    }                                                                                              \
+    constexpr inline ::rive::EnumBitset<ENUM> operator&(ENUM lhs, ::rive::EnumBitset<ENUM> rhs)    \
+    {                                                                                              \
+        return ::rive::EnumBitset<ENUM>(lhs) & rhs;                                                \
+    }                                                                                              \
+    constexpr inline ::rive::EnumBitset<ENUM> operator&(::rive::EnumBitset<ENUM> lhs, ENUM rhs)    \
+    {                                                                                              \
+        return lhs & ::rive::EnumBitset<ENUM>(rhs);                                                \
+    }                                                                                              \
+    constexpr inline ::rive::EnumBitset<ENUM> operator&(ENUM lhs, ENUM rhs)                        \
+    {                                                                                              \
+        return ::rive::EnumBitset<ENUM>(lhs) & ::rive::EnumBitset<ENUM>(rhs);                      \
+    }                                                                                              \
+                                                                                                   \
+    constexpr inline ::rive::EnumBitset<ENUM> operator|(::rive::EnumBitset<ENUM> lhs,              \
+                                                        ::rive::EnumBitset<ENUM> rhs)              \
+    {                                                                                              \
+        return static_cast<ENUM>(lhs.bits() | rhs.bits());                                         \
+    }                                                                                              \
+    constexpr inline ::rive::EnumBitset<ENUM> operator|(ENUM lhs, ::rive::EnumBitset<ENUM> rhs)    \
+    {                                                                                              \
+        return ::rive::EnumBitset<ENUM>(lhs) | rhs;                                                \
+    }                                                                                              \
+    constexpr inline ::rive::EnumBitset<ENUM> operator|(::rive::EnumBitset<ENUM> lhs, ENUM rhs)    \
+    {                                                                                              \
+        return lhs | ::rive::EnumBitset<ENUM>(rhs);                                                \
+    }                                                                                              \
+    constexpr inline ::rive::EnumBitset<ENUM> operator|(ENUM lhs, ENUM rhs)                        \
+    {                                                                                              \
+        return ::rive::EnumBitset<ENUM>(lhs) | ::rive::EnumBitset<ENUM>(rhs);                      \
+    }                                                                                              \
+                                                                                                   \
+    constexpr inline ::rive::EnumBitset<ENUM> operator~(::rive::EnumBitset<ENUM> rhs)              \
+    {                                                                                              \
+        return static_cast<ENUM>(~rhs.bits());                                                     \
+    }                                                                                              \
+    constexpr inline ::rive::EnumBitset<ENUM> operator~(ENUM rhs)                                  \
+    {                                                                                              \
+        return ~::rive::EnumBitset<ENUM>(rhs);                                                     \
+    }                                                                                              \
+                                                                                                   \
+    inline ENUM& operator&=(ENUM& lhs, ::rive::EnumBitset<ENUM> rhs) { return lhs = lhs & rhs; }   \
+    inline ENUM& operator&=(ENUM& lhs, ENUM rhs) { return lhs = lhs & rhs; }                       \
+                                                                                                   \
+    inline ENUM& operator|=(ENUM& lhs, ::rive::EnumBitset<ENUM> rhs) { return lhs = lhs | rhs; }   \
+    inline ENUM& operator|=(ENUM& lhs, ENUM rhs) { return lhs = lhs | rhs; }
+
+#endif
diff --git a/include/rive/event.hpp b/include/rive/event.hpp
new file mode 100644
index 0000000..743c6c5
--- /dev/null
+++ b/include/rive/event.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_EVENT_HPP_
+#define _RIVE_EVENT_HPP_
+#include "rive/generated/event_base.hpp"
+
+namespace rive
+{
+class Event : public EventBase
+{
+public:
+    void trigger(const CallbackData& value) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/event_report.hpp b/include/rive/event_report.hpp
new file mode 100644
index 0000000..fe56315
--- /dev/null
+++ b/include/rive/event_report.hpp
@@ -0,0 +1,21 @@
+#ifndef _RIVE_EVENT_REPORT_HPP_
+#define _RIVE_EVENT_REPORT_HPP_
+
+namespace rive
+{
+class Event;
+
+class EventReport
+{
+public:
+    EventReport(Event* event, float secondsDelay) : m_event(event), m_secondsDelay(secondsDelay) {}
+    Event* event() const { return m_event; }
+    float secondsDelay() const { return m_secondsDelay; }
+
+private:
+    Event* m_event;
+    float m_secondsDelay;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/factory.hpp b/include/rive/factory.hpp
new file mode 100644
index 0000000..417d4ef
--- /dev/null
+++ b/include/rive/factory.hpp
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_FACTORY_HPP_
+#define _RIVE_FACTORY_HPP_
+
+#include "rive/renderer.hpp"
+#include "rive/text_engine.hpp"
+#include "rive/audio/audio_source.hpp"
+#include "rive/refcnt.hpp"
+#include "rive/span.hpp"
+#include "rive/math/aabb.hpp"
+
+#include <stdio.h>
+#include <cstdint>
+
+namespace rive
+{
+
+class RawPath;
+
+class Factory
+{
+public:
+    Factory() {}
+    virtual ~Factory() {}
+
+    virtual rcp<RenderBuffer> makeRenderBuffer(RenderBufferType,
+                                               RenderBufferFlags,
+                                               size_t sizeInBytes) = 0;
+
+    virtual rcp<RenderShader> makeLinearGradient(float sx,
+                                                 float sy,
+                                                 float ex,
+                                                 float ey,
+                                                 const ColorInt colors[], // [count]
+                                                 const float stops[],     // [count]
+                                                 size_t count) = 0;
+
+    virtual rcp<RenderShader> makeRadialGradient(float cx,
+                                                 float cy,
+                                                 float radius,
+                                                 const ColorInt colors[], // [count]
+                                                 const float stops[],     // [count]
+                                                 size_t count) = 0;
+
+    // Returns a full-formed RenderPath -- can be treated as immutable
+    // This call might swap out the arrays backing the points and verbs in the given RawPath, so the
+    // caller can expect it to be in an undefined state upon return.
+    virtual rcp<RenderPath> makeRenderPath(RawPath&, FillRule) = 0;
+
+    // Deprecated -- working to make RenderPath's immutable
+    virtual rcp<RenderPath> makeEmptyRenderPath() = 0;
+
+    virtual rcp<RenderPaint> makeRenderPaint() = 0;
+
+    virtual rcp<RenderImage> decodeImage(Span<const uint8_t>) = 0;
+
+    virtual rcp<Font> decodeFont(Span<const uint8_t>);
+
+    virtual rcp<AudioSource> decodeAudio(Span<const uint8_t>);
+
+    // Non-virtual helpers
+
+    rcp<RenderPath> makeRenderPath(const AABB&);
+};
+
+} // namespace rive
+#endif
diff --git a/include/rive/file.hpp b/include/rive/file.hpp
new file mode 100644
index 0000000..52c1dfe
--- /dev/null
+++ b/include/rive/file.hpp
@@ -0,0 +1,146 @@
+#ifndef _RIVE_FILE_HPP_
+#define _RIVE_FILE_HPP_
+
+#include "rive/artboard.hpp"
+#include "rive/backboard.hpp"
+#include "rive/factory.hpp"
+#include "rive/file_asset_loader.hpp"
+#include "rive/viewmodel/data_enum.hpp"
+#include "rive/viewmodel/viewmodel_component.hpp"
+#include "rive/viewmodel/viewmodel_instance.hpp"
+#include "rive/viewmodel/viewmodel_instance_value.hpp"
+#include "rive/viewmodel/viewmodel_instance_viewmodel.hpp"
+#include "rive/viewmodel/viewmodel_instance_list_item.hpp"
+#include <vector>
+#include <set>
+
+///
+/// Default namespace for Rive Cpp runtime code.
+///
+namespace rive
+{
+class BinaryReader;
+class RuntimeHeader;
+class Factory;
+
+///
+/// Tracks the success/failure result when importing a Rive file.
+///
+enum class ImportResult
+{
+    /// Indicates that a file's been successfully imported.
+    success,
+    /// Indicates that the Rive file is not supported by this runtime.
+    unsupportedVersion,
+    /// Indicates that the there is a formatting problem in the file itself.
+    malformed
+};
+
+///
+/// A Rive file.
+///
+class File
+{
+public:
+    /// Major version number supported by the runtime.
+    static const int majorVersion = 7;
+    /// Minor version number supported by the runtime.
+    static const int minorVersion = 0;
+
+    File(Factory*, FileAssetLoader*);
+
+public:
+    ~File();
+
+    ///
+    /// Imports a Rive file from a binary buffer.
+    /// @param data the raw date of the file.
+    /// @param result is an optional status result.
+    /// @param assetLoader is an optional helper to load assets which
+    /// cannot be found in-band.
+    /// @returns a pointer to the file, or null on failure.
+    static std::unique_ptr<File> import(Span<const uint8_t> data,
+                                        Factory*,
+                                        ImportResult* result = nullptr,
+                                        FileAssetLoader* assetLoader = nullptr);
+
+    /// @returns the file's backboard. All files have exactly one backboard.
+    Backboard* backboard() const { return m_backboard; }
+
+    /// @returns the number of artboards in the file.
+    size_t artboardCount() const { return m_artboards.size(); }
+    std::string artboardNameAt(size_t index) const;
+
+    const std::vector<FileAsset*>& assets() const;
+
+    // Instances
+    std::unique_ptr<ArtboardInstance> artboardDefault() const;
+    std::unique_ptr<ArtboardInstance> artboardAt(size_t index) const;
+    std::unique_ptr<ArtboardInstance> artboardNamed(std::string name) const;
+
+    Artboard* artboard() const;
+
+    /// @returns the named artboard. If no artboard is found with that name,
+    /// the null pointer is returned.
+    Artboard* artboard(std::string name) const;
+
+    /// @returns the artboard at the specified index, or the nullptr if the
+    /// index is out of range.
+    Artboard* artboard(size_t index) const;
+
+    /// @returns a view model instance of the view model with the specified name.
+    ViewModelInstance* createViewModelInstance(std::string name);
+
+    /// @returns a view model instance attached to the artboard if it exists.
+    ViewModelInstance* createViewModelInstance(Artboard* artboard);
+
+    /// @returns a view model instance of the viewModel.
+    ViewModelInstance* createViewModelInstance(ViewModel* viewModel);
+
+    /// @returns a view model instance of the viewModel by name and instance name.
+    ViewModelInstance* createViewModelInstance(std::string name, std::string instanceName);
+
+    ViewModel* viewModel(std::string name);
+    ViewModelInstanceListItem* viewModelInstanceListItem(ViewModelInstance* viewModelInstance);
+    ViewModelInstanceListItem* viewModelInstanceListItem(ViewModelInstance* viewModelInstance,
+                                                         Artboard* artboard);
+
+#ifdef WITH_RIVE_TOOLS
+    /// Strips FileAssetContents for FileAssets of given typeKeys.
+    /// @param data the raw data of the file.
+    /// @param result is an optional status result.
+    /// @returns the data buffer of the file with the FileAssetContents objects
+    /// stripped out.
+    static const std::vector<uint8_t> stripAssets(Span<const uint8_t> data,
+                                                  std::set<uint16_t> typeKeys,
+                                                  ImportResult* result = nullptr);
+#endif
+
+private:
+    ImportResult read(BinaryReader&, const RuntimeHeader&);
+
+    /// The file's backboard. All Rive files have a single backboard
+    /// where the artboards live.
+    Backboard* m_backboard;
+
+    /// We just keep these alive for the life of this File
+    std::vector<FileAsset*> m_fileAssets;
+
+    /// List of artboards in the file. Each artboard encapsulates a set of
+    /// Rive components and animations.
+    std::vector<Artboard*> m_artboards;
+
+    std::vector<ViewModel*> m_ViewModels;
+    std::vector<DataEnum*> m_Enums;
+
+    Factory* m_factory;
+
+    /// The helper used to load assets when they're not provided in-band
+    /// with the file.
+    FileAssetLoader* m_assetLoader;
+
+    void completeViewModelInstance(ViewModelInstance* viewModelInstance);
+    ViewModelInstance* copyViewModelInstance(ViewModelInstance* viewModelInstance);
+};
+} // namespace rive
+#endif
diff --git a/include/rive/file_asset_loader.hpp b/include/rive/file_asset_loader.hpp
new file mode 100644
index 0000000..0f18206
--- /dev/null
+++ b/include/rive/file_asset_loader.hpp
@@ -0,0 +1,29 @@
+#ifndef _RIVE_FILE_ASSET_RESOLVER_HPP_
+#define _RIVE_FILE_ASSET_RESOLVER_HPP_
+
+#include <cstdint>
+#include <vector>
+#include "rive/span.hpp"
+
+namespace rive
+{
+class Factory;
+class FileAsset;
+class FileAssetLoader
+{
+public:
+    virtual ~FileAssetLoader() {}
+
+    /// Load the contents of the given asset
+    ///
+    /// @param asset describes the asset that Rive is looking for the
+    /// contents of.
+    /// @param inBandBytes is a pointer to the bytes in question
+    /// @returns bool indicating if we are loading or have loaded the contents
+
+    virtual bool loadContents(FileAsset& asset,
+                              Span<const uint8_t> inBandBytes,
+                              Factory* factory) = 0;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/generated/animation/advanceable_state_base.hpp b/include/rive/generated/animation/advanceable_state_base.hpp
new file mode 100644
index 0000000..bed363e
--- /dev/null
+++ b/include/rive/generated/animation/advanceable_state_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_ADVANCEABLE_STATE_BASE_HPP_
+#define _RIVE_ADVANCEABLE_STATE_BASE_HPP_
+#include "rive/animation/layer_state.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class AdvanceableStateBase : public LayerState
+{
+protected:
+    typedef LayerState Super;
+
+public:
+    static const uint16_t typeKey = 145;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case AdvanceableStateBase::typeKey:
+            case LayerStateBase::typeKey:
+            case StateMachineLayerComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t speedPropertyKey = 292;
+
+private:
+    float m_Speed = 1.0f;
+
+public:
+    inline float speed() const { return m_Speed; }
+    void speed(float value)
+    {
+        if (m_Speed == value)
+        {
+            return;
+        }
+        m_Speed = value;
+        speedChanged();
+    }
+
+    void copy(const AdvanceableStateBase& object)
+    {
+        m_Speed = object.m_Speed;
+        LayerState::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case speedPropertyKey:
+                m_Speed = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return LayerState::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void speedChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/animation_base.hpp b/include/rive/generated/animation/animation_base.hpp
new file mode 100644
index 0000000..7a11b3f
--- /dev/null
+++ b/include/rive/generated/animation/animation_base.hpp
@@ -0,0 +1,67 @@
+#ifndef _RIVE_ANIMATION_BASE_HPP_
+#define _RIVE_ANIMATION_BASE_HPP_
+#include <string>
+#include "rive/core.hpp"
+#include "rive/core/field_types/core_string_type.hpp"
+namespace rive
+{
+class AnimationBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 27;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case AnimationBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t namePropertyKey = 55;
+
+private:
+    std::string m_Name = "";
+
+public:
+    inline const std::string& name() const { return m_Name; }
+    void name(std::string value)
+    {
+        if (m_Name == value)
+        {
+            return;
+        }
+        m_Name = value;
+        nameChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const AnimationBase& object) { m_Name = object.m_Name; }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case namePropertyKey:
+                m_Name = CoreStringType::deserialize(reader);
+                return true;
+        }
+        return false;
+    }
+
+protected:
+    virtual void nameChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/animation_state_base.hpp b/include/rive/generated/animation/animation_state_base.hpp
new file mode 100644
index 0000000..dd71a3b
--- /dev/null
+++ b/include/rive/generated/animation/animation_state_base.hpp
@@ -0,0 +1,73 @@
+#ifndef _RIVE_ANIMATION_STATE_BASE_HPP_
+#define _RIVE_ANIMATION_STATE_BASE_HPP_
+#include "rive/animation/advanceable_state.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class AnimationStateBase : public AdvanceableState
+{
+protected:
+    typedef AdvanceableState Super;
+
+public:
+    static const uint16_t typeKey = 61;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case AnimationStateBase::typeKey:
+            case AdvanceableStateBase::typeKey:
+            case LayerStateBase::typeKey:
+            case StateMachineLayerComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t animationIdPropertyKey = 149;
+
+private:
+    uint32_t m_AnimationId = -1;
+
+public:
+    inline uint32_t animationId() const { return m_AnimationId; }
+    void animationId(uint32_t value)
+    {
+        if (m_AnimationId == value)
+        {
+            return;
+        }
+        m_AnimationId = value;
+        animationIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const AnimationStateBase& object)
+    {
+        m_AnimationId = object.m_AnimationId;
+        AdvanceableState::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case animationIdPropertyKey:
+                m_AnimationId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return AdvanceableState::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void animationIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/any_state_base.hpp b/include/rive/generated/animation/any_state_base.hpp
new file mode 100644
index 0000000..d41bd12
--- /dev/null
+++ b/include/rive/generated/animation/any_state_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_ANY_STATE_BASE_HPP_
+#define _RIVE_ANY_STATE_BASE_HPP_
+#include "rive/animation/layer_state.hpp"
+namespace rive
+{
+class AnyStateBase : public LayerState
+{
+protected:
+    typedef LayerState Super;
+
+public:
+    static const uint16_t typeKey = 62;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case AnyStateBase::typeKey:
+            case LayerStateBase::typeKey:
+            case StateMachineLayerComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/blend_animation_1d_base.hpp b/include/rive/generated/animation/blend_animation_1d_base.hpp
new file mode 100644
index 0000000..c6b147e
--- /dev/null
+++ b/include/rive/generated/animation/blend_animation_1d_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_BLEND_ANIMATION1_DBASE_HPP_
+#define _RIVE_BLEND_ANIMATION1_DBASE_HPP_
+#include "rive/animation/blend_animation.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class BlendAnimation1DBase : public BlendAnimation
+{
+protected:
+    typedef BlendAnimation Super;
+
+public:
+    static const uint16_t typeKey = 75;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case BlendAnimation1DBase::typeKey:
+            case BlendAnimationBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t valuePropertyKey = 166;
+
+private:
+    float m_Value = 0.0f;
+
+public:
+    inline float value() const { return m_Value; }
+    void value(float value)
+    {
+        if (m_Value == value)
+        {
+            return;
+        }
+        m_Value = value;
+        valueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const BlendAnimation1DBase& object)
+    {
+        m_Value = object.m_Value;
+        BlendAnimation::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case valuePropertyKey:
+                m_Value = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return BlendAnimation::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void valueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/blend_animation_base.hpp b/include/rive/generated/animation/blend_animation_base.hpp
new file mode 100644
index 0000000..5247bcb
--- /dev/null
+++ b/include/rive/generated/animation/blend_animation_base.hpp
@@ -0,0 +1,65 @@
+#ifndef _RIVE_BLEND_ANIMATION_BASE_HPP_
+#define _RIVE_BLEND_ANIMATION_BASE_HPP_
+#include "rive/core.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class BlendAnimationBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 74;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case BlendAnimationBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t animationIdPropertyKey = 165;
+
+private:
+    uint32_t m_AnimationId = -1;
+
+public:
+    inline uint32_t animationId() const { return m_AnimationId; }
+    void animationId(uint32_t value)
+    {
+        if (m_AnimationId == value)
+        {
+            return;
+        }
+        m_AnimationId = value;
+        animationIdChanged();
+    }
+
+    void copy(const BlendAnimationBase& object) { m_AnimationId = object.m_AnimationId; }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case animationIdPropertyKey:
+                m_AnimationId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return false;
+    }
+
+protected:
+    virtual void animationIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/blend_animation_direct_base.hpp b/include/rive/generated/animation/blend_animation_direct_base.hpp
new file mode 100644
index 0000000..915c2d1
--- /dev/null
+++ b/include/rive/generated/animation/blend_animation_direct_base.hpp
@@ -0,0 +1,108 @@
+#ifndef _RIVE_BLEND_ANIMATION_DIRECT_BASE_HPP_
+#define _RIVE_BLEND_ANIMATION_DIRECT_BASE_HPP_
+#include "rive/animation/blend_animation.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class BlendAnimationDirectBase : public BlendAnimation
+{
+protected:
+    typedef BlendAnimation Super;
+
+public:
+    static const uint16_t typeKey = 77;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case BlendAnimationDirectBase::typeKey:
+            case BlendAnimationBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t inputIdPropertyKey = 168;
+    static const uint16_t mixValuePropertyKey = 297;
+    static const uint16_t blendSourcePropertyKey = 298;
+
+private:
+    uint32_t m_InputId = -1;
+    float m_MixValue = 100.0f;
+    uint32_t m_BlendSource = 0;
+
+public:
+    inline uint32_t inputId() const { return m_InputId; }
+    void inputId(uint32_t value)
+    {
+        if (m_InputId == value)
+        {
+            return;
+        }
+        m_InputId = value;
+        inputIdChanged();
+    }
+
+    inline float mixValue() const { return m_MixValue; }
+    void mixValue(float value)
+    {
+        if (m_MixValue == value)
+        {
+            return;
+        }
+        m_MixValue = value;
+        mixValueChanged();
+    }
+
+    inline uint32_t blendSource() const { return m_BlendSource; }
+    void blendSource(uint32_t value)
+    {
+        if (m_BlendSource == value)
+        {
+            return;
+        }
+        m_BlendSource = value;
+        blendSourceChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const BlendAnimationDirectBase& object)
+    {
+        m_InputId = object.m_InputId;
+        m_MixValue = object.m_MixValue;
+        m_BlendSource = object.m_BlendSource;
+        BlendAnimation::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case inputIdPropertyKey:
+                m_InputId = CoreUintType::deserialize(reader);
+                return true;
+            case mixValuePropertyKey:
+                m_MixValue = CoreDoubleType::deserialize(reader);
+                return true;
+            case blendSourcePropertyKey:
+                m_BlendSource = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return BlendAnimation::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void inputIdChanged() {}
+    virtual void mixValueChanged() {}
+    virtual void blendSourceChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/blend_state_1d_base.hpp b/include/rive/generated/animation/blend_state_1d_base.hpp
new file mode 100644
index 0000000..8b1ba7a
--- /dev/null
+++ b/include/rive/generated/animation/blend_state_1d_base.hpp
@@ -0,0 +1,73 @@
+#ifndef _RIVE_BLEND_STATE1_DBASE_HPP_
+#define _RIVE_BLEND_STATE1_DBASE_HPP_
+#include "rive/animation/blend_state.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class BlendState1DBase : public BlendState
+{
+protected:
+    typedef BlendState Super;
+
+public:
+    static const uint16_t typeKey = 76;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case BlendState1DBase::typeKey:
+            case BlendStateBase::typeKey:
+            case LayerStateBase::typeKey:
+            case StateMachineLayerComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t inputIdPropertyKey = 167;
+
+private:
+    uint32_t m_InputId = -1;
+
+public:
+    inline uint32_t inputId() const { return m_InputId; }
+    void inputId(uint32_t value)
+    {
+        if (m_InputId == value)
+        {
+            return;
+        }
+        m_InputId = value;
+        inputIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const BlendState1DBase& object)
+    {
+        m_InputId = object.m_InputId;
+        BlendState::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case inputIdPropertyKey:
+                m_InputId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return BlendState::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void inputIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/blend_state_base.hpp b/include/rive/generated/animation/blend_state_base.hpp
new file mode 100644
index 0000000..31ee3bc
--- /dev/null
+++ b/include/rive/generated/animation/blend_state_base.hpp
@@ -0,0 +1,35 @@
+#ifndef _RIVE_BLEND_STATE_BASE_HPP_
+#define _RIVE_BLEND_STATE_BASE_HPP_
+#include "rive/animation/layer_state.hpp"
+namespace rive
+{
+class BlendStateBase : public LayerState
+{
+protected:
+    typedef LayerState Super;
+
+public:
+    static const uint16_t typeKey = 72;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case BlendStateBase::typeKey:
+            case LayerStateBase::typeKey:
+            case StateMachineLayerComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/blend_state_direct_base.hpp b/include/rive/generated/animation/blend_state_direct_base.hpp
new file mode 100644
index 0000000..232a90b
--- /dev/null
+++ b/include/rive/generated/animation/blend_state_direct_base.hpp
@@ -0,0 +1,38 @@
+#ifndef _RIVE_BLEND_STATE_DIRECT_BASE_HPP_
+#define _RIVE_BLEND_STATE_DIRECT_BASE_HPP_
+#include "rive/animation/blend_state.hpp"
+namespace rive
+{
+class BlendStateDirectBase : public BlendState
+{
+protected:
+    typedef BlendState Super;
+
+public:
+    static const uint16_t typeKey = 73;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case BlendStateDirectBase::typeKey:
+            case BlendStateBase::typeKey:
+            case LayerStateBase::typeKey:
+            case StateMachineLayerComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/blend_state_transition_base.hpp b/include/rive/generated/animation/blend_state_transition_base.hpp
new file mode 100644
index 0000000..6494ee8
--- /dev/null
+++ b/include/rive/generated/animation/blend_state_transition_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_BLEND_STATE_TRANSITION_BASE_HPP_
+#define _RIVE_BLEND_STATE_TRANSITION_BASE_HPP_
+#include "rive/animation/state_transition.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class BlendStateTransitionBase : public StateTransition
+{
+protected:
+    typedef StateTransition Super;
+
+public:
+    static const uint16_t typeKey = 78;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case BlendStateTransitionBase::typeKey:
+            case StateTransitionBase::typeKey:
+            case StateMachineLayerComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t exitBlendAnimationIdPropertyKey = 171;
+
+private:
+    uint32_t m_ExitBlendAnimationId = -1;
+
+public:
+    inline uint32_t exitBlendAnimationId() const { return m_ExitBlendAnimationId; }
+    void exitBlendAnimationId(uint32_t value)
+    {
+        if (m_ExitBlendAnimationId == value)
+        {
+            return;
+        }
+        m_ExitBlendAnimationId = value;
+        exitBlendAnimationIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const BlendStateTransitionBase& object)
+    {
+        m_ExitBlendAnimationId = object.m_ExitBlendAnimationId;
+        StateTransition::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case exitBlendAnimationIdPropertyKey:
+                m_ExitBlendAnimationId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return StateTransition::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void exitBlendAnimationIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/cubic_ease_interpolator_base.hpp b/include/rive/generated/animation/cubic_ease_interpolator_base.hpp
new file mode 100644
index 0000000..aae4cf0
--- /dev/null
+++ b/include/rive/generated/animation/cubic_ease_interpolator_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_CUBIC_EASE_INTERPOLATOR_BASE_HPP_
+#define _RIVE_CUBIC_EASE_INTERPOLATOR_BASE_HPP_
+#include "rive/animation/cubic_interpolator.hpp"
+namespace rive
+{
+class CubicEaseInterpolatorBase : public CubicInterpolator
+{
+protected:
+    typedef CubicInterpolator Super;
+
+public:
+    static const uint16_t typeKey = 28;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case CubicEaseInterpolatorBase::typeKey:
+            case CubicInterpolatorBase::typeKey:
+            case KeyFrameInterpolatorBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/cubic_interpolator_base.hpp b/include/rive/generated/animation/cubic_interpolator_base.hpp
new file mode 100644
index 0000000..3ea421d
--- /dev/null
+++ b/include/rive/generated/animation/cubic_interpolator_base.hpp
@@ -0,0 +1,124 @@
+#ifndef _RIVE_CUBIC_INTERPOLATOR_BASE_HPP_
+#define _RIVE_CUBIC_INTERPOLATOR_BASE_HPP_
+#include "rive/animation/keyframe_interpolator.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class CubicInterpolatorBase : public KeyFrameInterpolator
+{
+protected:
+    typedef KeyFrameInterpolator Super;
+
+public:
+    static const uint16_t typeKey = 139;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case CubicInterpolatorBase::typeKey:
+            case KeyFrameInterpolatorBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t x1PropertyKey = 63;
+    static const uint16_t y1PropertyKey = 64;
+    static const uint16_t x2PropertyKey = 65;
+    static const uint16_t y2PropertyKey = 66;
+
+private:
+    float m_X1 = 0.42f;
+    float m_Y1 = 0.0f;
+    float m_X2 = 0.58f;
+    float m_Y2 = 1.0f;
+
+public:
+    inline float x1() const { return m_X1; }
+    void x1(float value)
+    {
+        if (m_X1 == value)
+        {
+            return;
+        }
+        m_X1 = value;
+        x1Changed();
+    }
+
+    inline float y1() const { return m_Y1; }
+    void y1(float value)
+    {
+        if (m_Y1 == value)
+        {
+            return;
+        }
+        m_Y1 = value;
+        y1Changed();
+    }
+
+    inline float x2() const { return m_X2; }
+    void x2(float value)
+    {
+        if (m_X2 == value)
+        {
+            return;
+        }
+        m_X2 = value;
+        x2Changed();
+    }
+
+    inline float y2() const { return m_Y2; }
+    void y2(float value)
+    {
+        if (m_Y2 == value)
+        {
+            return;
+        }
+        m_Y2 = value;
+        y2Changed();
+    }
+
+    void copy(const CubicInterpolatorBase& object)
+    {
+        m_X1 = object.m_X1;
+        m_Y1 = object.m_Y1;
+        m_X2 = object.m_X2;
+        m_Y2 = object.m_Y2;
+        KeyFrameInterpolator::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case x1PropertyKey:
+                m_X1 = CoreDoubleType::deserialize(reader);
+                return true;
+            case y1PropertyKey:
+                m_Y1 = CoreDoubleType::deserialize(reader);
+                return true;
+            case x2PropertyKey:
+                m_X2 = CoreDoubleType::deserialize(reader);
+                return true;
+            case y2PropertyKey:
+                m_Y2 = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return KeyFrameInterpolator::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void x1Changed() {}
+    virtual void y1Changed() {}
+    virtual void x2Changed() {}
+    virtual void y2Changed() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/cubic_interpolator_component_base.hpp b/include/rive/generated/animation/cubic_interpolator_component_base.hpp
new file mode 100644
index 0000000..d7262b4
--- /dev/null
+++ b/include/rive/generated/animation/cubic_interpolator_component_base.hpp
@@ -0,0 +1,125 @@
+#ifndef _RIVE_CUBIC_INTERPOLATOR_COMPONENT_BASE_HPP_
+#define _RIVE_CUBIC_INTERPOLATOR_COMPONENT_BASE_HPP_
+#include "rive/component.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class CubicInterpolatorComponentBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 163;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case CubicInterpolatorComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t x1PropertyKey = 337;
+    static const uint16_t y1PropertyKey = 338;
+    static const uint16_t x2PropertyKey = 339;
+    static const uint16_t y2PropertyKey = 340;
+
+private:
+    float m_X1 = 0.42f;
+    float m_Y1 = 0.0f;
+    float m_X2 = 0.58f;
+    float m_Y2 = 1.0f;
+
+public:
+    inline float x1() const { return m_X1; }
+    void x1(float value)
+    {
+        if (m_X1 == value)
+        {
+            return;
+        }
+        m_X1 = value;
+        x1Changed();
+    }
+
+    inline float y1() const { return m_Y1; }
+    void y1(float value)
+    {
+        if (m_Y1 == value)
+        {
+            return;
+        }
+        m_Y1 = value;
+        y1Changed();
+    }
+
+    inline float x2() const { return m_X2; }
+    void x2(float value)
+    {
+        if (m_X2 == value)
+        {
+            return;
+        }
+        m_X2 = value;
+        x2Changed();
+    }
+
+    inline float y2() const { return m_Y2; }
+    void y2(float value)
+    {
+        if (m_Y2 == value)
+        {
+            return;
+        }
+        m_Y2 = value;
+        y2Changed();
+    }
+
+    Core* clone() const override;
+    void copy(const CubicInterpolatorComponentBase& object)
+    {
+        m_X1 = object.m_X1;
+        m_Y1 = object.m_Y1;
+        m_X2 = object.m_X2;
+        m_Y2 = object.m_Y2;
+        Component::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case x1PropertyKey:
+                m_X1 = CoreDoubleType::deserialize(reader);
+                return true;
+            case y1PropertyKey:
+                m_Y1 = CoreDoubleType::deserialize(reader);
+                return true;
+            case x2PropertyKey:
+                m_X2 = CoreDoubleType::deserialize(reader);
+                return true;
+            case y2PropertyKey:
+                m_Y2 = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return Component::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void x1Changed() {}
+    virtual void y1Changed() {}
+    virtual void x2Changed() {}
+    virtual void y2Changed() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/cubic_value_interpolator_base.hpp b/include/rive/generated/animation/cubic_value_interpolator_base.hpp
new file mode 100644
index 0000000..1b159c2
--- /dev/null
+++ b/include/rive/generated/animation/cubic_value_interpolator_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_CUBIC_VALUE_INTERPOLATOR_BASE_HPP_
+#define _RIVE_CUBIC_VALUE_INTERPOLATOR_BASE_HPP_
+#include "rive/animation/cubic_interpolator.hpp"
+namespace rive
+{
+class CubicValueInterpolatorBase : public CubicInterpolator
+{
+protected:
+    typedef CubicInterpolator Super;
+
+public:
+    static const uint16_t typeKey = 138;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case CubicValueInterpolatorBase::typeKey:
+            case CubicInterpolatorBase::typeKey:
+            case KeyFrameInterpolatorBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/elastic_interpolator_base.hpp b/include/rive/generated/animation/elastic_interpolator_base.hpp
new file mode 100644
index 0000000..ea1df11
--- /dev/null
+++ b/include/rive/generated/animation/elastic_interpolator_base.hpp
@@ -0,0 +1,108 @@
+#ifndef _RIVE_ELASTIC_INTERPOLATOR_BASE_HPP_
+#define _RIVE_ELASTIC_INTERPOLATOR_BASE_HPP_
+#include "rive/animation/keyframe_interpolator.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class ElasticInterpolatorBase : public KeyFrameInterpolator
+{
+protected:
+    typedef KeyFrameInterpolator Super;
+
+public:
+    static const uint16_t typeKey = 174;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ElasticInterpolatorBase::typeKey:
+            case KeyFrameInterpolatorBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t easingValuePropertyKey = 405;
+    static const uint16_t amplitudePropertyKey = 406;
+    static const uint16_t periodPropertyKey = 407;
+
+private:
+    uint32_t m_EasingValue = 1;
+    float m_Amplitude = 1.0f;
+    float m_Period = 1.0f;
+
+public:
+    inline uint32_t easingValue() const { return m_EasingValue; }
+    void easingValue(uint32_t value)
+    {
+        if (m_EasingValue == value)
+        {
+            return;
+        }
+        m_EasingValue = value;
+        easingValueChanged();
+    }
+
+    inline float amplitude() const { return m_Amplitude; }
+    void amplitude(float value)
+    {
+        if (m_Amplitude == value)
+        {
+            return;
+        }
+        m_Amplitude = value;
+        amplitudeChanged();
+    }
+
+    inline float period() const { return m_Period; }
+    void period(float value)
+    {
+        if (m_Period == value)
+        {
+            return;
+        }
+        m_Period = value;
+        periodChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ElasticInterpolatorBase& object)
+    {
+        m_EasingValue = object.m_EasingValue;
+        m_Amplitude = object.m_Amplitude;
+        m_Period = object.m_Period;
+        KeyFrameInterpolator::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case easingValuePropertyKey:
+                m_EasingValue = CoreUintType::deserialize(reader);
+                return true;
+            case amplitudePropertyKey:
+                m_Amplitude = CoreDoubleType::deserialize(reader);
+                return true;
+            case periodPropertyKey:
+                m_Period = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return KeyFrameInterpolator::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void easingValueChanged() {}
+    virtual void amplitudeChanged() {}
+    virtual void periodChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/entry_state_base.hpp b/include/rive/generated/animation/entry_state_base.hpp
new file mode 100644
index 0000000..69b913a
--- /dev/null
+++ b/include/rive/generated/animation/entry_state_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_ENTRY_STATE_BASE_HPP_
+#define _RIVE_ENTRY_STATE_BASE_HPP_
+#include "rive/animation/layer_state.hpp"
+namespace rive
+{
+class EntryStateBase : public LayerState
+{
+protected:
+    typedef LayerState Super;
+
+public:
+    static const uint16_t typeKey = 63;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case EntryStateBase::typeKey:
+            case LayerStateBase::typeKey:
+            case StateMachineLayerComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/exit_state_base.hpp b/include/rive/generated/animation/exit_state_base.hpp
new file mode 100644
index 0000000..b69f10f
--- /dev/null
+++ b/include/rive/generated/animation/exit_state_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_EXIT_STATE_BASE_HPP_
+#define _RIVE_EXIT_STATE_BASE_HPP_
+#include "rive/animation/layer_state.hpp"
+namespace rive
+{
+class ExitStateBase : public LayerState
+{
+protected:
+    typedef LayerState Super;
+
+public:
+    static const uint16_t typeKey = 64;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ExitStateBase::typeKey:
+            case LayerStateBase::typeKey:
+            case StateMachineLayerComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/interpolating_keyframe_base.hpp b/include/rive/generated/animation/interpolating_keyframe_base.hpp
new file mode 100644
index 0000000..7e84216
--- /dev/null
+++ b/include/rive/generated/animation/interpolating_keyframe_base.hpp
@@ -0,0 +1,88 @@
+#ifndef _RIVE_INTERPOLATING_KEY_FRAME_BASE_HPP_
+#define _RIVE_INTERPOLATING_KEY_FRAME_BASE_HPP_
+#include "rive/animation/keyframe.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class InterpolatingKeyFrameBase : public KeyFrame
+{
+protected:
+    typedef KeyFrame Super;
+
+public:
+    static const uint16_t typeKey = 170;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case InterpolatingKeyFrameBase::typeKey:
+            case KeyFrameBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t interpolationTypePropertyKey = 68;
+    static const uint16_t interpolatorIdPropertyKey = 69;
+
+private:
+    uint32_t m_InterpolationType = 0;
+    uint32_t m_InterpolatorId = -1;
+
+public:
+    inline uint32_t interpolationType() const { return m_InterpolationType; }
+    void interpolationType(uint32_t value)
+    {
+        if (m_InterpolationType == value)
+        {
+            return;
+        }
+        m_InterpolationType = value;
+        interpolationTypeChanged();
+    }
+
+    inline uint32_t interpolatorId() const { return m_InterpolatorId; }
+    void interpolatorId(uint32_t value)
+    {
+        if (m_InterpolatorId == value)
+        {
+            return;
+        }
+        m_InterpolatorId = value;
+        interpolatorIdChanged();
+    }
+
+    void copy(const InterpolatingKeyFrameBase& object)
+    {
+        m_InterpolationType = object.m_InterpolationType;
+        m_InterpolatorId = object.m_InterpolatorId;
+        KeyFrame::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case interpolationTypePropertyKey:
+                m_InterpolationType = CoreUintType::deserialize(reader);
+                return true;
+            case interpolatorIdPropertyKey:
+                m_InterpolatorId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return KeyFrame::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void interpolationTypeChanged() {}
+    virtual void interpolatorIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/keyed_object_base.hpp b/include/rive/generated/animation/keyed_object_base.hpp
new file mode 100644
index 0000000..37d2f51
--- /dev/null
+++ b/include/rive/generated/animation/keyed_object_base.hpp
@@ -0,0 +1,66 @@
+#ifndef _RIVE_KEYED_OBJECT_BASE_HPP_
+#define _RIVE_KEYED_OBJECT_BASE_HPP_
+#include "rive/core.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class KeyedObjectBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 25;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case KeyedObjectBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t objectIdPropertyKey = 51;
+
+private:
+    uint32_t m_ObjectId = 0;
+
+public:
+    inline uint32_t objectId() const { return m_ObjectId; }
+    void objectId(uint32_t value)
+    {
+        if (m_ObjectId == value)
+        {
+            return;
+        }
+        m_ObjectId = value;
+        objectIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const KeyedObjectBase& object) { m_ObjectId = object.m_ObjectId; }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case objectIdPropertyKey:
+                m_ObjectId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return false;
+    }
+
+protected:
+    virtual void objectIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/keyed_property_base.hpp b/include/rive/generated/animation/keyed_property_base.hpp
new file mode 100644
index 0000000..6a111eb
--- /dev/null
+++ b/include/rive/generated/animation/keyed_property_base.hpp
@@ -0,0 +1,66 @@
+#ifndef _RIVE_KEYED_PROPERTY_BASE_HPP_
+#define _RIVE_KEYED_PROPERTY_BASE_HPP_
+#include "rive/core.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class KeyedPropertyBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 26;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case KeyedPropertyBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t propertyKeyPropertyKey = 53;
+
+private:
+    uint32_t m_PropertyKey = Core::invalidPropertyKey;
+
+public:
+    inline uint32_t propertyKey() const { return m_PropertyKey; }
+    void propertyKey(uint32_t value)
+    {
+        if (m_PropertyKey == value)
+        {
+            return;
+        }
+        m_PropertyKey = value;
+        propertyKeyChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const KeyedPropertyBase& object) { m_PropertyKey = object.m_PropertyKey; }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case propertyKeyPropertyKey:
+                m_PropertyKey = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return false;
+    }
+
+protected:
+    virtual void propertyKeyChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/keyframe_base.hpp b/include/rive/generated/animation/keyframe_base.hpp
new file mode 100644
index 0000000..9e8fa47
--- /dev/null
+++ b/include/rive/generated/animation/keyframe_base.hpp
@@ -0,0 +1,65 @@
+#ifndef _RIVE_KEY_FRAME_BASE_HPP_
+#define _RIVE_KEY_FRAME_BASE_HPP_
+#include "rive/core.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class KeyFrameBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 29;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case KeyFrameBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t framePropertyKey = 67;
+
+private:
+    uint32_t m_Frame = 0;
+
+public:
+    inline uint32_t frame() const { return m_Frame; }
+    void frame(uint32_t value)
+    {
+        if (m_Frame == value)
+        {
+            return;
+        }
+        m_Frame = value;
+        frameChanged();
+    }
+
+    void copy(const KeyFrameBase& object) { m_Frame = object.m_Frame; }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case framePropertyKey:
+                m_Frame = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return false;
+    }
+
+protected:
+    virtual void frameChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/keyframe_bool_base.hpp b/include/rive/generated/animation/keyframe_bool_base.hpp
new file mode 100644
index 0000000..ffe12be
--- /dev/null
+++ b/include/rive/generated/animation/keyframe_bool_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_KEY_FRAME_BOOL_BASE_HPP_
+#define _RIVE_KEY_FRAME_BOOL_BASE_HPP_
+#include "rive/animation/interpolating_keyframe.hpp"
+#include "rive/core/field_types/core_bool_type.hpp"
+namespace rive
+{
+class KeyFrameBoolBase : public InterpolatingKeyFrame
+{
+protected:
+    typedef InterpolatingKeyFrame Super;
+
+public:
+    static const uint16_t typeKey = 84;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case KeyFrameBoolBase::typeKey:
+            case InterpolatingKeyFrameBase::typeKey:
+            case KeyFrameBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t valuePropertyKey = 181;
+
+private:
+    bool m_Value = false;
+
+public:
+    inline bool value() const { return m_Value; }
+    void value(bool value)
+    {
+        if (m_Value == value)
+        {
+            return;
+        }
+        m_Value = value;
+        valueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const KeyFrameBoolBase& object)
+    {
+        m_Value = object.m_Value;
+        InterpolatingKeyFrame::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case valuePropertyKey:
+                m_Value = CoreBoolType::deserialize(reader);
+                return true;
+        }
+        return InterpolatingKeyFrame::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void valueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/keyframe_callback_base.hpp b/include/rive/generated/animation/keyframe_callback_base.hpp
new file mode 100644
index 0000000..066256e
--- /dev/null
+++ b/include/rive/generated/animation/keyframe_callback_base.hpp
@@ -0,0 +1,36 @@
+#ifndef _RIVE_KEY_FRAME_CALLBACK_BASE_HPP_
+#define _RIVE_KEY_FRAME_CALLBACK_BASE_HPP_
+#include "rive/animation/keyframe.hpp"
+namespace rive
+{
+class KeyFrameCallbackBase : public KeyFrame
+{
+protected:
+    typedef KeyFrame Super;
+
+public:
+    static const uint16_t typeKey = 171;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case KeyFrameCallbackBase::typeKey:
+            case KeyFrameBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/keyframe_color_base.hpp b/include/rive/generated/animation/keyframe_color_base.hpp
new file mode 100644
index 0000000..4814a47
--- /dev/null
+++ b/include/rive/generated/animation/keyframe_color_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_KEY_FRAME_COLOR_BASE_HPP_
+#define _RIVE_KEY_FRAME_COLOR_BASE_HPP_
+#include "rive/animation/interpolating_keyframe.hpp"
+#include "rive/core/field_types/core_color_type.hpp"
+namespace rive
+{
+class KeyFrameColorBase : public InterpolatingKeyFrame
+{
+protected:
+    typedef InterpolatingKeyFrame Super;
+
+public:
+    static const uint16_t typeKey = 37;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case KeyFrameColorBase::typeKey:
+            case InterpolatingKeyFrameBase::typeKey:
+            case KeyFrameBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t valuePropertyKey = 88;
+
+private:
+    int m_Value = 0;
+
+public:
+    inline int value() const { return m_Value; }
+    void value(int value)
+    {
+        if (m_Value == value)
+        {
+            return;
+        }
+        m_Value = value;
+        valueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const KeyFrameColorBase& object)
+    {
+        m_Value = object.m_Value;
+        InterpolatingKeyFrame::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case valuePropertyKey:
+                m_Value = CoreColorType::deserialize(reader);
+                return true;
+        }
+        return InterpolatingKeyFrame::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void valueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/keyframe_double_base.hpp b/include/rive/generated/animation/keyframe_double_base.hpp
new file mode 100644
index 0000000..0ee26f0
--- /dev/null
+++ b/include/rive/generated/animation/keyframe_double_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_KEY_FRAME_DOUBLE_BASE_HPP_
+#define _RIVE_KEY_FRAME_DOUBLE_BASE_HPP_
+#include "rive/animation/interpolating_keyframe.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class KeyFrameDoubleBase : public InterpolatingKeyFrame
+{
+protected:
+    typedef InterpolatingKeyFrame Super;
+
+public:
+    static const uint16_t typeKey = 30;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case KeyFrameDoubleBase::typeKey:
+            case InterpolatingKeyFrameBase::typeKey:
+            case KeyFrameBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t valuePropertyKey = 70;
+
+private:
+    float m_Value = 0.0f;
+
+public:
+    inline float value() const { return m_Value; }
+    void value(float value)
+    {
+        if (m_Value == value)
+        {
+            return;
+        }
+        m_Value = value;
+        valueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const KeyFrameDoubleBase& object)
+    {
+        m_Value = object.m_Value;
+        InterpolatingKeyFrame::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case valuePropertyKey:
+                m_Value = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return InterpolatingKeyFrame::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void valueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/keyframe_id_base.hpp b/include/rive/generated/animation/keyframe_id_base.hpp
new file mode 100644
index 0000000..229b1a3
--- /dev/null
+++ b/include/rive/generated/animation/keyframe_id_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_KEY_FRAME_ID_BASE_HPP_
+#define _RIVE_KEY_FRAME_ID_BASE_HPP_
+#include "rive/animation/interpolating_keyframe.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class KeyFrameIdBase : public InterpolatingKeyFrame
+{
+protected:
+    typedef InterpolatingKeyFrame Super;
+
+public:
+    static const uint16_t typeKey = 50;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case KeyFrameIdBase::typeKey:
+            case InterpolatingKeyFrameBase::typeKey:
+            case KeyFrameBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t valuePropertyKey = 122;
+
+private:
+    uint32_t m_Value = -1;
+
+public:
+    inline uint32_t value() const { return m_Value; }
+    void value(uint32_t value)
+    {
+        if (m_Value == value)
+        {
+            return;
+        }
+        m_Value = value;
+        valueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const KeyFrameIdBase& object)
+    {
+        m_Value = object.m_Value;
+        InterpolatingKeyFrame::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case valuePropertyKey:
+                m_Value = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return InterpolatingKeyFrame::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void valueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/keyframe_interpolator_base.hpp b/include/rive/generated/animation/keyframe_interpolator_base.hpp
new file mode 100644
index 0000000..f3e5773
--- /dev/null
+++ b/include/rive/generated/animation/keyframe_interpolator_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_KEY_FRAME_INTERPOLATOR_BASE_HPP_
+#define _RIVE_KEY_FRAME_INTERPOLATOR_BASE_HPP_
+#include "rive/core.hpp"
+namespace rive
+{
+class KeyFrameInterpolatorBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 175;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case KeyFrameInterpolatorBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    void copy(const KeyFrameInterpolatorBase& object) {}
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override { return false; }
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/keyframe_string_base.hpp b/include/rive/generated/animation/keyframe_string_base.hpp
new file mode 100644
index 0000000..e1a54c7
--- /dev/null
+++ b/include/rive/generated/animation/keyframe_string_base.hpp
@@ -0,0 +1,73 @@
+#ifndef _RIVE_KEY_FRAME_STRING_BASE_HPP_
+#define _RIVE_KEY_FRAME_STRING_BASE_HPP_
+#include <string>
+#include "rive/animation/interpolating_keyframe.hpp"
+#include "rive/core/field_types/core_string_type.hpp"
+namespace rive
+{
+class KeyFrameStringBase : public InterpolatingKeyFrame
+{
+protected:
+    typedef InterpolatingKeyFrame Super;
+
+public:
+    static const uint16_t typeKey = 142;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case KeyFrameStringBase::typeKey:
+            case InterpolatingKeyFrameBase::typeKey:
+            case KeyFrameBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t valuePropertyKey = 280;
+
+private:
+    std::string m_Value = "";
+
+public:
+    inline const std::string& value() const { return m_Value; }
+    void value(std::string value)
+    {
+        if (m_Value == value)
+        {
+            return;
+        }
+        m_Value = value;
+        valueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const KeyFrameStringBase& object)
+    {
+        m_Value = object.m_Value;
+        InterpolatingKeyFrame::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case valuePropertyKey:
+                m_Value = CoreStringType::deserialize(reader);
+                return true;
+        }
+        return InterpolatingKeyFrame::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void valueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/keyframe_uint_base.hpp b/include/rive/generated/animation/keyframe_uint_base.hpp
new file mode 100644
index 0000000..cdf0bd1
--- /dev/null
+++ b/include/rive/generated/animation/keyframe_uint_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_KEY_FRAME_UINT_BASE_HPP_
+#define _RIVE_KEY_FRAME_UINT_BASE_HPP_
+#include "rive/animation/interpolating_keyframe.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class KeyFrameUintBase : public InterpolatingKeyFrame
+{
+protected:
+    typedef InterpolatingKeyFrame Super;
+
+public:
+    static const uint16_t typeKey = 450;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case KeyFrameUintBase::typeKey:
+            case InterpolatingKeyFrameBase::typeKey:
+            case KeyFrameBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t valuePropertyKey = 631;
+
+private:
+    uint32_t m_Value = 0;
+
+public:
+    inline uint32_t value() const { return m_Value; }
+    void value(uint32_t value)
+    {
+        if (m_Value == value)
+        {
+            return;
+        }
+        m_Value = value;
+        valueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const KeyFrameUintBase& object)
+    {
+        m_Value = object.m_Value;
+        InterpolatingKeyFrame::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case valuePropertyKey:
+                m_Value = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return InterpolatingKeyFrame::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void valueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/layer_state_base.hpp b/include/rive/generated/animation/layer_state_base.hpp
new file mode 100644
index 0000000..3c4cfbe
--- /dev/null
+++ b/include/rive/generated/animation/layer_state_base.hpp
@@ -0,0 +1,70 @@
+#ifndef _RIVE_LAYER_STATE_BASE_HPP_
+#define _RIVE_LAYER_STATE_BASE_HPP_
+#include "rive/animation/state_machine_layer_component.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class LayerStateBase : public StateMachineLayerComponent
+{
+protected:
+    typedef StateMachineLayerComponent Super;
+
+public:
+    static const uint16_t typeKey = 60;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case LayerStateBase::typeKey:
+            case StateMachineLayerComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t flagsPropertyKey = 536;
+
+private:
+    uint32_t m_Flags = 0;
+
+public:
+    inline uint32_t flags() const { return m_Flags; }
+    void flags(uint32_t value)
+    {
+        if (m_Flags == value)
+        {
+            return;
+        }
+        m_Flags = value;
+        flagsChanged();
+    }
+
+    void copy(const LayerStateBase& object)
+    {
+        m_Flags = object.m_Flags;
+        StateMachineLayerComponent::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case flagsPropertyKey:
+                m_Flags = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return StateMachineLayerComponent::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void flagsChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/linear_animation_base.hpp b/include/rive/generated/animation/linear_animation_base.hpp
new file mode 100644
index 0000000..15bf635
--- /dev/null
+++ b/include/rive/generated/animation/linear_animation_base.hpp
@@ -0,0 +1,199 @@
+#ifndef _RIVE_LINEAR_ANIMATION_BASE_HPP_
+#define _RIVE_LINEAR_ANIMATION_BASE_HPP_
+#include "rive/animation/animation.hpp"
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class LinearAnimationBase : public Animation
+{
+protected:
+    typedef Animation Super;
+
+public:
+    static const uint16_t typeKey = 31;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case LinearAnimationBase::typeKey:
+            case AnimationBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t fpsPropertyKey = 56;
+    static const uint16_t durationPropertyKey = 57;
+    static const uint16_t speedPropertyKey = 58;
+    static const uint16_t loopValuePropertyKey = 59;
+    static const uint16_t workStartPropertyKey = 60;
+    static const uint16_t workEndPropertyKey = 61;
+    static const uint16_t enableWorkAreaPropertyKey = 62;
+    static const uint16_t quantizePropertyKey = 376;
+
+private:
+    uint32_t m_Fps = 60;
+    uint32_t m_Duration = 60;
+    float m_Speed = 1.0f;
+    uint32_t m_LoopValue = 0;
+    uint32_t m_WorkStart = -1;
+    uint32_t m_WorkEnd = -1;
+    bool m_EnableWorkArea = false;
+    bool m_Quantize = false;
+
+public:
+    inline uint32_t fps() const { return m_Fps; }
+    void fps(uint32_t value)
+    {
+        if (m_Fps == value)
+        {
+            return;
+        }
+        m_Fps = value;
+        fpsChanged();
+    }
+
+    inline uint32_t duration() const { return m_Duration; }
+    void duration(uint32_t value)
+    {
+        if (m_Duration == value)
+        {
+            return;
+        }
+        m_Duration = value;
+        durationChanged();
+    }
+
+    inline float speed() const { return m_Speed; }
+    void speed(float value)
+    {
+        if (m_Speed == value)
+        {
+            return;
+        }
+        m_Speed = value;
+        speedChanged();
+    }
+
+    inline uint32_t loopValue() const { return m_LoopValue; }
+    void loopValue(uint32_t value)
+    {
+        if (m_LoopValue == value)
+        {
+            return;
+        }
+        m_LoopValue = value;
+        loopValueChanged();
+    }
+
+    inline uint32_t workStart() const { return m_WorkStart; }
+    void workStart(uint32_t value)
+    {
+        if (m_WorkStart == value)
+        {
+            return;
+        }
+        m_WorkStart = value;
+        workStartChanged();
+    }
+
+    inline uint32_t workEnd() const { return m_WorkEnd; }
+    void workEnd(uint32_t value)
+    {
+        if (m_WorkEnd == value)
+        {
+            return;
+        }
+        m_WorkEnd = value;
+        workEndChanged();
+    }
+
+    inline bool enableWorkArea() const { return m_EnableWorkArea; }
+    void enableWorkArea(bool value)
+    {
+        if (m_EnableWorkArea == value)
+        {
+            return;
+        }
+        m_EnableWorkArea = value;
+        enableWorkAreaChanged();
+    }
+
+    inline bool quantize() const { return m_Quantize; }
+    void quantize(bool value)
+    {
+        if (m_Quantize == value)
+        {
+            return;
+        }
+        m_Quantize = value;
+        quantizeChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const LinearAnimationBase& object)
+    {
+        m_Fps = object.m_Fps;
+        m_Duration = object.m_Duration;
+        m_Speed = object.m_Speed;
+        m_LoopValue = object.m_LoopValue;
+        m_WorkStart = object.m_WorkStart;
+        m_WorkEnd = object.m_WorkEnd;
+        m_EnableWorkArea = object.m_EnableWorkArea;
+        m_Quantize = object.m_Quantize;
+        Animation::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case fpsPropertyKey:
+                m_Fps = CoreUintType::deserialize(reader);
+                return true;
+            case durationPropertyKey:
+                m_Duration = CoreUintType::deserialize(reader);
+                return true;
+            case speedPropertyKey:
+                m_Speed = CoreDoubleType::deserialize(reader);
+                return true;
+            case loopValuePropertyKey:
+                m_LoopValue = CoreUintType::deserialize(reader);
+                return true;
+            case workStartPropertyKey:
+                m_WorkStart = CoreUintType::deserialize(reader);
+                return true;
+            case workEndPropertyKey:
+                m_WorkEnd = CoreUintType::deserialize(reader);
+                return true;
+            case enableWorkAreaPropertyKey:
+                m_EnableWorkArea = CoreBoolType::deserialize(reader);
+                return true;
+            case quantizePropertyKey:
+                m_Quantize = CoreBoolType::deserialize(reader);
+                return true;
+        }
+        return Animation::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void fpsChanged() {}
+    virtual void durationChanged() {}
+    virtual void speedChanged() {}
+    virtual void loopValueChanged() {}
+    virtual void workStartChanged() {}
+    virtual void workEndChanged() {}
+    virtual void enableWorkAreaChanged() {}
+    virtual void quantizeChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/listener_action_base.hpp b/include/rive/generated/animation/listener_action_base.hpp
new file mode 100644
index 0000000..d10a5f1
--- /dev/null
+++ b/include/rive/generated/animation/listener_action_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_LISTENER_ACTION_BASE_HPP_
+#define _RIVE_LISTENER_ACTION_BASE_HPP_
+#include "rive/core.hpp"
+namespace rive
+{
+class ListenerActionBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 125;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ListenerActionBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    void copy(const ListenerActionBase& object) {}
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override { return false; }
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/listener_align_target_base.hpp b/include/rive/generated/animation/listener_align_target_base.hpp
new file mode 100644
index 0000000..b7e5800
--- /dev/null
+++ b/include/rive/generated/animation/listener_align_target_base.hpp
@@ -0,0 +1,90 @@
+#ifndef _RIVE_LISTENER_ALIGN_TARGET_BASE_HPP_
+#define _RIVE_LISTENER_ALIGN_TARGET_BASE_HPP_
+#include "rive/animation/listener_action.hpp"
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class ListenerAlignTargetBase : public ListenerAction
+{
+protected:
+    typedef ListenerAction Super;
+
+public:
+    static const uint16_t typeKey = 126;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ListenerAlignTargetBase::typeKey:
+            case ListenerActionBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t targetIdPropertyKey = 240;
+    static const uint16_t preserveOffsetPropertyKey = 541;
+
+private:
+    uint32_t m_TargetId = 0;
+    bool m_PreserveOffset = false;
+
+public:
+    inline uint32_t targetId() const { return m_TargetId; }
+    void targetId(uint32_t value)
+    {
+        if (m_TargetId == value)
+        {
+            return;
+        }
+        m_TargetId = value;
+        targetIdChanged();
+    }
+
+    inline bool preserveOffset() const { return m_PreserveOffset; }
+    void preserveOffset(bool value)
+    {
+        if (m_PreserveOffset == value)
+        {
+            return;
+        }
+        m_PreserveOffset = value;
+        preserveOffsetChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ListenerAlignTargetBase& object)
+    {
+        m_TargetId = object.m_TargetId;
+        m_PreserveOffset = object.m_PreserveOffset;
+        ListenerAction::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case targetIdPropertyKey:
+                m_TargetId = CoreUintType::deserialize(reader);
+                return true;
+            case preserveOffsetPropertyKey:
+                m_PreserveOffset = CoreBoolType::deserialize(reader);
+                return true;
+        }
+        return ListenerAction::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void targetIdChanged() {}
+    virtual void preserveOffsetChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/listener_bool_change_base.hpp b/include/rive/generated/animation/listener_bool_change_base.hpp
new file mode 100644
index 0000000..c7260b9
--- /dev/null
+++ b/include/rive/generated/animation/listener_bool_change_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_LISTENER_BOOL_CHANGE_BASE_HPP_
+#define _RIVE_LISTENER_BOOL_CHANGE_BASE_HPP_
+#include "rive/animation/listener_input_change.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class ListenerBoolChangeBase : public ListenerInputChange
+{
+protected:
+    typedef ListenerInputChange Super;
+
+public:
+    static const uint16_t typeKey = 117;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ListenerBoolChangeBase::typeKey:
+            case ListenerInputChangeBase::typeKey:
+            case ListenerActionBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t valuePropertyKey = 228;
+
+private:
+    uint32_t m_Value = 1;
+
+public:
+    inline uint32_t value() const { return m_Value; }
+    void value(uint32_t value)
+    {
+        if (m_Value == value)
+        {
+            return;
+        }
+        m_Value = value;
+        valueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ListenerBoolChangeBase& object)
+    {
+        m_Value = object.m_Value;
+        ListenerInputChange::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case valuePropertyKey:
+                m_Value = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return ListenerInputChange::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void valueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/listener_fire_event_base.hpp b/include/rive/generated/animation/listener_fire_event_base.hpp
new file mode 100644
index 0000000..33280cd
--- /dev/null
+++ b/include/rive/generated/animation/listener_fire_event_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_LISTENER_FIRE_EVENT_BASE_HPP_
+#define _RIVE_LISTENER_FIRE_EVENT_BASE_HPP_
+#include "rive/animation/listener_action.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class ListenerFireEventBase : public ListenerAction
+{
+protected:
+    typedef ListenerAction Super;
+
+public:
+    static const uint16_t typeKey = 168;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ListenerFireEventBase::typeKey:
+            case ListenerActionBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t eventIdPropertyKey = 389;
+
+private:
+    uint32_t m_EventId = -1;
+
+public:
+    inline uint32_t eventId() const { return m_EventId; }
+    void eventId(uint32_t value)
+    {
+        if (m_EventId == value)
+        {
+            return;
+        }
+        m_EventId = value;
+        eventIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ListenerFireEventBase& object)
+    {
+        m_EventId = object.m_EventId;
+        ListenerAction::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case eventIdPropertyKey:
+                m_EventId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return ListenerAction::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void eventIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/listener_input_change_base.hpp b/include/rive/generated/animation/listener_input_change_base.hpp
new file mode 100644
index 0000000..835bb76
--- /dev/null
+++ b/include/rive/generated/animation/listener_input_change_base.hpp
@@ -0,0 +1,88 @@
+#ifndef _RIVE_LISTENER_INPUT_CHANGE_BASE_HPP_
+#define _RIVE_LISTENER_INPUT_CHANGE_BASE_HPP_
+#include "rive/animation/listener_action.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class ListenerInputChangeBase : public ListenerAction
+{
+protected:
+    typedef ListenerAction Super;
+
+public:
+    static const uint16_t typeKey = 116;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ListenerInputChangeBase::typeKey:
+            case ListenerActionBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t inputIdPropertyKey = 227;
+    static const uint16_t nestedInputIdPropertyKey = 400;
+
+private:
+    uint32_t m_InputId = -1;
+    uint32_t m_NestedInputId = -1;
+
+public:
+    inline uint32_t inputId() const { return m_InputId; }
+    void inputId(uint32_t value)
+    {
+        if (m_InputId == value)
+        {
+            return;
+        }
+        m_InputId = value;
+        inputIdChanged();
+    }
+
+    inline uint32_t nestedInputId() const { return m_NestedInputId; }
+    void nestedInputId(uint32_t value)
+    {
+        if (m_NestedInputId == value)
+        {
+            return;
+        }
+        m_NestedInputId = value;
+        nestedInputIdChanged();
+    }
+
+    void copy(const ListenerInputChangeBase& object)
+    {
+        m_InputId = object.m_InputId;
+        m_NestedInputId = object.m_NestedInputId;
+        ListenerAction::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case inputIdPropertyKey:
+                m_InputId = CoreUintType::deserialize(reader);
+                return true;
+            case nestedInputIdPropertyKey:
+                m_NestedInputId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return ListenerAction::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void inputIdChanged() {}
+    virtual void nestedInputIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/listener_number_change_base.hpp b/include/rive/generated/animation/listener_number_change_base.hpp
new file mode 100644
index 0000000..b75949f
--- /dev/null
+++ b/include/rive/generated/animation/listener_number_change_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_LISTENER_NUMBER_CHANGE_BASE_HPP_
+#define _RIVE_LISTENER_NUMBER_CHANGE_BASE_HPP_
+#include "rive/animation/listener_input_change.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class ListenerNumberChangeBase : public ListenerInputChange
+{
+protected:
+    typedef ListenerInputChange Super;
+
+public:
+    static const uint16_t typeKey = 118;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ListenerNumberChangeBase::typeKey:
+            case ListenerInputChangeBase::typeKey:
+            case ListenerActionBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t valuePropertyKey = 229;
+
+private:
+    float m_Value = 0.0f;
+
+public:
+    inline float value() const { return m_Value; }
+    void value(float value)
+    {
+        if (m_Value == value)
+        {
+            return;
+        }
+        m_Value = value;
+        valueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ListenerNumberChangeBase& object)
+    {
+        m_Value = object.m_Value;
+        ListenerInputChange::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case valuePropertyKey:
+                m_Value = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return ListenerInputChange::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void valueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/listener_trigger_change_base.hpp b/include/rive/generated/animation/listener_trigger_change_base.hpp
new file mode 100644
index 0000000..a8306db
--- /dev/null
+++ b/include/rive/generated/animation/listener_trigger_change_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_LISTENER_TRIGGER_CHANGE_BASE_HPP_
+#define _RIVE_LISTENER_TRIGGER_CHANGE_BASE_HPP_
+#include "rive/animation/listener_input_change.hpp"
+namespace rive
+{
+class ListenerTriggerChangeBase : public ListenerInputChange
+{
+protected:
+    typedef ListenerInputChange Super;
+
+public:
+    static const uint16_t typeKey = 115;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ListenerTriggerChangeBase::typeKey:
+            case ListenerInputChangeBase::typeKey:
+            case ListenerActionBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/listener_viewmodel_change_base.hpp b/include/rive/generated/animation/listener_viewmodel_change_base.hpp
new file mode 100644
index 0000000..4171141
--- /dev/null
+++ b/include/rive/generated/animation/listener_viewmodel_change_base.hpp
@@ -0,0 +1,36 @@
+#ifndef _RIVE_LISTENER_VIEW_MODEL_CHANGE_BASE_HPP_
+#define _RIVE_LISTENER_VIEW_MODEL_CHANGE_BASE_HPP_
+#include "rive/animation/listener_action.hpp"
+namespace rive
+{
+class ListenerViewModelChangeBase : public ListenerAction
+{
+protected:
+    typedef ListenerAction Super;
+
+public:
+    static const uint16_t typeKey = 487;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ListenerViewModelChangeBase::typeKey:
+            case ListenerActionBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/nested_bool_base.hpp b/include/rive/generated/animation/nested_bool_base.hpp
new file mode 100644
index 0000000..8c015f5
--- /dev/null
+++ b/include/rive/generated/animation/nested_bool_base.hpp
@@ -0,0 +1,64 @@
+#ifndef _RIVE_NESTED_BOOL_BASE_HPP_
+#define _RIVE_NESTED_BOOL_BASE_HPP_
+#include "rive/animation/nested_input.hpp"
+#include "rive/core/field_types/core_bool_type.hpp"
+namespace rive
+{
+class NestedBoolBase : public NestedInput
+{
+protected:
+    typedef NestedInput Super;
+
+public:
+    static const uint16_t typeKey = 123;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case NestedBoolBase::typeKey:
+            case NestedInputBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t nestedValuePropertyKey = 238;
+
+private:
+    bool m_NestedValue = false;
+
+public:
+    virtual bool nestedValue() const { return m_NestedValue; }
+    virtual void nestedValue(bool value) = 0;
+
+    Core* clone() const override;
+    void copy(const NestedBoolBase& object)
+    {
+        m_NestedValue = object.m_NestedValue;
+        NestedInput::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case nestedValuePropertyKey:
+                m_NestedValue = CoreBoolType::deserialize(reader);
+                return true;
+        }
+        return NestedInput::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void nestedValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/nested_input_base.hpp b/include/rive/generated/animation/nested_input_base.hpp
new file mode 100644
index 0000000..40cab82
--- /dev/null
+++ b/include/rive/generated/animation/nested_input_base.hpp
@@ -0,0 +1,70 @@
+#ifndef _RIVE_NESTED_INPUT_BASE_HPP_
+#define _RIVE_NESTED_INPUT_BASE_HPP_
+#include "rive/component.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class NestedInputBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 121;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case NestedInputBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t inputIdPropertyKey = 237;
+
+private:
+    uint32_t m_InputId = -1;
+
+public:
+    inline uint32_t inputId() const { return m_InputId; }
+    void inputId(uint32_t value)
+    {
+        if (m_InputId == value)
+        {
+            return;
+        }
+        m_InputId = value;
+        inputIdChanged();
+    }
+
+    void copy(const NestedInputBase& object)
+    {
+        m_InputId = object.m_InputId;
+        Component::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case inputIdPropertyKey:
+                m_InputId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return Component::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void inputIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/nested_linear_animation_base.hpp b/include/rive/generated/animation/nested_linear_animation_base.hpp
new file mode 100644
index 0000000..e3875e4
--- /dev/null
+++ b/include/rive/generated/animation/nested_linear_animation_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_NESTED_LINEAR_ANIMATION_BASE_HPP_
+#define _RIVE_NESTED_LINEAR_ANIMATION_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/nested_animation.hpp"
+namespace rive
+{
+class NestedLinearAnimationBase : public NestedAnimation
+{
+protected:
+    typedef NestedAnimation Super;
+
+public:
+    static const uint16_t typeKey = 97;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case NestedLinearAnimationBase::typeKey:
+            case NestedAnimationBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t mixPropertyKey = 200;
+
+private:
+    float m_Mix = 1.0f;
+
+public:
+    inline float mix() const { return m_Mix; }
+    void mix(float value)
+    {
+        if (m_Mix == value)
+        {
+            return;
+        }
+        m_Mix = value;
+        mixChanged();
+    }
+
+    void copy(const NestedLinearAnimationBase& object)
+    {
+        m_Mix = object.m_Mix;
+        NestedAnimation::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case mixPropertyKey:
+                m_Mix = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return NestedAnimation::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void mixChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/nested_number_base.hpp b/include/rive/generated/animation/nested_number_base.hpp
new file mode 100644
index 0000000..82bc0c9
--- /dev/null
+++ b/include/rive/generated/animation/nested_number_base.hpp
@@ -0,0 +1,64 @@
+#ifndef _RIVE_NESTED_NUMBER_BASE_HPP_
+#define _RIVE_NESTED_NUMBER_BASE_HPP_
+#include "rive/animation/nested_input.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class NestedNumberBase : public NestedInput
+{
+protected:
+    typedef NestedInput Super;
+
+public:
+    static const uint16_t typeKey = 124;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case NestedNumberBase::typeKey:
+            case NestedInputBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t nestedValuePropertyKey = 239;
+
+private:
+    float m_NestedValue = 0.0f;
+
+public:
+    virtual float nestedValue() const { return m_NestedValue; }
+    virtual void nestedValue(float value) = 0;
+
+    Core* clone() const override;
+    void copy(const NestedNumberBase& object)
+    {
+        m_NestedValue = object.m_NestedValue;
+        NestedInput::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case nestedValuePropertyKey:
+                m_NestedValue = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return NestedInput::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void nestedValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/nested_remap_animation_base.hpp b/include/rive/generated/animation/nested_remap_animation_base.hpp
new file mode 100644
index 0000000..5050aff
--- /dev/null
+++ b/include/rive/generated/animation/nested_remap_animation_base.hpp
@@ -0,0 +1,74 @@
+#ifndef _RIVE_NESTED_REMAP_ANIMATION_BASE_HPP_
+#define _RIVE_NESTED_REMAP_ANIMATION_BASE_HPP_
+#include "rive/animation/nested_linear_animation.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class NestedRemapAnimationBase : public NestedLinearAnimation
+{
+protected:
+    typedef NestedLinearAnimation Super;
+
+public:
+    static const uint16_t typeKey = 98;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case NestedRemapAnimationBase::typeKey:
+            case NestedLinearAnimationBase::typeKey:
+            case NestedAnimationBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t timePropertyKey = 202;
+
+private:
+    float m_Time = 0.0f;
+
+public:
+    inline float time() const { return m_Time; }
+    void time(float value)
+    {
+        if (m_Time == value)
+        {
+            return;
+        }
+        m_Time = value;
+        timeChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const NestedRemapAnimationBase& object)
+    {
+        m_Time = object.m_Time;
+        NestedLinearAnimation::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case timePropertyKey:
+                m_Time = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return NestedLinearAnimation::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void timeChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/nested_simple_animation_base.hpp b/include/rive/generated/animation/nested_simple_animation_base.hpp
new file mode 100644
index 0000000..e9306d9
--- /dev/null
+++ b/include/rive/generated/animation/nested_simple_animation_base.hpp
@@ -0,0 +1,93 @@
+#ifndef _RIVE_NESTED_SIMPLE_ANIMATION_BASE_HPP_
+#define _RIVE_NESTED_SIMPLE_ANIMATION_BASE_HPP_
+#include "rive/animation/nested_linear_animation.hpp"
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class NestedSimpleAnimationBase : public NestedLinearAnimation
+{
+protected:
+    typedef NestedLinearAnimation Super;
+
+public:
+    static const uint16_t typeKey = 96;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case NestedSimpleAnimationBase::typeKey:
+            case NestedLinearAnimationBase::typeKey:
+            case NestedAnimationBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t speedPropertyKey = 199;
+    static const uint16_t isPlayingPropertyKey = 201;
+
+private:
+    float m_Speed = 1.0f;
+    bool m_IsPlaying = false;
+
+public:
+    inline float speed() const { return m_Speed; }
+    void speed(float value)
+    {
+        if (m_Speed == value)
+        {
+            return;
+        }
+        m_Speed = value;
+        speedChanged();
+    }
+
+    inline bool isPlaying() const { return m_IsPlaying; }
+    void isPlaying(bool value)
+    {
+        if (m_IsPlaying == value)
+        {
+            return;
+        }
+        m_IsPlaying = value;
+        isPlayingChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const NestedSimpleAnimationBase& object)
+    {
+        m_Speed = object.m_Speed;
+        m_IsPlaying = object.m_IsPlaying;
+        NestedLinearAnimation::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case speedPropertyKey:
+                m_Speed = CoreDoubleType::deserialize(reader);
+                return true;
+            case isPlayingPropertyKey:
+                m_IsPlaying = CoreBoolType::deserialize(reader);
+                return true;
+        }
+        return NestedLinearAnimation::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void speedChanged() {}
+    virtual void isPlayingChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/nested_state_machine_base.hpp b/include/rive/generated/animation/nested_state_machine_base.hpp
new file mode 100644
index 0000000..6a72d26
--- /dev/null
+++ b/include/rive/generated/animation/nested_state_machine_base.hpp
@@ -0,0 +1,38 @@
+#ifndef _RIVE_NESTED_STATE_MACHINE_BASE_HPP_
+#define _RIVE_NESTED_STATE_MACHINE_BASE_HPP_
+#include "rive/nested_animation.hpp"
+namespace rive
+{
+class NestedStateMachineBase : public NestedAnimation
+{
+protected:
+    typedef NestedAnimation Super;
+
+public:
+    static const uint16_t typeKey = 95;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case NestedStateMachineBase::typeKey:
+            case NestedAnimationBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/nested_trigger_base.hpp b/include/rive/generated/animation/nested_trigger_base.hpp
new file mode 100644
index 0000000..6fd3a5d
--- /dev/null
+++ b/include/rive/generated/animation/nested_trigger_base.hpp
@@ -0,0 +1,43 @@
+#ifndef _RIVE_NESTED_TRIGGER_BASE_HPP_
+#define _RIVE_NESTED_TRIGGER_BASE_HPP_
+#include "rive/animation/nested_input.hpp"
+#include "rive/core/field_types/core_callback_type.hpp"
+namespace rive
+{
+class NestedTriggerBase : public NestedInput
+{
+protected:
+    typedef NestedInput Super;
+
+public:
+    static const uint16_t typeKey = 122;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case NestedTriggerBase::typeKey:
+            case NestedInputBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t firePropertyKey = 401;
+
+public:
+    virtual void fire(const CallbackData& value) = 0;
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/state_machine_base.hpp b/include/rive/generated/animation/state_machine_base.hpp
new file mode 100644
index 0000000..8b72ddb
--- /dev/null
+++ b/include/rive/generated/animation/state_machine_base.hpp
@@ -0,0 +1,36 @@
+#ifndef _RIVE_STATE_MACHINE_BASE_HPP_
+#define _RIVE_STATE_MACHINE_BASE_HPP_
+#include "rive/animation/animation.hpp"
+namespace rive
+{
+class StateMachineBase : public Animation
+{
+protected:
+    typedef Animation Super;
+
+public:
+    static const uint16_t typeKey = 53;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case StateMachineBase::typeKey:
+            case AnimationBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/state_machine_bool_base.hpp b/include/rive/generated/animation/state_machine_bool_base.hpp
new file mode 100644
index 0000000..2e08c1e
--- /dev/null
+++ b/include/rive/generated/animation/state_machine_bool_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_STATE_MACHINE_BOOL_BASE_HPP_
+#define _RIVE_STATE_MACHINE_BOOL_BASE_HPP_
+#include "rive/animation/state_machine_input.hpp"
+#include "rive/core/field_types/core_bool_type.hpp"
+namespace rive
+{
+class StateMachineBoolBase : public StateMachineInput
+{
+protected:
+    typedef StateMachineInput Super;
+
+public:
+    static const uint16_t typeKey = 59;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case StateMachineBoolBase::typeKey:
+            case StateMachineInputBase::typeKey:
+            case StateMachineComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t valuePropertyKey = 141;
+
+private:
+    bool m_Value = false;
+
+public:
+    inline bool value() const { return m_Value; }
+    void value(bool value)
+    {
+        if (m_Value == value)
+        {
+            return;
+        }
+        m_Value = value;
+        valueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const StateMachineBoolBase& object)
+    {
+        m_Value = object.m_Value;
+        StateMachineInput::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case valuePropertyKey:
+                m_Value = CoreBoolType::deserialize(reader);
+                return true;
+        }
+        return StateMachineInput::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void valueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/state_machine_component_base.hpp b/include/rive/generated/animation/state_machine_component_base.hpp
new file mode 100644
index 0000000..eea14a5
--- /dev/null
+++ b/include/rive/generated/animation/state_machine_component_base.hpp
@@ -0,0 +1,66 @@
+#ifndef _RIVE_STATE_MACHINE_COMPONENT_BASE_HPP_
+#define _RIVE_STATE_MACHINE_COMPONENT_BASE_HPP_
+#include <string>
+#include "rive/core.hpp"
+#include "rive/core/field_types/core_string_type.hpp"
+namespace rive
+{
+class StateMachineComponentBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 54;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case StateMachineComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t namePropertyKey = 138;
+
+private:
+    std::string m_Name = "";
+
+public:
+    inline const std::string& name() const { return m_Name; }
+    void name(std::string value)
+    {
+        if (m_Name == value)
+        {
+            return;
+        }
+        m_Name = value;
+        nameChanged();
+    }
+
+    void copy(const StateMachineComponentBase& object) { m_Name = object.m_Name; }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case namePropertyKey:
+                m_Name = CoreStringType::deserialize(reader);
+                return true;
+        }
+        return false;
+    }
+
+protected:
+    virtual void nameChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/state_machine_fire_event_base.hpp b/include/rive/generated/animation/state_machine_fire_event_base.hpp
new file mode 100644
index 0000000..f9dd827
--- /dev/null
+++ b/include/rive/generated/animation/state_machine_fire_event_base.hpp
@@ -0,0 +1,87 @@
+#ifndef _RIVE_STATE_MACHINE_FIRE_EVENT_BASE_HPP_
+#define _RIVE_STATE_MACHINE_FIRE_EVENT_BASE_HPP_
+#include "rive/core.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class StateMachineFireEventBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 169;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case StateMachineFireEventBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t eventIdPropertyKey = 392;
+    static const uint16_t occursValuePropertyKey = 393;
+
+private:
+    uint32_t m_EventId = -1;
+    uint32_t m_OccursValue = 0;
+
+public:
+    inline uint32_t eventId() const { return m_EventId; }
+    void eventId(uint32_t value)
+    {
+        if (m_EventId == value)
+        {
+            return;
+        }
+        m_EventId = value;
+        eventIdChanged();
+    }
+
+    inline uint32_t occursValue() const { return m_OccursValue; }
+    void occursValue(uint32_t value)
+    {
+        if (m_OccursValue == value)
+        {
+            return;
+        }
+        m_OccursValue = value;
+        occursValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const StateMachineFireEventBase& object)
+    {
+        m_EventId = object.m_EventId;
+        m_OccursValue = object.m_OccursValue;
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case eventIdPropertyKey:
+                m_EventId = CoreUintType::deserialize(reader);
+                return true;
+            case occursValuePropertyKey:
+                m_OccursValue = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return false;
+    }
+
+protected:
+    virtual void eventIdChanged() {}
+    virtual void occursValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/state_machine_input_base.hpp b/include/rive/generated/animation/state_machine_input_base.hpp
new file mode 100644
index 0000000..c78f9f1
--- /dev/null
+++ b/include/rive/generated/animation/state_machine_input_base.hpp
@@ -0,0 +1,34 @@
+#ifndef _RIVE_STATE_MACHINE_INPUT_BASE_HPP_
+#define _RIVE_STATE_MACHINE_INPUT_BASE_HPP_
+#include "rive/animation/state_machine_component.hpp"
+namespace rive
+{
+class StateMachineInputBase : public StateMachineComponent
+{
+protected:
+    typedef StateMachineComponent Super;
+
+public:
+    static const uint16_t typeKey = 55;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case StateMachineInputBase::typeKey:
+            case StateMachineComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/state_machine_layer_base.hpp b/include/rive/generated/animation/state_machine_layer_base.hpp
new file mode 100644
index 0000000..0545cd8
--- /dev/null
+++ b/include/rive/generated/animation/state_machine_layer_base.hpp
@@ -0,0 +1,36 @@
+#ifndef _RIVE_STATE_MACHINE_LAYER_BASE_HPP_
+#define _RIVE_STATE_MACHINE_LAYER_BASE_HPP_
+#include "rive/animation/state_machine_component.hpp"
+namespace rive
+{
+class StateMachineLayerBase : public StateMachineComponent
+{
+protected:
+    typedef StateMachineComponent Super;
+
+public:
+    static const uint16_t typeKey = 57;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case StateMachineLayerBase::typeKey:
+            case StateMachineComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/state_machine_layer_component_base.hpp b/include/rive/generated/animation/state_machine_layer_component_base.hpp
new file mode 100644
index 0000000..f99f201
--- /dev/null
+++ b/include/rive/generated/animation/state_machine_layer_component_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_STATE_MACHINE_LAYER_COMPONENT_BASE_HPP_
+#define _RIVE_STATE_MACHINE_LAYER_COMPONENT_BASE_HPP_
+#include "rive/core.hpp"
+namespace rive
+{
+class StateMachineLayerComponentBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 66;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case StateMachineLayerComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    void copy(const StateMachineLayerComponentBase& object) {}
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override { return false; }
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/state_machine_listener_base.hpp b/include/rive/generated/animation/state_machine_listener_base.hpp
new file mode 100644
index 0000000..d02347b
--- /dev/null
+++ b/include/rive/generated/animation/state_machine_listener_base.hpp
@@ -0,0 +1,107 @@
+#ifndef _RIVE_STATE_MACHINE_LISTENER_BASE_HPP_
+#define _RIVE_STATE_MACHINE_LISTENER_BASE_HPP_
+#include "rive/animation/state_machine_component.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class StateMachineListenerBase : public StateMachineComponent
+{
+protected:
+    typedef StateMachineComponent Super;
+
+public:
+    static const uint16_t typeKey = 114;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case StateMachineListenerBase::typeKey:
+            case StateMachineComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t targetIdPropertyKey = 224;
+    static const uint16_t listenerTypeValuePropertyKey = 225;
+    static const uint16_t eventIdPropertyKey = 399;
+
+private:
+    uint32_t m_TargetId = 0;
+    uint32_t m_ListenerTypeValue = 0;
+    uint32_t m_EventId = -1;
+
+public:
+    inline uint32_t targetId() const { return m_TargetId; }
+    void targetId(uint32_t value)
+    {
+        if (m_TargetId == value)
+        {
+            return;
+        }
+        m_TargetId = value;
+        targetIdChanged();
+    }
+
+    inline uint32_t listenerTypeValue() const { return m_ListenerTypeValue; }
+    void listenerTypeValue(uint32_t value)
+    {
+        if (m_ListenerTypeValue == value)
+        {
+            return;
+        }
+        m_ListenerTypeValue = value;
+        listenerTypeValueChanged();
+    }
+
+    inline uint32_t eventId() const { return m_EventId; }
+    void eventId(uint32_t value)
+    {
+        if (m_EventId == value)
+        {
+            return;
+        }
+        m_EventId = value;
+        eventIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const StateMachineListenerBase& object)
+    {
+        m_TargetId = object.m_TargetId;
+        m_ListenerTypeValue = object.m_ListenerTypeValue;
+        m_EventId = object.m_EventId;
+        StateMachineComponent::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case targetIdPropertyKey:
+                m_TargetId = CoreUintType::deserialize(reader);
+                return true;
+            case listenerTypeValuePropertyKey:
+                m_ListenerTypeValue = CoreUintType::deserialize(reader);
+                return true;
+            case eventIdPropertyKey:
+                m_EventId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return StateMachineComponent::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void targetIdChanged() {}
+    virtual void listenerTypeValueChanged() {}
+    virtual void eventIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/state_machine_number_base.hpp b/include/rive/generated/animation/state_machine_number_base.hpp
new file mode 100644
index 0000000..73d605f
--- /dev/null
+++ b/include/rive/generated/animation/state_machine_number_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_STATE_MACHINE_NUMBER_BASE_HPP_
+#define _RIVE_STATE_MACHINE_NUMBER_BASE_HPP_
+#include "rive/animation/state_machine_input.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class StateMachineNumberBase : public StateMachineInput
+{
+protected:
+    typedef StateMachineInput Super;
+
+public:
+    static const uint16_t typeKey = 56;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case StateMachineNumberBase::typeKey:
+            case StateMachineInputBase::typeKey:
+            case StateMachineComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t valuePropertyKey = 140;
+
+private:
+    float m_Value = 0.0f;
+
+public:
+    inline float value() const { return m_Value; }
+    void value(float value)
+    {
+        if (m_Value == value)
+        {
+            return;
+        }
+        m_Value = value;
+        valueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const StateMachineNumberBase& object)
+    {
+        m_Value = object.m_Value;
+        StateMachineInput::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case valuePropertyKey:
+                m_Value = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return StateMachineInput::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void valueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/state_machine_trigger_base.hpp b/include/rive/generated/animation/state_machine_trigger_base.hpp
new file mode 100644
index 0000000..871d10e
--- /dev/null
+++ b/include/rive/generated/animation/state_machine_trigger_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_STATE_MACHINE_TRIGGER_BASE_HPP_
+#define _RIVE_STATE_MACHINE_TRIGGER_BASE_HPP_
+#include "rive/animation/state_machine_input.hpp"
+namespace rive
+{
+class StateMachineTriggerBase : public StateMachineInput
+{
+protected:
+    typedef StateMachineInput Super;
+
+public:
+    static const uint16_t typeKey = 58;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case StateMachineTriggerBase::typeKey:
+            case StateMachineInputBase::typeKey:
+            case StateMachineComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/state_transition_base.hpp b/include/rive/generated/animation/state_transition_base.hpp
new file mode 100644
index 0000000..7de3c36
--- /dev/null
+++ b/include/rive/generated/animation/state_transition_base.hpp
@@ -0,0 +1,179 @@
+#ifndef _RIVE_STATE_TRANSITION_BASE_HPP_
+#define _RIVE_STATE_TRANSITION_BASE_HPP_
+#include "rive/animation/state_machine_layer_component.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class StateTransitionBase : public StateMachineLayerComponent
+{
+protected:
+    typedef StateMachineLayerComponent Super;
+
+public:
+    static const uint16_t typeKey = 65;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case StateTransitionBase::typeKey:
+            case StateMachineLayerComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t stateToIdPropertyKey = 151;
+    static const uint16_t flagsPropertyKey = 152;
+    static const uint16_t durationPropertyKey = 158;
+    static const uint16_t exitTimePropertyKey = 160;
+    static const uint16_t interpolationTypePropertyKey = 349;
+    static const uint16_t interpolatorIdPropertyKey = 350;
+    static const uint16_t randomWeightPropertyKey = 537;
+
+private:
+    uint32_t m_StateToId = -1;
+    uint32_t m_Flags = 0;
+    uint32_t m_Duration = 0;
+    uint32_t m_ExitTime = 0;
+    uint32_t m_InterpolationType = 1;
+    uint32_t m_InterpolatorId = -1;
+    uint32_t m_RandomWeight = 1;
+
+public:
+    inline uint32_t stateToId() const { return m_StateToId; }
+    void stateToId(uint32_t value)
+    {
+        if (m_StateToId == value)
+        {
+            return;
+        }
+        m_StateToId = value;
+        stateToIdChanged();
+    }
+
+    inline uint32_t flags() const { return m_Flags; }
+    void flags(uint32_t value)
+    {
+        if (m_Flags == value)
+        {
+            return;
+        }
+        m_Flags = value;
+        flagsChanged();
+    }
+
+    inline uint32_t duration() const { return m_Duration; }
+    void duration(uint32_t value)
+    {
+        if (m_Duration == value)
+        {
+            return;
+        }
+        m_Duration = value;
+        durationChanged();
+    }
+
+    inline uint32_t exitTime() const { return m_ExitTime; }
+    void exitTime(uint32_t value)
+    {
+        if (m_ExitTime == value)
+        {
+            return;
+        }
+        m_ExitTime = value;
+        exitTimeChanged();
+    }
+
+    inline uint32_t interpolationType() const { return m_InterpolationType; }
+    void interpolationType(uint32_t value)
+    {
+        if (m_InterpolationType == value)
+        {
+            return;
+        }
+        m_InterpolationType = value;
+        interpolationTypeChanged();
+    }
+
+    inline uint32_t interpolatorId() const { return m_InterpolatorId; }
+    void interpolatorId(uint32_t value)
+    {
+        if (m_InterpolatorId == value)
+        {
+            return;
+        }
+        m_InterpolatorId = value;
+        interpolatorIdChanged();
+    }
+
+    inline uint32_t randomWeight() const { return m_RandomWeight; }
+    void randomWeight(uint32_t value)
+    {
+        if (m_RandomWeight == value)
+        {
+            return;
+        }
+        m_RandomWeight = value;
+        randomWeightChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const StateTransitionBase& object)
+    {
+        m_StateToId = object.m_StateToId;
+        m_Flags = object.m_Flags;
+        m_Duration = object.m_Duration;
+        m_ExitTime = object.m_ExitTime;
+        m_InterpolationType = object.m_InterpolationType;
+        m_InterpolatorId = object.m_InterpolatorId;
+        m_RandomWeight = object.m_RandomWeight;
+        StateMachineLayerComponent::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case stateToIdPropertyKey:
+                m_StateToId = CoreUintType::deserialize(reader);
+                return true;
+            case flagsPropertyKey:
+                m_Flags = CoreUintType::deserialize(reader);
+                return true;
+            case durationPropertyKey:
+                m_Duration = CoreUintType::deserialize(reader);
+                return true;
+            case exitTimePropertyKey:
+                m_ExitTime = CoreUintType::deserialize(reader);
+                return true;
+            case interpolationTypePropertyKey:
+                m_InterpolationType = CoreUintType::deserialize(reader);
+                return true;
+            case interpolatorIdPropertyKey:
+                m_InterpolatorId = CoreUintType::deserialize(reader);
+                return true;
+            case randomWeightPropertyKey:
+                m_RandomWeight = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return StateMachineLayerComponent::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void stateToIdChanged() {}
+    virtual void flagsChanged() {}
+    virtual void durationChanged() {}
+    virtual void exitTimeChanged() {}
+    virtual void interpolationTypeChanged() {}
+    virtual void interpolatorIdChanged() {}
+    virtual void randomWeightChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/transition_artboard_condition_base.hpp b/include/rive/generated/animation/transition_artboard_condition_base.hpp
new file mode 100644
index 0000000..5f079c4
--- /dev/null
+++ b/include/rive/generated/animation/transition_artboard_condition_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_TRANSITION_ARTBOARD_CONDITION_BASE_HPP_
+#define _RIVE_TRANSITION_ARTBOARD_CONDITION_BASE_HPP_
+#include "rive/animation/transition_viewmodel_condition.hpp"
+namespace rive
+{
+class TransitionArtboardConditionBase : public TransitionViewModelCondition
+{
+protected:
+    typedef TransitionViewModelCondition Super;
+
+public:
+    static const uint16_t typeKey = 497;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransitionArtboardConditionBase::typeKey:
+            case TransitionViewModelConditionBase::typeKey:
+            case TransitionConditionBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/transition_bool_condition_base.hpp b/include/rive/generated/animation/transition_bool_condition_base.hpp
new file mode 100644
index 0000000..c9783ea
--- /dev/null
+++ b/include/rive/generated/animation/transition_bool_condition_base.hpp
@@ -0,0 +1,38 @@
+#ifndef _RIVE_TRANSITION_BOOL_CONDITION_BASE_HPP_
+#define _RIVE_TRANSITION_BOOL_CONDITION_BASE_HPP_
+#include "rive/animation/transition_value_condition.hpp"
+namespace rive
+{
+class TransitionBoolConditionBase : public TransitionValueCondition
+{
+protected:
+    typedef TransitionValueCondition Super;
+
+public:
+    static const uint16_t typeKey = 71;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransitionBoolConditionBase::typeKey:
+            case TransitionValueConditionBase::typeKey:
+            case TransitionInputConditionBase::typeKey:
+            case TransitionConditionBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/transition_comparator_base.hpp b/include/rive/generated/animation/transition_comparator_base.hpp
new file mode 100644
index 0000000..7b66f88
--- /dev/null
+++ b/include/rive/generated/animation/transition_comparator_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_TRANSITION_COMPARATOR_BASE_HPP_
+#define _RIVE_TRANSITION_COMPARATOR_BASE_HPP_
+#include "rive/core.hpp"
+namespace rive
+{
+class TransitionComparatorBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 477;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransitionComparatorBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    void copy(const TransitionComparatorBase& object) {}
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override { return false; }
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/transition_condition_base.hpp b/include/rive/generated/animation/transition_condition_base.hpp
new file mode 100644
index 0000000..23145d5
--- /dev/null
+++ b/include/rive/generated/animation/transition_condition_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_TRANSITION_CONDITION_BASE_HPP_
+#define _RIVE_TRANSITION_CONDITION_BASE_HPP_
+#include "rive/core.hpp"
+namespace rive
+{
+class TransitionConditionBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 476;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransitionConditionBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    void copy(const TransitionConditionBase& object) {}
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override { return false; }
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/transition_input_condition_base.hpp b/include/rive/generated/animation/transition_input_condition_base.hpp
new file mode 100644
index 0000000..1440bf7
--- /dev/null
+++ b/include/rive/generated/animation/transition_input_condition_base.hpp
@@ -0,0 +1,70 @@
+#ifndef _RIVE_TRANSITION_INPUT_CONDITION_BASE_HPP_
+#define _RIVE_TRANSITION_INPUT_CONDITION_BASE_HPP_
+#include "rive/animation/transition_condition.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class TransitionInputConditionBase : public TransitionCondition
+{
+protected:
+    typedef TransitionCondition Super;
+
+public:
+    static const uint16_t typeKey = 67;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransitionInputConditionBase::typeKey:
+            case TransitionConditionBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t inputIdPropertyKey = 155;
+
+private:
+    uint32_t m_InputId = -1;
+
+public:
+    inline uint32_t inputId() const { return m_InputId; }
+    void inputId(uint32_t value)
+    {
+        if (m_InputId == value)
+        {
+            return;
+        }
+        m_InputId = value;
+        inputIdChanged();
+    }
+
+    void copy(const TransitionInputConditionBase& object)
+    {
+        m_InputId = object.m_InputId;
+        TransitionCondition::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case inputIdPropertyKey:
+                m_InputId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return TransitionCondition::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void inputIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/transition_number_condition_base.hpp b/include/rive/generated/animation/transition_number_condition_base.hpp
new file mode 100644
index 0000000..82e1a81
--- /dev/null
+++ b/include/rive/generated/animation/transition_number_condition_base.hpp
@@ -0,0 +1,73 @@
+#ifndef _RIVE_TRANSITION_NUMBER_CONDITION_BASE_HPP_
+#define _RIVE_TRANSITION_NUMBER_CONDITION_BASE_HPP_
+#include "rive/animation/transition_value_condition.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class TransitionNumberConditionBase : public TransitionValueCondition
+{
+protected:
+    typedef TransitionValueCondition Super;
+
+public:
+    static const uint16_t typeKey = 70;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransitionNumberConditionBase::typeKey:
+            case TransitionValueConditionBase::typeKey:
+            case TransitionInputConditionBase::typeKey:
+            case TransitionConditionBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t valuePropertyKey = 157;
+
+private:
+    float m_Value = 0.0f;
+
+public:
+    inline float value() const { return m_Value; }
+    void value(float value)
+    {
+        if (m_Value == value)
+        {
+            return;
+        }
+        m_Value = value;
+        valueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const TransitionNumberConditionBase& object)
+    {
+        m_Value = object.m_Value;
+        TransitionValueCondition::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case valuePropertyKey:
+                m_Value = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return TransitionValueCondition::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void valueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/transition_property_artboard_comparator_base.hpp b/include/rive/generated/animation/transition_property_artboard_comparator_base.hpp
new file mode 100644
index 0000000..354087a
--- /dev/null
+++ b/include/rive/generated/animation/transition_property_artboard_comparator_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_TRANSITION_PROPERTY_ARTBOARD_COMPARATOR_BASE_HPP_
+#define _RIVE_TRANSITION_PROPERTY_ARTBOARD_COMPARATOR_BASE_HPP_
+#include "rive/animation/transition_property_comparator.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class TransitionPropertyArtboardComparatorBase : public TransitionPropertyComparator
+{
+protected:
+    typedef TransitionPropertyComparator Super;
+
+public:
+    static const uint16_t typeKey = 496;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransitionPropertyArtboardComparatorBase::typeKey:
+            case TransitionPropertyComparatorBase::typeKey:
+            case TransitionComparatorBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t propertyTypePropertyKey = 677;
+
+private:
+    uint32_t m_PropertyType = 0;
+
+public:
+    inline uint32_t propertyType() const { return m_PropertyType; }
+    void propertyType(uint32_t value)
+    {
+        if (m_PropertyType == value)
+        {
+            return;
+        }
+        m_PropertyType = value;
+        propertyTypeChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const TransitionPropertyArtboardComparatorBase& object)
+    {
+        m_PropertyType = object.m_PropertyType;
+        TransitionPropertyComparator::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case propertyTypePropertyKey:
+                m_PropertyType = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return TransitionPropertyComparator::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void propertyTypeChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/transition_property_comparator_base.hpp b/include/rive/generated/animation/transition_property_comparator_base.hpp
new file mode 100644
index 0000000..33497c2
--- /dev/null
+++ b/include/rive/generated/animation/transition_property_comparator_base.hpp
@@ -0,0 +1,34 @@
+#ifndef _RIVE_TRANSITION_PROPERTY_COMPARATOR_BASE_HPP_
+#define _RIVE_TRANSITION_PROPERTY_COMPARATOR_BASE_HPP_
+#include "rive/animation/transition_comparator.hpp"
+namespace rive
+{
+class TransitionPropertyComparatorBase : public TransitionComparator
+{
+protected:
+    typedef TransitionComparator Super;
+
+public:
+    static const uint16_t typeKey = 478;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransitionPropertyComparatorBase::typeKey:
+            case TransitionComparatorBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/transition_property_viewmodel_comparator_base.hpp b/include/rive/generated/animation/transition_property_viewmodel_comparator_base.hpp
new file mode 100644
index 0000000..d7d2168
--- /dev/null
+++ b/include/rive/generated/animation/transition_property_viewmodel_comparator_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_TRANSITION_PROPERTY_VIEW_MODEL_COMPARATOR_BASE_HPP_
+#define _RIVE_TRANSITION_PROPERTY_VIEW_MODEL_COMPARATOR_BASE_HPP_
+#include "rive/animation/transition_property_comparator.hpp"
+namespace rive
+{
+class TransitionPropertyViewModelComparatorBase : public TransitionPropertyComparator
+{
+protected:
+    typedef TransitionPropertyComparator Super;
+
+public:
+    static const uint16_t typeKey = 479;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransitionPropertyViewModelComparatorBase::typeKey:
+            case TransitionPropertyComparatorBase::typeKey:
+            case TransitionComparatorBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/transition_trigger_condition_base.hpp b/include/rive/generated/animation/transition_trigger_condition_base.hpp
new file mode 100644
index 0000000..902f1a4
--- /dev/null
+++ b/include/rive/generated/animation/transition_trigger_condition_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_TRANSITION_TRIGGER_CONDITION_BASE_HPP_
+#define _RIVE_TRANSITION_TRIGGER_CONDITION_BASE_HPP_
+#include "rive/animation/transition_input_condition.hpp"
+namespace rive
+{
+class TransitionTriggerConditionBase : public TransitionInputCondition
+{
+protected:
+    typedef TransitionInputCondition Super;
+
+public:
+    static const uint16_t typeKey = 68;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransitionTriggerConditionBase::typeKey:
+            case TransitionInputConditionBase::typeKey:
+            case TransitionConditionBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/transition_value_boolean_comparator_base.hpp b/include/rive/generated/animation/transition_value_boolean_comparator_base.hpp
new file mode 100644
index 0000000..bdbcf28
--- /dev/null
+++ b/include/rive/generated/animation/transition_value_boolean_comparator_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_TRANSITION_VALUE_BOOLEAN_COMPARATOR_BASE_HPP_
+#define _RIVE_TRANSITION_VALUE_BOOLEAN_COMPARATOR_BASE_HPP_
+#include "rive/animation/transition_value_comparator.hpp"
+#include "rive/core/field_types/core_bool_type.hpp"
+namespace rive
+{
+class TransitionValueBooleanComparatorBase : public TransitionValueComparator
+{
+protected:
+    typedef TransitionValueComparator Super;
+
+public:
+    static const uint16_t typeKey = 481;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransitionValueBooleanComparatorBase::typeKey:
+            case TransitionValueComparatorBase::typeKey:
+            case TransitionComparatorBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t valuePropertyKey = 647;
+
+private:
+    bool m_Value = false;
+
+public:
+    inline bool value() const { return m_Value; }
+    void value(bool value)
+    {
+        if (m_Value == value)
+        {
+            return;
+        }
+        m_Value = value;
+        valueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const TransitionValueBooleanComparatorBase& object)
+    {
+        m_Value = object.m_Value;
+        TransitionValueComparator::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case valuePropertyKey:
+                m_Value = CoreBoolType::deserialize(reader);
+                return true;
+        }
+        return TransitionValueComparator::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void valueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/transition_value_color_comparator_base.hpp b/include/rive/generated/animation/transition_value_color_comparator_base.hpp
new file mode 100644
index 0000000..f44d2be
--- /dev/null
+++ b/include/rive/generated/animation/transition_value_color_comparator_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_TRANSITION_VALUE_COLOR_COMPARATOR_BASE_HPP_
+#define _RIVE_TRANSITION_VALUE_COLOR_COMPARATOR_BASE_HPP_
+#include "rive/animation/transition_value_comparator.hpp"
+#include "rive/core/field_types/core_color_type.hpp"
+namespace rive
+{
+class TransitionValueColorComparatorBase : public TransitionValueComparator
+{
+protected:
+    typedef TransitionValueComparator Super;
+
+public:
+    static const uint16_t typeKey = 483;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransitionValueColorComparatorBase::typeKey:
+            case TransitionValueComparatorBase::typeKey:
+            case TransitionComparatorBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t valuePropertyKey = 651;
+
+private:
+    int m_Value = 0xFF1D1D1D;
+
+public:
+    inline int value() const { return m_Value; }
+    void value(int value)
+    {
+        if (m_Value == value)
+        {
+            return;
+        }
+        m_Value = value;
+        valueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const TransitionValueColorComparatorBase& object)
+    {
+        m_Value = object.m_Value;
+        TransitionValueComparator::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case valuePropertyKey:
+                m_Value = CoreColorType::deserialize(reader);
+                return true;
+        }
+        return TransitionValueComparator::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void valueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/transition_value_comparator_base.hpp b/include/rive/generated/animation/transition_value_comparator_base.hpp
new file mode 100644
index 0000000..aced372
--- /dev/null
+++ b/include/rive/generated/animation/transition_value_comparator_base.hpp
@@ -0,0 +1,34 @@
+#ifndef _RIVE_TRANSITION_VALUE_COMPARATOR_BASE_HPP_
+#define _RIVE_TRANSITION_VALUE_COMPARATOR_BASE_HPP_
+#include "rive/animation/transition_comparator.hpp"
+namespace rive
+{
+class TransitionValueComparatorBase : public TransitionComparator
+{
+protected:
+    typedef TransitionComparator Super;
+
+public:
+    static const uint16_t typeKey = 480;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransitionValueComparatorBase::typeKey:
+            case TransitionComparatorBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/transition_value_condition_base.hpp b/include/rive/generated/animation/transition_value_condition_base.hpp
new file mode 100644
index 0000000..e9c680e
--- /dev/null
+++ b/include/rive/generated/animation/transition_value_condition_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_TRANSITION_VALUE_CONDITION_BASE_HPP_
+#define _RIVE_TRANSITION_VALUE_CONDITION_BASE_HPP_
+#include "rive/animation/transition_input_condition.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class TransitionValueConditionBase : public TransitionInputCondition
+{
+protected:
+    typedef TransitionInputCondition Super;
+
+public:
+    static const uint16_t typeKey = 69;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransitionValueConditionBase::typeKey:
+            case TransitionInputConditionBase::typeKey:
+            case TransitionConditionBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t opValuePropertyKey = 156;
+
+private:
+    uint32_t m_OpValue = 0;
+
+public:
+    inline uint32_t opValue() const { return m_OpValue; }
+    void opValue(uint32_t value)
+    {
+        if (m_OpValue == value)
+        {
+            return;
+        }
+        m_OpValue = value;
+        opValueChanged();
+    }
+
+    void copy(const TransitionValueConditionBase& object)
+    {
+        m_OpValue = object.m_OpValue;
+        TransitionInputCondition::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case opValuePropertyKey:
+                m_OpValue = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return TransitionInputCondition::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void opValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/transition_value_enum_comparator_base.hpp b/include/rive/generated/animation/transition_value_enum_comparator_base.hpp
new file mode 100644
index 0000000..ec9bb45
--- /dev/null
+++ b/include/rive/generated/animation/transition_value_enum_comparator_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_TRANSITION_VALUE_ENUM_COMPARATOR_BASE_HPP_
+#define _RIVE_TRANSITION_VALUE_ENUM_COMPARATOR_BASE_HPP_
+#include "rive/animation/transition_value_comparator.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class TransitionValueEnumComparatorBase : public TransitionValueComparator
+{
+protected:
+    typedef TransitionValueComparator Super;
+
+public:
+    static const uint16_t typeKey = 485;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransitionValueEnumComparatorBase::typeKey:
+            case TransitionValueComparatorBase::typeKey:
+            case TransitionComparatorBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t valuePropertyKey = 653;
+
+private:
+    uint32_t m_Value = -1;
+
+public:
+    inline uint32_t value() const { return m_Value; }
+    void value(uint32_t value)
+    {
+        if (m_Value == value)
+        {
+            return;
+        }
+        m_Value = value;
+        valueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const TransitionValueEnumComparatorBase& object)
+    {
+        m_Value = object.m_Value;
+        TransitionValueComparator::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case valuePropertyKey:
+                m_Value = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return TransitionValueComparator::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void valueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/transition_value_number_comparator_base.hpp b/include/rive/generated/animation/transition_value_number_comparator_base.hpp
new file mode 100644
index 0000000..7fc7e7c
--- /dev/null
+++ b/include/rive/generated/animation/transition_value_number_comparator_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_TRANSITION_VALUE_NUMBER_COMPARATOR_BASE_HPP_
+#define _RIVE_TRANSITION_VALUE_NUMBER_COMPARATOR_BASE_HPP_
+#include "rive/animation/transition_value_comparator.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class TransitionValueNumberComparatorBase : public TransitionValueComparator
+{
+protected:
+    typedef TransitionValueComparator Super;
+
+public:
+    static const uint16_t typeKey = 484;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransitionValueNumberComparatorBase::typeKey:
+            case TransitionValueComparatorBase::typeKey:
+            case TransitionComparatorBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t valuePropertyKey = 652;
+
+private:
+    float m_Value = 0.0f;
+
+public:
+    inline float value() const { return m_Value; }
+    void value(float value)
+    {
+        if (m_Value == value)
+        {
+            return;
+        }
+        m_Value = value;
+        valueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const TransitionValueNumberComparatorBase& object)
+    {
+        m_Value = object.m_Value;
+        TransitionValueComparator::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case valuePropertyKey:
+                m_Value = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return TransitionValueComparator::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void valueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/transition_value_string_comparator_base.hpp b/include/rive/generated/animation/transition_value_string_comparator_base.hpp
new file mode 100644
index 0000000..ecd1740
--- /dev/null
+++ b/include/rive/generated/animation/transition_value_string_comparator_base.hpp
@@ -0,0 +1,73 @@
+#ifndef _RIVE_TRANSITION_VALUE_STRING_COMPARATOR_BASE_HPP_
+#define _RIVE_TRANSITION_VALUE_STRING_COMPARATOR_BASE_HPP_
+#include <string>
+#include "rive/animation/transition_value_comparator.hpp"
+#include "rive/core/field_types/core_string_type.hpp"
+namespace rive
+{
+class TransitionValueStringComparatorBase : public TransitionValueComparator
+{
+protected:
+    typedef TransitionValueComparator Super;
+
+public:
+    static const uint16_t typeKey = 486;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransitionValueStringComparatorBase::typeKey:
+            case TransitionValueComparatorBase::typeKey:
+            case TransitionComparatorBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t valuePropertyKey = 654;
+
+private:
+    std::string m_Value = "";
+
+public:
+    inline const std::string& value() const { return m_Value; }
+    void value(std::string value)
+    {
+        if (m_Value == value)
+        {
+            return;
+        }
+        m_Value = value;
+        valueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const TransitionValueStringComparatorBase& object)
+    {
+        m_Value = object.m_Value;
+        TransitionValueComparator::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case valuePropertyKey:
+                m_Value = CoreStringType::deserialize(reader);
+                return true;
+        }
+        return TransitionValueComparator::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void valueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/animation/transition_viewmodel_condition_base.hpp b/include/rive/generated/animation/transition_viewmodel_condition_base.hpp
new file mode 100644
index 0000000..7c156a4
--- /dev/null
+++ b/include/rive/generated/animation/transition_viewmodel_condition_base.hpp
@@ -0,0 +1,107 @@
+#ifndef _RIVE_TRANSITION_VIEW_MODEL_CONDITION_BASE_HPP_
+#define _RIVE_TRANSITION_VIEW_MODEL_CONDITION_BASE_HPP_
+#include "rive/animation/transition_condition.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class TransitionViewModelConditionBase : public TransitionCondition
+{
+protected:
+    typedef TransitionCondition Super;
+
+public:
+    static const uint16_t typeKey = 482;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransitionViewModelConditionBase::typeKey:
+            case TransitionConditionBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t leftComparatorIdPropertyKey = 648;
+    static const uint16_t rightComparatorIdPropertyKey = 649;
+    static const uint16_t opValuePropertyKey = 650;
+
+private:
+    uint32_t m_LeftComparatorId = -1;
+    uint32_t m_RightComparatorId = -1;
+    uint32_t m_OpValue = 0;
+
+public:
+    inline uint32_t leftComparatorId() const { return m_LeftComparatorId; }
+    void leftComparatorId(uint32_t value)
+    {
+        if (m_LeftComparatorId == value)
+        {
+            return;
+        }
+        m_LeftComparatorId = value;
+        leftComparatorIdChanged();
+    }
+
+    inline uint32_t rightComparatorId() const { return m_RightComparatorId; }
+    void rightComparatorId(uint32_t value)
+    {
+        if (m_RightComparatorId == value)
+        {
+            return;
+        }
+        m_RightComparatorId = value;
+        rightComparatorIdChanged();
+    }
+
+    inline uint32_t opValue() const { return m_OpValue; }
+    void opValue(uint32_t value)
+    {
+        if (m_OpValue == value)
+        {
+            return;
+        }
+        m_OpValue = value;
+        opValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const TransitionViewModelConditionBase& object)
+    {
+        m_LeftComparatorId = object.m_LeftComparatorId;
+        m_RightComparatorId = object.m_RightComparatorId;
+        m_OpValue = object.m_OpValue;
+        TransitionCondition::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case leftComparatorIdPropertyKey:
+                m_LeftComparatorId = CoreUintType::deserialize(reader);
+                return true;
+            case rightComparatorIdPropertyKey:
+                m_RightComparatorId = CoreUintType::deserialize(reader);
+                return true;
+            case opValuePropertyKey:
+                m_OpValue = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return TransitionCondition::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void leftComparatorIdChanged() {}
+    virtual void rightComparatorIdChanged() {}
+    virtual void opValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/artboard_base.hpp b/include/rive/generated/artboard_base.hpp
new file mode 100644
index 0000000..b7ae1b9
--- /dev/null
+++ b/include/rive/generated/artboard_base.hpp
@@ -0,0 +1,132 @@
+#ifndef _RIVE_ARTBOARD_BASE_HPP_
+#define _RIVE_ARTBOARD_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/layout_component.hpp"
+namespace rive
+{
+class ArtboardBase : public LayoutComponent
+{
+protected:
+    typedef LayoutComponent Super;
+
+public:
+    static const uint16_t typeKey = 1;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ArtboardBase::typeKey:
+            case LayoutComponentBase::typeKey:
+            case DrawableBase::typeKey:
+            case NodeBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t originXPropertyKey = 11;
+    static const uint16_t originYPropertyKey = 12;
+    static const uint16_t defaultStateMachineIdPropertyKey = 236;
+    static const uint16_t viewModelIdPropertyKey = 583;
+
+private:
+    float m_OriginX = 0.0f;
+    float m_OriginY = 0.0f;
+    uint32_t m_DefaultStateMachineId = -1;
+    uint32_t m_ViewModelId = -1;
+
+public:
+    inline float originX() const { return m_OriginX; }
+    void originX(float value)
+    {
+        if (m_OriginX == value)
+        {
+            return;
+        }
+        m_OriginX = value;
+        originXChanged();
+    }
+
+    inline float originY() const { return m_OriginY; }
+    void originY(float value)
+    {
+        if (m_OriginY == value)
+        {
+            return;
+        }
+        m_OriginY = value;
+        originYChanged();
+    }
+
+    inline uint32_t defaultStateMachineId() const { return m_DefaultStateMachineId; }
+    void defaultStateMachineId(uint32_t value)
+    {
+        if (m_DefaultStateMachineId == value)
+        {
+            return;
+        }
+        m_DefaultStateMachineId = value;
+        defaultStateMachineIdChanged();
+    }
+
+    inline uint32_t viewModelId() const { return m_ViewModelId; }
+    void viewModelId(uint32_t value)
+    {
+        if (m_ViewModelId == value)
+        {
+            return;
+        }
+        m_ViewModelId = value;
+        viewModelIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ArtboardBase& object)
+    {
+        m_OriginX = object.m_OriginX;
+        m_OriginY = object.m_OriginY;
+        m_DefaultStateMachineId = object.m_DefaultStateMachineId;
+        m_ViewModelId = object.m_ViewModelId;
+        LayoutComponent::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case originXPropertyKey:
+                m_OriginX = CoreDoubleType::deserialize(reader);
+                return true;
+            case originYPropertyKey:
+                m_OriginY = CoreDoubleType::deserialize(reader);
+                return true;
+            case defaultStateMachineIdPropertyKey:
+                m_DefaultStateMachineId = CoreUintType::deserialize(reader);
+                return true;
+            case viewModelIdPropertyKey:
+                m_ViewModelId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return LayoutComponent::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void originXChanged() {}
+    virtual void originYChanged() {}
+    virtual void defaultStateMachineIdChanged() {}
+    virtual void viewModelIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/assets/asset_base.hpp b/include/rive/generated/assets/asset_base.hpp
new file mode 100644
index 0000000..794b563
--- /dev/null
+++ b/include/rive/generated/assets/asset_base.hpp
@@ -0,0 +1,66 @@
+#ifndef _RIVE_ASSET_BASE_HPP_
+#define _RIVE_ASSET_BASE_HPP_
+#include <string>
+#include "rive/core.hpp"
+#include "rive/core/field_types/core_string_type.hpp"
+namespace rive
+{
+class AssetBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 99;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case AssetBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t namePropertyKey = 203;
+
+private:
+    std::string m_Name = "";
+
+public:
+    inline const std::string& name() const { return m_Name; }
+    void name(std::string value)
+    {
+        if (m_Name == value)
+        {
+            return;
+        }
+        m_Name = value;
+        nameChanged();
+    }
+
+    void copy(const AssetBase& object) { m_Name = object.m_Name; }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case namePropertyKey:
+                m_Name = CoreStringType::deserialize(reader);
+                return true;
+        }
+        return false;
+    }
+
+protected:
+    virtual void nameChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/assets/audio_asset_base.hpp b/include/rive/generated/assets/audio_asset_base.hpp
new file mode 100644
index 0000000..49f6578
--- /dev/null
+++ b/include/rive/generated/assets/audio_asset_base.hpp
@@ -0,0 +1,38 @@
+#ifndef _RIVE_AUDIO_ASSET_BASE_HPP_
+#define _RIVE_AUDIO_ASSET_BASE_HPP_
+#include "rive/assets/export_audio.hpp"
+namespace rive
+{
+class AudioAssetBase : public ExportAudio
+{
+protected:
+    typedef ExportAudio Super;
+
+public:
+    static const uint16_t typeKey = 406;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case AudioAssetBase::typeKey:
+            case ExportAudioBase::typeKey:
+            case FileAssetBase::typeKey:
+            case AssetBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/assets/drawable_asset_base.hpp b/include/rive/generated/assets/drawable_asset_base.hpp
new file mode 100644
index 0000000..22690ed
--- /dev/null
+++ b/include/rive/generated/assets/drawable_asset_base.hpp
@@ -0,0 +1,89 @@
+#ifndef _RIVE_DRAWABLE_ASSET_BASE_HPP_
+#define _RIVE_DRAWABLE_ASSET_BASE_HPP_
+#include "rive/assets/file_asset.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class DrawableAssetBase : public FileAsset
+{
+protected:
+    typedef FileAsset Super;
+
+public:
+    static const uint16_t typeKey = 104;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case DrawableAssetBase::typeKey:
+            case FileAssetBase::typeKey:
+            case AssetBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t heightPropertyKey = 207;
+    static const uint16_t widthPropertyKey = 208;
+
+private:
+    float m_Height = 0.0f;
+    float m_Width = 0.0f;
+
+public:
+    inline float height() const { return m_Height; }
+    void height(float value)
+    {
+        if (m_Height == value)
+        {
+            return;
+        }
+        m_Height = value;
+        heightChanged();
+    }
+
+    inline float width() const { return m_Width; }
+    void width(float value)
+    {
+        if (m_Width == value)
+        {
+            return;
+        }
+        m_Width = value;
+        widthChanged();
+    }
+
+    void copy(const DrawableAssetBase& object)
+    {
+        m_Height = object.m_Height;
+        m_Width = object.m_Width;
+        FileAsset::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case heightPropertyKey:
+                m_Height = CoreDoubleType::deserialize(reader);
+                return true;
+            case widthPropertyKey:
+                m_Width = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return FileAsset::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void heightChanged() {}
+    virtual void widthChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/assets/export_audio_base.hpp b/include/rive/generated/assets/export_audio_base.hpp
new file mode 100644
index 0000000..12cb58c
--- /dev/null
+++ b/include/rive/generated/assets/export_audio_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_EXPORT_AUDIO_BASE_HPP_
+#define _RIVE_EXPORT_AUDIO_BASE_HPP_
+#include "rive/assets/file_asset.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class ExportAudioBase : public FileAsset
+{
+protected:
+    typedef FileAsset Super;
+
+public:
+    static const uint16_t typeKey = 422;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ExportAudioBase::typeKey:
+            case FileAssetBase::typeKey:
+            case AssetBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t volumePropertyKey = 530;
+
+private:
+    float m_Volume = 1.0f;
+
+public:
+    inline float volume() const { return m_Volume; }
+    void volume(float value)
+    {
+        if (m_Volume == value)
+        {
+            return;
+        }
+        m_Volume = value;
+        volumeChanged();
+    }
+
+    void copy(const ExportAudioBase& object)
+    {
+        m_Volume = object.m_Volume;
+        FileAsset::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case volumePropertyKey:
+                m_Volume = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return FileAsset::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void volumeChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/assets/file_asset_base.hpp b/include/rive/generated/assets/file_asset_base.hpp
new file mode 100644
index 0000000..664b4b7
--- /dev/null
+++ b/include/rive/generated/assets/file_asset_base.hpp
@@ -0,0 +1,101 @@
+#ifndef _RIVE_FILE_ASSET_BASE_HPP_
+#define _RIVE_FILE_ASSET_BASE_HPP_
+#include <string>
+#include "rive/assets/asset.hpp"
+#include "rive/core/field_types/core_bytes_type.hpp"
+#include "rive/core/field_types/core_string_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/span.hpp"
+namespace rive
+{
+class FileAssetBase : public Asset
+{
+protected:
+    typedef Asset Super;
+
+public:
+    static const uint16_t typeKey = 103;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case FileAssetBase::typeKey:
+            case AssetBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t assetIdPropertyKey = 204;
+    static const uint16_t cdnUuidPropertyKey = 359;
+    static const uint16_t cdnBaseUrlPropertyKey = 362;
+
+private:
+    uint32_t m_AssetId = 0;
+    std::string m_CdnBaseUrl = "https://public.rive.app/cdn/uuid";
+
+public:
+    inline uint32_t assetId() const { return m_AssetId; }
+    void assetId(uint32_t value)
+    {
+        if (m_AssetId == value)
+        {
+            return;
+        }
+        m_AssetId = value;
+        assetIdChanged();
+    }
+
+    virtual void decodeCdnUuid(Span<const uint8_t> value) = 0;
+    virtual void copyCdnUuid(const FileAssetBase& object) = 0;
+
+    inline const std::string& cdnBaseUrl() const { return m_CdnBaseUrl; }
+    void cdnBaseUrl(std::string value)
+    {
+        if (m_CdnBaseUrl == value)
+        {
+            return;
+        }
+        m_CdnBaseUrl = value;
+        cdnBaseUrlChanged();
+    }
+
+    void copy(const FileAssetBase& object)
+    {
+        m_AssetId = object.m_AssetId;
+        copyCdnUuid(object);
+        m_CdnBaseUrl = object.m_CdnBaseUrl;
+        Asset::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case assetIdPropertyKey:
+                m_AssetId = CoreUintType::deserialize(reader);
+                return true;
+            case cdnUuidPropertyKey:
+                decodeCdnUuid(CoreBytesType::deserialize(reader));
+                return true;
+            case cdnBaseUrlPropertyKey:
+                m_CdnBaseUrl = CoreStringType::deserialize(reader);
+                return true;
+        }
+        return Asset::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void assetIdChanged() {}
+    virtual void cdnUuidChanged() {}
+    virtual void cdnBaseUrlChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/assets/file_asset_contents_base.hpp b/include/rive/generated/assets/file_asset_contents_base.hpp
new file mode 100644
index 0000000..dd50e07
--- /dev/null
+++ b/include/rive/generated/assets/file_asset_contents_base.hpp
@@ -0,0 +1,56 @@
+#ifndef _RIVE_FILE_ASSET_CONTENTS_BASE_HPP_
+#define _RIVE_FILE_ASSET_CONTENTS_BASE_HPP_
+#include "rive/core.hpp"
+#include "rive/core/field_types/core_bytes_type.hpp"
+#include "rive/span.hpp"
+namespace rive
+{
+class FileAssetContentsBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 106;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case FileAssetContentsBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t bytesPropertyKey = 212;
+
+public:
+    virtual void decodeBytes(Span<const uint8_t> value) = 0;
+    virtual void copyBytes(const FileAssetContentsBase& object) = 0;
+
+    Core* clone() const override;
+    void copy(const FileAssetContentsBase& object) { copyBytes(object); }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case bytesPropertyKey:
+                decodeBytes(CoreBytesType::deserialize(reader));
+                return true;
+        }
+        return false;
+    }
+
+protected:
+    virtual void bytesChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/assets/folder_base.hpp b/include/rive/generated/assets/folder_base.hpp
new file mode 100644
index 0000000..77f9750
--- /dev/null
+++ b/include/rive/generated/assets/folder_base.hpp
@@ -0,0 +1,36 @@
+#ifndef _RIVE_FOLDER_BASE_HPP_
+#define _RIVE_FOLDER_BASE_HPP_
+#include "rive/assets/asset.hpp"
+namespace rive
+{
+class FolderBase : public Asset
+{
+protected:
+    typedef Asset Super;
+
+public:
+    static const uint16_t typeKey = 102;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case FolderBase::typeKey:
+            case AssetBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/assets/font_asset_base.hpp b/include/rive/generated/assets/font_asset_base.hpp
new file mode 100644
index 0000000..fa9203f
--- /dev/null
+++ b/include/rive/generated/assets/font_asset_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_FONT_ASSET_BASE_HPP_
+#define _RIVE_FONT_ASSET_BASE_HPP_
+#include "rive/assets/file_asset.hpp"
+namespace rive
+{
+class FontAssetBase : public FileAsset
+{
+protected:
+    typedef FileAsset Super;
+
+public:
+    static const uint16_t typeKey = 141;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case FontAssetBase::typeKey:
+            case FileAssetBase::typeKey:
+            case AssetBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/assets/image_asset_base.hpp b/include/rive/generated/assets/image_asset_base.hpp
new file mode 100644
index 0000000..d51220f
--- /dev/null
+++ b/include/rive/generated/assets/image_asset_base.hpp
@@ -0,0 +1,38 @@
+#ifndef _RIVE_IMAGE_ASSET_BASE_HPP_
+#define _RIVE_IMAGE_ASSET_BASE_HPP_
+#include "rive/assets/drawable_asset.hpp"
+namespace rive
+{
+class ImageAssetBase : public DrawableAsset
+{
+protected:
+    typedef DrawableAsset Super;
+
+public:
+    static const uint16_t typeKey = 105;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ImageAssetBase::typeKey:
+            case DrawableAssetBase::typeKey:
+            case FileAssetBase::typeKey:
+            case AssetBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/audio_event_base.hpp b/include/rive/generated/audio_event_base.hpp
new file mode 100644
index 0000000..fece563
--- /dev/null
+++ b/include/rive/generated/audio_event_base.hpp
@@ -0,0 +1,73 @@
+#ifndef _RIVE_AUDIO_EVENT_BASE_HPP_
+#define _RIVE_AUDIO_EVENT_BASE_HPP_
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/event.hpp"
+namespace rive
+{
+class AudioEventBase : public Event
+{
+protected:
+    typedef Event Super;
+
+public:
+    static const uint16_t typeKey = 407;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case AudioEventBase::typeKey:
+            case EventBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t assetIdPropertyKey = 408;
+
+private:
+    uint32_t m_AssetId = -1;
+
+public:
+    inline uint32_t assetId() const { return m_AssetId; }
+    void assetId(uint32_t value)
+    {
+        if (m_AssetId == value)
+        {
+            return;
+        }
+        m_AssetId = value;
+        assetIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const AudioEventBase& object)
+    {
+        m_AssetId = object.m_AssetId;
+        Event::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case assetIdPropertyKey:
+                m_AssetId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return Event::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void assetIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/backboard_base.hpp b/include/rive/generated/backboard_base.hpp
new file mode 100644
index 0000000..9f0cf19
--- /dev/null
+++ b/include/rive/generated/backboard_base.hpp
@@ -0,0 +1,38 @@
+#ifndef _RIVE_BACKBOARD_BASE_HPP_
+#define _RIVE_BACKBOARD_BASE_HPP_
+#include "rive/core.hpp"
+namespace rive
+{
+class BackboardBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 23;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case BackboardBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+    void copy(const BackboardBase& object) {}
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override { return false; }
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/bones/bone_base.hpp b/include/rive/generated/bones/bone_base.hpp
new file mode 100644
index 0000000..a6d2a01
--- /dev/null
+++ b/include/rive/generated/bones/bone_base.hpp
@@ -0,0 +1,75 @@
+#ifndef _RIVE_BONE_BASE_HPP_
+#define _RIVE_BONE_BASE_HPP_
+#include "rive/bones/skeletal_component.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class BoneBase : public SkeletalComponent
+{
+protected:
+    typedef SkeletalComponent Super;
+
+public:
+    static const uint16_t typeKey = 40;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case BoneBase::typeKey:
+            case SkeletalComponentBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t lengthPropertyKey = 89;
+
+private:
+    float m_Length = 0.0f;
+
+public:
+    inline float length() const { return m_Length; }
+    void length(float value)
+    {
+        if (m_Length == value)
+        {
+            return;
+        }
+        m_Length = value;
+        lengthChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const BoneBase& object)
+    {
+        m_Length = object.m_Length;
+        SkeletalComponent::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case lengthPropertyKey:
+                m_Length = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return SkeletalComponent::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void lengthChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/bones/cubic_weight_base.hpp b/include/rive/generated/bones/cubic_weight_base.hpp
new file mode 100644
index 0000000..13d016e
--- /dev/null
+++ b/include/rive/generated/bones/cubic_weight_base.hpp
@@ -0,0 +1,126 @@
+#ifndef _RIVE_CUBIC_WEIGHT_BASE_HPP_
+#define _RIVE_CUBIC_WEIGHT_BASE_HPP_
+#include "rive/bones/weight.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class CubicWeightBase : public Weight
+{
+protected:
+    typedef Weight Super;
+
+public:
+    static const uint16_t typeKey = 46;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case CubicWeightBase::typeKey:
+            case WeightBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t inValuesPropertyKey = 110;
+    static const uint16_t inIndicesPropertyKey = 111;
+    static const uint16_t outValuesPropertyKey = 112;
+    static const uint16_t outIndicesPropertyKey = 113;
+
+private:
+    uint32_t m_InValues = 255;
+    uint32_t m_InIndices = 1;
+    uint32_t m_OutValues = 255;
+    uint32_t m_OutIndices = 1;
+
+public:
+    inline uint32_t inValues() const { return m_InValues; }
+    void inValues(uint32_t value)
+    {
+        if (m_InValues == value)
+        {
+            return;
+        }
+        m_InValues = value;
+        inValuesChanged();
+    }
+
+    inline uint32_t inIndices() const { return m_InIndices; }
+    void inIndices(uint32_t value)
+    {
+        if (m_InIndices == value)
+        {
+            return;
+        }
+        m_InIndices = value;
+        inIndicesChanged();
+    }
+
+    inline uint32_t outValues() const { return m_OutValues; }
+    void outValues(uint32_t value)
+    {
+        if (m_OutValues == value)
+        {
+            return;
+        }
+        m_OutValues = value;
+        outValuesChanged();
+    }
+
+    inline uint32_t outIndices() const { return m_OutIndices; }
+    void outIndices(uint32_t value)
+    {
+        if (m_OutIndices == value)
+        {
+            return;
+        }
+        m_OutIndices = value;
+        outIndicesChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const CubicWeightBase& object)
+    {
+        m_InValues = object.m_InValues;
+        m_InIndices = object.m_InIndices;
+        m_OutValues = object.m_OutValues;
+        m_OutIndices = object.m_OutIndices;
+        Weight::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case inValuesPropertyKey:
+                m_InValues = CoreUintType::deserialize(reader);
+                return true;
+            case inIndicesPropertyKey:
+                m_InIndices = CoreUintType::deserialize(reader);
+                return true;
+            case outValuesPropertyKey:
+                m_OutValues = CoreUintType::deserialize(reader);
+                return true;
+            case outIndicesPropertyKey:
+                m_OutIndices = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return Weight::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void inValuesChanged() {}
+    virtual void inIndicesChanged() {}
+    virtual void outValuesChanged() {}
+    virtual void outIndicesChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/bones/root_bone_base.hpp b/include/rive/generated/bones/root_bone_base.hpp
new file mode 100644
index 0000000..6612a1d
--- /dev/null
+++ b/include/rive/generated/bones/root_bone_base.hpp
@@ -0,0 +1,94 @@
+#ifndef _RIVE_ROOT_BONE_BASE_HPP_
+#define _RIVE_ROOT_BONE_BASE_HPP_
+#include "rive/bones/bone.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class RootBoneBase : public Bone
+{
+protected:
+    typedef Bone Super;
+
+public:
+    static const uint16_t typeKey = 41;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case RootBoneBase::typeKey:
+            case BoneBase::typeKey:
+            case SkeletalComponentBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t xPropertyKey = 90;
+    static const uint16_t yPropertyKey = 91;
+
+private:
+    float m_X = 0.0f;
+    float m_Y = 0.0f;
+
+public:
+    inline float x() const override { return m_X; }
+    void x(float value)
+    {
+        if (m_X == value)
+        {
+            return;
+        }
+        m_X = value;
+        xChanged();
+    }
+
+    inline float y() const override { return m_Y; }
+    void y(float value)
+    {
+        if (m_Y == value)
+        {
+            return;
+        }
+        m_Y = value;
+        yChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const RootBoneBase& object)
+    {
+        m_X = object.m_X;
+        m_Y = object.m_Y;
+        Bone::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case xPropertyKey:
+                m_X = CoreDoubleType::deserialize(reader);
+                return true;
+            case yPropertyKey:
+                m_Y = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return Bone::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void xChanged() {}
+    virtual void yChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/bones/skeletal_component_base.hpp b/include/rive/generated/bones/skeletal_component_base.hpp
new file mode 100644
index 0000000..46bd185
--- /dev/null
+++ b/include/rive/generated/bones/skeletal_component_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_SKELETAL_COMPONENT_BASE_HPP_
+#define _RIVE_SKELETAL_COMPONENT_BASE_HPP_
+#include "rive/transform_component.hpp"
+namespace rive
+{
+class SkeletalComponentBase : public TransformComponent
+{
+protected:
+    typedef TransformComponent Super;
+
+public:
+    static const uint16_t typeKey = 39;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case SkeletalComponentBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/bones/skin_base.hpp b/include/rive/generated/bones/skin_base.hpp
new file mode 100644
index 0000000..2cbe0ce
--- /dev/null
+++ b/include/rive/generated/bones/skin_base.hpp
@@ -0,0 +1,162 @@
+#ifndef _RIVE_SKIN_BASE_HPP_
+#define _RIVE_SKIN_BASE_HPP_
+#include "rive/container_component.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class SkinBase : public ContainerComponent
+{
+protected:
+    typedef ContainerComponent Super;
+
+public:
+    static const uint16_t typeKey = 43;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case SkinBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t xxPropertyKey = 104;
+    static const uint16_t yxPropertyKey = 105;
+    static const uint16_t xyPropertyKey = 106;
+    static const uint16_t yyPropertyKey = 107;
+    static const uint16_t txPropertyKey = 108;
+    static const uint16_t tyPropertyKey = 109;
+
+private:
+    float m_Xx = 1.0f;
+    float m_Yx = 0.0f;
+    float m_Xy = 0.0f;
+    float m_Yy = 1.0f;
+    float m_Tx = 0.0f;
+    float m_Ty = 0.0f;
+
+public:
+    inline float xx() const { return m_Xx; }
+    void xx(float value)
+    {
+        if (m_Xx == value)
+        {
+            return;
+        }
+        m_Xx = value;
+        xxChanged();
+    }
+
+    inline float yx() const { return m_Yx; }
+    void yx(float value)
+    {
+        if (m_Yx == value)
+        {
+            return;
+        }
+        m_Yx = value;
+        yxChanged();
+    }
+
+    inline float xy() const { return m_Xy; }
+    void xy(float value)
+    {
+        if (m_Xy == value)
+        {
+            return;
+        }
+        m_Xy = value;
+        xyChanged();
+    }
+
+    inline float yy() const { return m_Yy; }
+    void yy(float value)
+    {
+        if (m_Yy == value)
+        {
+            return;
+        }
+        m_Yy = value;
+        yyChanged();
+    }
+
+    inline float tx() const { return m_Tx; }
+    void tx(float value)
+    {
+        if (m_Tx == value)
+        {
+            return;
+        }
+        m_Tx = value;
+        txChanged();
+    }
+
+    inline float ty() const { return m_Ty; }
+    void ty(float value)
+    {
+        if (m_Ty == value)
+        {
+            return;
+        }
+        m_Ty = value;
+        tyChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const SkinBase& object)
+    {
+        m_Xx = object.m_Xx;
+        m_Yx = object.m_Yx;
+        m_Xy = object.m_Xy;
+        m_Yy = object.m_Yy;
+        m_Tx = object.m_Tx;
+        m_Ty = object.m_Ty;
+        ContainerComponent::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case xxPropertyKey:
+                m_Xx = CoreDoubleType::deserialize(reader);
+                return true;
+            case yxPropertyKey:
+                m_Yx = CoreDoubleType::deserialize(reader);
+                return true;
+            case xyPropertyKey:
+                m_Xy = CoreDoubleType::deserialize(reader);
+                return true;
+            case yyPropertyKey:
+                m_Yy = CoreDoubleType::deserialize(reader);
+                return true;
+            case txPropertyKey:
+                m_Tx = CoreDoubleType::deserialize(reader);
+                return true;
+            case tyPropertyKey:
+                m_Ty = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return ContainerComponent::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void xxChanged() {}
+    virtual void yxChanged() {}
+    virtual void xyChanged() {}
+    virtual void yyChanged() {}
+    virtual void txChanged() {}
+    virtual void tyChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/bones/tendon_base.hpp b/include/rive/generated/bones/tendon_base.hpp
new file mode 100644
index 0000000..5ccaed1
--- /dev/null
+++ b/include/rive/generated/bones/tendon_base.hpp
@@ -0,0 +1,180 @@
+#ifndef _RIVE_TENDON_BASE_HPP_
+#define _RIVE_TENDON_BASE_HPP_
+#include "rive/component.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class TendonBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 44;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TendonBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t boneIdPropertyKey = 95;
+    static const uint16_t xxPropertyKey = 96;
+    static const uint16_t yxPropertyKey = 97;
+    static const uint16_t xyPropertyKey = 98;
+    static const uint16_t yyPropertyKey = 99;
+    static const uint16_t txPropertyKey = 100;
+    static const uint16_t tyPropertyKey = 101;
+
+private:
+    uint32_t m_BoneId = -1;
+    float m_Xx = 1.0f;
+    float m_Yx = 0.0f;
+    float m_Xy = 0.0f;
+    float m_Yy = 1.0f;
+    float m_Tx = 0.0f;
+    float m_Ty = 0.0f;
+
+public:
+    inline uint32_t boneId() const { return m_BoneId; }
+    void boneId(uint32_t value)
+    {
+        if (m_BoneId == value)
+        {
+            return;
+        }
+        m_BoneId = value;
+        boneIdChanged();
+    }
+
+    inline float xx() const { return m_Xx; }
+    void xx(float value)
+    {
+        if (m_Xx == value)
+        {
+            return;
+        }
+        m_Xx = value;
+        xxChanged();
+    }
+
+    inline float yx() const { return m_Yx; }
+    void yx(float value)
+    {
+        if (m_Yx == value)
+        {
+            return;
+        }
+        m_Yx = value;
+        yxChanged();
+    }
+
+    inline float xy() const { return m_Xy; }
+    void xy(float value)
+    {
+        if (m_Xy == value)
+        {
+            return;
+        }
+        m_Xy = value;
+        xyChanged();
+    }
+
+    inline float yy() const { return m_Yy; }
+    void yy(float value)
+    {
+        if (m_Yy == value)
+        {
+            return;
+        }
+        m_Yy = value;
+        yyChanged();
+    }
+
+    inline float tx() const { return m_Tx; }
+    void tx(float value)
+    {
+        if (m_Tx == value)
+        {
+            return;
+        }
+        m_Tx = value;
+        txChanged();
+    }
+
+    inline float ty() const { return m_Ty; }
+    void ty(float value)
+    {
+        if (m_Ty == value)
+        {
+            return;
+        }
+        m_Ty = value;
+        tyChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const TendonBase& object)
+    {
+        m_BoneId = object.m_BoneId;
+        m_Xx = object.m_Xx;
+        m_Yx = object.m_Yx;
+        m_Xy = object.m_Xy;
+        m_Yy = object.m_Yy;
+        m_Tx = object.m_Tx;
+        m_Ty = object.m_Ty;
+        Component::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case boneIdPropertyKey:
+                m_BoneId = CoreUintType::deserialize(reader);
+                return true;
+            case xxPropertyKey:
+                m_Xx = CoreDoubleType::deserialize(reader);
+                return true;
+            case yxPropertyKey:
+                m_Yx = CoreDoubleType::deserialize(reader);
+                return true;
+            case xyPropertyKey:
+                m_Xy = CoreDoubleType::deserialize(reader);
+                return true;
+            case yyPropertyKey:
+                m_Yy = CoreDoubleType::deserialize(reader);
+                return true;
+            case txPropertyKey:
+                m_Tx = CoreDoubleType::deserialize(reader);
+                return true;
+            case tyPropertyKey:
+                m_Ty = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return Component::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void boneIdChanged() {}
+    virtual void xxChanged() {}
+    virtual void yxChanged() {}
+    virtual void xyChanged() {}
+    virtual void yyChanged() {}
+    virtual void txChanged() {}
+    virtual void tyChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/bones/weight_base.hpp b/include/rive/generated/bones/weight_base.hpp
new file mode 100644
index 0000000..a9fa235
--- /dev/null
+++ b/include/rive/generated/bones/weight_base.hpp
@@ -0,0 +1,89 @@
+#ifndef _RIVE_WEIGHT_BASE_HPP_
+#define _RIVE_WEIGHT_BASE_HPP_
+#include "rive/component.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class WeightBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 45;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case WeightBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t valuesPropertyKey = 102;
+    static const uint16_t indicesPropertyKey = 103;
+
+private:
+    uint32_t m_Values = 255;
+    uint32_t m_Indices = 1;
+
+public:
+    inline uint32_t values() const { return m_Values; }
+    void values(uint32_t value)
+    {
+        if (m_Values == value)
+        {
+            return;
+        }
+        m_Values = value;
+        valuesChanged();
+    }
+
+    inline uint32_t indices() const { return m_Indices; }
+    void indices(uint32_t value)
+    {
+        if (m_Indices == value)
+        {
+            return;
+        }
+        m_Indices = value;
+        indicesChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const WeightBase& object)
+    {
+        m_Values = object.m_Values;
+        m_Indices = object.m_Indices;
+        Component::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case valuesPropertyKey:
+                m_Values = CoreUintType::deserialize(reader);
+                return true;
+            case indicesPropertyKey:
+                m_Indices = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return Component::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void valuesChanged() {}
+    virtual void indicesChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/component_base.hpp b/include/rive/generated/component_base.hpp
new file mode 100644
index 0000000..46463bd
--- /dev/null
+++ b/include/rive/generated/component_base.hpp
@@ -0,0 +1,88 @@
+#ifndef _RIVE_COMPONENT_BASE_HPP_
+#define _RIVE_COMPONENT_BASE_HPP_
+#include <string>
+#include "rive/core.hpp"
+#include "rive/core/field_types/core_string_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class ComponentBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 10;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t namePropertyKey = 4;
+    static const uint16_t parentIdPropertyKey = 5;
+
+private:
+    std::string m_Name = "";
+    uint32_t m_ParentId = 0;
+
+public:
+    inline const std::string& name() const { return m_Name; }
+    void name(std::string value)
+    {
+        if (m_Name == value)
+        {
+            return;
+        }
+        m_Name = value;
+        nameChanged();
+    }
+
+    inline uint32_t parentId() const { return m_ParentId; }
+    void parentId(uint32_t value)
+    {
+        if (m_ParentId == value)
+        {
+            return;
+        }
+        m_ParentId = value;
+        parentIdChanged();
+    }
+
+    void copy(const ComponentBase& object)
+    {
+        m_Name = object.m_Name;
+        m_ParentId = object.m_ParentId;
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case namePropertyKey:
+                m_Name = CoreStringType::deserialize(reader);
+                return true;
+            case parentIdPropertyKey:
+                m_ParentId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return false;
+    }
+
+protected:
+    virtual void nameChanged() {}
+    virtual void parentIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/constraints/constraint_base.hpp b/include/rive/generated/constraints/constraint_base.hpp
new file mode 100644
index 0000000..148a3f0
--- /dev/null
+++ b/include/rive/generated/constraints/constraint_base.hpp
@@ -0,0 +1,70 @@
+#ifndef _RIVE_CONSTRAINT_BASE_HPP_
+#define _RIVE_CONSTRAINT_BASE_HPP_
+#include "rive/component.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class ConstraintBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 79;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ConstraintBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t strengthPropertyKey = 172;
+
+private:
+    float m_Strength = 1.0f;
+
+public:
+    inline float strength() const { return m_Strength; }
+    void strength(float value)
+    {
+        if (m_Strength == value)
+        {
+            return;
+        }
+        m_Strength = value;
+        strengthChanged();
+    }
+
+    void copy(const ConstraintBase& object)
+    {
+        m_Strength = object.m_Strength;
+        Component::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case strengthPropertyKey:
+                m_Strength = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return Component::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void strengthChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/constraints/distance_constraint_base.hpp b/include/rive/generated/constraints/distance_constraint_base.hpp
new file mode 100644
index 0000000..0c8b5ae
--- /dev/null
+++ b/include/rive/generated/constraints/distance_constraint_base.hpp
@@ -0,0 +1,92 @@
+#ifndef _RIVE_DISTANCE_CONSTRAINT_BASE_HPP_
+#define _RIVE_DISTANCE_CONSTRAINT_BASE_HPP_
+#include "rive/constraints/targeted_constraint.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class DistanceConstraintBase : public TargetedConstraint
+{
+protected:
+    typedef TargetedConstraint Super;
+
+public:
+    static const uint16_t typeKey = 82;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case DistanceConstraintBase::typeKey:
+            case TargetedConstraintBase::typeKey:
+            case ConstraintBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t distancePropertyKey = 177;
+    static const uint16_t modeValuePropertyKey = 178;
+
+private:
+    float m_Distance = 100.0f;
+    uint32_t m_ModeValue = 0;
+
+public:
+    inline float distance() const { return m_Distance; }
+    void distance(float value)
+    {
+        if (m_Distance == value)
+        {
+            return;
+        }
+        m_Distance = value;
+        distanceChanged();
+    }
+
+    inline uint32_t modeValue() const { return m_ModeValue; }
+    void modeValue(uint32_t value)
+    {
+        if (m_ModeValue == value)
+        {
+            return;
+        }
+        m_ModeValue = value;
+        modeValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const DistanceConstraintBase& object)
+    {
+        m_Distance = object.m_Distance;
+        m_ModeValue = object.m_ModeValue;
+        TargetedConstraint::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case distancePropertyKey:
+                m_Distance = CoreDoubleType::deserialize(reader);
+                return true;
+            case modeValuePropertyKey:
+                m_ModeValue = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return TargetedConstraint::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void distanceChanged() {}
+    virtual void modeValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/constraints/follow_path_constraint_base.hpp b/include/rive/generated/constraints/follow_path_constraint_base.hpp
new file mode 100644
index 0000000..089ba15
--- /dev/null
+++ b/include/rive/generated/constraints/follow_path_constraint_base.hpp
@@ -0,0 +1,111 @@
+#ifndef _RIVE_FOLLOW_PATH_CONSTRAINT_BASE_HPP_
+#define _RIVE_FOLLOW_PATH_CONSTRAINT_BASE_HPP_
+#include "rive/constraints/transform_space_constraint.hpp"
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class FollowPathConstraintBase : public TransformSpaceConstraint
+{
+protected:
+    typedef TransformSpaceConstraint Super;
+
+public:
+    static const uint16_t typeKey = 165;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case FollowPathConstraintBase::typeKey:
+            case TransformSpaceConstraintBase::typeKey:
+            case TargetedConstraintBase::typeKey:
+            case ConstraintBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t distancePropertyKey = 363;
+    static const uint16_t orientPropertyKey = 364;
+    static const uint16_t offsetPropertyKey = 365;
+
+private:
+    float m_Distance = 0.0f;
+    bool m_Orient = true;
+    bool m_Offset = false;
+
+public:
+    inline float distance() const { return m_Distance; }
+    void distance(float value)
+    {
+        if (m_Distance == value)
+        {
+            return;
+        }
+        m_Distance = value;
+        distanceChanged();
+    }
+
+    inline bool orient() const { return m_Orient; }
+    void orient(bool value)
+    {
+        if (m_Orient == value)
+        {
+            return;
+        }
+        m_Orient = value;
+        orientChanged();
+    }
+
+    inline bool offset() const { return m_Offset; }
+    void offset(bool value)
+    {
+        if (m_Offset == value)
+        {
+            return;
+        }
+        m_Offset = value;
+        offsetChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const FollowPathConstraintBase& object)
+    {
+        m_Distance = object.m_Distance;
+        m_Orient = object.m_Orient;
+        m_Offset = object.m_Offset;
+        TransformSpaceConstraint::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case distancePropertyKey:
+                m_Distance = CoreDoubleType::deserialize(reader);
+                return true;
+            case orientPropertyKey:
+                m_Orient = CoreBoolType::deserialize(reader);
+                return true;
+            case offsetPropertyKey:
+                m_Offset = CoreBoolType::deserialize(reader);
+                return true;
+        }
+        return TransformSpaceConstraint::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void distanceChanged() {}
+    virtual void orientChanged() {}
+    virtual void offsetChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/constraints/ik_constraint_base.hpp b/include/rive/generated/constraints/ik_constraint_base.hpp
new file mode 100644
index 0000000..ec38ee0
--- /dev/null
+++ b/include/rive/generated/constraints/ik_constraint_base.hpp
@@ -0,0 +1,92 @@
+#ifndef _RIVE_I_KCONSTRAINT_BASE_HPP_
+#define _RIVE_I_KCONSTRAINT_BASE_HPP_
+#include "rive/constraints/targeted_constraint.hpp"
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class IKConstraintBase : public TargetedConstraint
+{
+protected:
+    typedef TargetedConstraint Super;
+
+public:
+    static const uint16_t typeKey = 81;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case IKConstraintBase::typeKey:
+            case TargetedConstraintBase::typeKey:
+            case ConstraintBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t invertDirectionPropertyKey = 174;
+    static const uint16_t parentBoneCountPropertyKey = 175;
+
+private:
+    bool m_InvertDirection = false;
+    uint32_t m_ParentBoneCount = 0;
+
+public:
+    inline bool invertDirection() const { return m_InvertDirection; }
+    void invertDirection(bool value)
+    {
+        if (m_InvertDirection == value)
+        {
+            return;
+        }
+        m_InvertDirection = value;
+        invertDirectionChanged();
+    }
+
+    inline uint32_t parentBoneCount() const { return m_ParentBoneCount; }
+    void parentBoneCount(uint32_t value)
+    {
+        if (m_ParentBoneCount == value)
+        {
+            return;
+        }
+        m_ParentBoneCount = value;
+        parentBoneCountChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const IKConstraintBase& object)
+    {
+        m_InvertDirection = object.m_InvertDirection;
+        m_ParentBoneCount = object.m_ParentBoneCount;
+        TargetedConstraint::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case invertDirectionPropertyKey:
+                m_InvertDirection = CoreBoolType::deserialize(reader);
+                return true;
+            case parentBoneCountPropertyKey:
+                m_ParentBoneCount = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return TargetedConstraint::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void invertDirectionChanged() {}
+    virtual void parentBoneCountChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/constraints/rotation_constraint_base.hpp b/include/rive/generated/constraints/rotation_constraint_base.hpp
new file mode 100644
index 0000000..6e34fcb
--- /dev/null
+++ b/include/rive/generated/constraints/rotation_constraint_base.hpp
@@ -0,0 +1,40 @@
+#ifndef _RIVE_ROTATION_CONSTRAINT_BASE_HPP_
+#define _RIVE_ROTATION_CONSTRAINT_BASE_HPP_
+#include "rive/constraints/transform_component_constraint.hpp"
+namespace rive
+{
+class RotationConstraintBase : public TransformComponentConstraint
+{
+protected:
+    typedef TransformComponentConstraint Super;
+
+public:
+    static const uint16_t typeKey = 89;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case RotationConstraintBase::typeKey:
+            case TransformComponentConstraintBase::typeKey:
+            case TransformSpaceConstraintBase::typeKey:
+            case TargetedConstraintBase::typeKey:
+            case ConstraintBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/constraints/scale_constraint_base.hpp b/include/rive/generated/constraints/scale_constraint_base.hpp
new file mode 100644
index 0000000..abe8729
--- /dev/null
+++ b/include/rive/generated/constraints/scale_constraint_base.hpp
@@ -0,0 +1,41 @@
+#ifndef _RIVE_SCALE_CONSTRAINT_BASE_HPP_
+#define _RIVE_SCALE_CONSTRAINT_BASE_HPP_
+#include "rive/constraints/transform_component_constraint_y.hpp"
+namespace rive
+{
+class ScaleConstraintBase : public TransformComponentConstraintY
+{
+protected:
+    typedef TransformComponentConstraintY Super;
+
+public:
+    static const uint16_t typeKey = 88;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ScaleConstraintBase::typeKey:
+            case TransformComponentConstraintYBase::typeKey:
+            case TransformComponentConstraintBase::typeKey:
+            case TransformSpaceConstraintBase::typeKey:
+            case TargetedConstraintBase::typeKey:
+            case ConstraintBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/constraints/targeted_constraint_base.hpp b/include/rive/generated/constraints/targeted_constraint_base.hpp
new file mode 100644
index 0000000..e236d0f
--- /dev/null
+++ b/include/rive/generated/constraints/targeted_constraint_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_TARGETED_CONSTRAINT_BASE_HPP_
+#define _RIVE_TARGETED_CONSTRAINT_BASE_HPP_
+#include "rive/constraints/constraint.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class TargetedConstraintBase : public Constraint
+{
+protected:
+    typedef Constraint Super;
+
+public:
+    static const uint16_t typeKey = 80;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TargetedConstraintBase::typeKey:
+            case ConstraintBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t targetIdPropertyKey = 173;
+
+private:
+    uint32_t m_TargetId = -1;
+
+public:
+    inline uint32_t targetId() const { return m_TargetId; }
+    void targetId(uint32_t value)
+    {
+        if (m_TargetId == value)
+        {
+            return;
+        }
+        m_TargetId = value;
+        targetIdChanged();
+    }
+
+    void copy(const TargetedConstraintBase& object)
+    {
+        m_TargetId = object.m_TargetId;
+        Constraint::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case targetIdPropertyKey:
+                m_TargetId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return Constraint::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void targetIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/constraints/transform_component_constraint_base.hpp b/include/rive/generated/constraints/transform_component_constraint_base.hpp
new file mode 100644
index 0000000..e52573a
--- /dev/null
+++ b/include/rive/generated/constraints/transform_component_constraint_base.hpp
@@ -0,0 +1,201 @@
+#ifndef _RIVE_TRANSFORM_COMPONENT_CONSTRAINT_BASE_HPP_
+#define _RIVE_TRANSFORM_COMPONENT_CONSTRAINT_BASE_HPP_
+#include "rive/constraints/transform_space_constraint.hpp"
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class TransformComponentConstraintBase : public TransformSpaceConstraint
+{
+protected:
+    typedef TransformSpaceConstraint Super;
+
+public:
+    static const uint16_t typeKey = 85;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransformComponentConstraintBase::typeKey:
+            case TransformSpaceConstraintBase::typeKey:
+            case TargetedConstraintBase::typeKey:
+            case ConstraintBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t minMaxSpaceValuePropertyKey = 195;
+    static const uint16_t copyFactorPropertyKey = 182;
+    static const uint16_t minValuePropertyKey = 183;
+    static const uint16_t maxValuePropertyKey = 184;
+    static const uint16_t offsetPropertyKey = 188;
+    static const uint16_t doesCopyPropertyKey = 189;
+    static const uint16_t minPropertyKey = 190;
+    static const uint16_t maxPropertyKey = 191;
+
+private:
+    uint32_t m_MinMaxSpaceValue = 0;
+    float m_CopyFactor = 1.0f;
+    float m_MinValue = 0.0f;
+    float m_MaxValue = 0.0f;
+    bool m_Offset = false;
+    bool m_DoesCopy = true;
+    bool m_Min = false;
+    bool m_Max = false;
+
+public:
+    inline uint32_t minMaxSpaceValue() const { return m_MinMaxSpaceValue; }
+    void minMaxSpaceValue(uint32_t value)
+    {
+        if (m_MinMaxSpaceValue == value)
+        {
+            return;
+        }
+        m_MinMaxSpaceValue = value;
+        minMaxSpaceValueChanged();
+    }
+
+    inline float copyFactor() const { return m_CopyFactor; }
+    void copyFactor(float value)
+    {
+        if (m_CopyFactor == value)
+        {
+            return;
+        }
+        m_CopyFactor = value;
+        copyFactorChanged();
+    }
+
+    inline float minValue() const { return m_MinValue; }
+    void minValue(float value)
+    {
+        if (m_MinValue == value)
+        {
+            return;
+        }
+        m_MinValue = value;
+        minValueChanged();
+    }
+
+    inline float maxValue() const { return m_MaxValue; }
+    void maxValue(float value)
+    {
+        if (m_MaxValue == value)
+        {
+            return;
+        }
+        m_MaxValue = value;
+        maxValueChanged();
+    }
+
+    inline bool offset() const { return m_Offset; }
+    void offset(bool value)
+    {
+        if (m_Offset == value)
+        {
+            return;
+        }
+        m_Offset = value;
+        offsetChanged();
+    }
+
+    inline bool doesCopy() const { return m_DoesCopy; }
+    void doesCopy(bool value)
+    {
+        if (m_DoesCopy == value)
+        {
+            return;
+        }
+        m_DoesCopy = value;
+        doesCopyChanged();
+    }
+
+    inline bool min() const { return m_Min; }
+    void min(bool value)
+    {
+        if (m_Min == value)
+        {
+            return;
+        }
+        m_Min = value;
+        minChanged();
+    }
+
+    inline bool max() const { return m_Max; }
+    void max(bool value)
+    {
+        if (m_Max == value)
+        {
+            return;
+        }
+        m_Max = value;
+        maxChanged();
+    }
+
+    void copy(const TransformComponentConstraintBase& object)
+    {
+        m_MinMaxSpaceValue = object.m_MinMaxSpaceValue;
+        m_CopyFactor = object.m_CopyFactor;
+        m_MinValue = object.m_MinValue;
+        m_MaxValue = object.m_MaxValue;
+        m_Offset = object.m_Offset;
+        m_DoesCopy = object.m_DoesCopy;
+        m_Min = object.m_Min;
+        m_Max = object.m_Max;
+        TransformSpaceConstraint::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case minMaxSpaceValuePropertyKey:
+                m_MinMaxSpaceValue = CoreUintType::deserialize(reader);
+                return true;
+            case copyFactorPropertyKey:
+                m_CopyFactor = CoreDoubleType::deserialize(reader);
+                return true;
+            case minValuePropertyKey:
+                m_MinValue = CoreDoubleType::deserialize(reader);
+                return true;
+            case maxValuePropertyKey:
+                m_MaxValue = CoreDoubleType::deserialize(reader);
+                return true;
+            case offsetPropertyKey:
+                m_Offset = CoreBoolType::deserialize(reader);
+                return true;
+            case doesCopyPropertyKey:
+                m_DoesCopy = CoreBoolType::deserialize(reader);
+                return true;
+            case minPropertyKey:
+                m_Min = CoreBoolType::deserialize(reader);
+                return true;
+            case maxPropertyKey:
+                m_Max = CoreBoolType::deserialize(reader);
+                return true;
+        }
+        return TransformSpaceConstraint::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void minMaxSpaceValueChanged() {}
+    virtual void copyFactorChanged() {}
+    virtual void minValueChanged() {}
+    virtual void maxValueChanged() {}
+    virtual void offsetChanged() {}
+    virtual void doesCopyChanged() {}
+    virtual void minChanged() {}
+    virtual void maxChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/constraints/transform_component_constraint_y_base.hpp b/include/rive/generated/constraints/transform_component_constraint_y_base.hpp
new file mode 100644
index 0000000..eea8b04
--- /dev/null
+++ b/include/rive/generated/constraints/transform_component_constraint_y_base.hpp
@@ -0,0 +1,165 @@
+#ifndef _RIVE_TRANSFORM_COMPONENT_CONSTRAINT_YBASE_HPP_
+#define _RIVE_TRANSFORM_COMPONENT_CONSTRAINT_YBASE_HPP_
+#include "rive/constraints/transform_component_constraint.hpp"
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class TransformComponentConstraintYBase : public TransformComponentConstraint
+{
+protected:
+    typedef TransformComponentConstraint Super;
+
+public:
+    static const uint16_t typeKey = 86;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransformComponentConstraintYBase::typeKey:
+            case TransformComponentConstraintBase::typeKey:
+            case TransformSpaceConstraintBase::typeKey:
+            case TargetedConstraintBase::typeKey:
+            case ConstraintBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t copyFactorYPropertyKey = 185;
+    static const uint16_t minValueYPropertyKey = 186;
+    static const uint16_t maxValueYPropertyKey = 187;
+    static const uint16_t doesCopyYPropertyKey = 192;
+    static const uint16_t minYPropertyKey = 193;
+    static const uint16_t maxYPropertyKey = 194;
+
+private:
+    float m_CopyFactorY = 1.0f;
+    float m_MinValueY = 0.0f;
+    float m_MaxValueY = 0.0f;
+    bool m_DoesCopyY = true;
+    bool m_MinY = false;
+    bool m_MaxY = false;
+
+public:
+    inline float copyFactorY() const { return m_CopyFactorY; }
+    void copyFactorY(float value)
+    {
+        if (m_CopyFactorY == value)
+        {
+            return;
+        }
+        m_CopyFactorY = value;
+        copyFactorYChanged();
+    }
+
+    inline float minValueY() const { return m_MinValueY; }
+    void minValueY(float value)
+    {
+        if (m_MinValueY == value)
+        {
+            return;
+        }
+        m_MinValueY = value;
+        minValueYChanged();
+    }
+
+    inline float maxValueY() const { return m_MaxValueY; }
+    void maxValueY(float value)
+    {
+        if (m_MaxValueY == value)
+        {
+            return;
+        }
+        m_MaxValueY = value;
+        maxValueYChanged();
+    }
+
+    inline bool doesCopyY() const { return m_DoesCopyY; }
+    void doesCopyY(bool value)
+    {
+        if (m_DoesCopyY == value)
+        {
+            return;
+        }
+        m_DoesCopyY = value;
+        doesCopyYChanged();
+    }
+
+    inline bool minY() const { return m_MinY; }
+    void minY(bool value)
+    {
+        if (m_MinY == value)
+        {
+            return;
+        }
+        m_MinY = value;
+        minYChanged();
+    }
+
+    inline bool maxY() const { return m_MaxY; }
+    void maxY(bool value)
+    {
+        if (m_MaxY == value)
+        {
+            return;
+        }
+        m_MaxY = value;
+        maxYChanged();
+    }
+
+    void copy(const TransformComponentConstraintYBase& object)
+    {
+        m_CopyFactorY = object.m_CopyFactorY;
+        m_MinValueY = object.m_MinValueY;
+        m_MaxValueY = object.m_MaxValueY;
+        m_DoesCopyY = object.m_DoesCopyY;
+        m_MinY = object.m_MinY;
+        m_MaxY = object.m_MaxY;
+        TransformComponentConstraint::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case copyFactorYPropertyKey:
+                m_CopyFactorY = CoreDoubleType::deserialize(reader);
+                return true;
+            case minValueYPropertyKey:
+                m_MinValueY = CoreDoubleType::deserialize(reader);
+                return true;
+            case maxValueYPropertyKey:
+                m_MaxValueY = CoreDoubleType::deserialize(reader);
+                return true;
+            case doesCopyYPropertyKey:
+                m_DoesCopyY = CoreBoolType::deserialize(reader);
+                return true;
+            case minYPropertyKey:
+                m_MinY = CoreBoolType::deserialize(reader);
+                return true;
+            case maxYPropertyKey:
+                m_MaxY = CoreBoolType::deserialize(reader);
+                return true;
+        }
+        return TransformComponentConstraint::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void copyFactorYChanged() {}
+    virtual void minValueYChanged() {}
+    virtual void maxValueYChanged() {}
+    virtual void doesCopyYChanged() {}
+    virtual void minYChanged() {}
+    virtual void maxYChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/constraints/transform_constraint_base.hpp b/include/rive/generated/constraints/transform_constraint_base.hpp
new file mode 100644
index 0000000..e3a4d76
--- /dev/null
+++ b/include/rive/generated/constraints/transform_constraint_base.hpp
@@ -0,0 +1,92 @@
+#ifndef _RIVE_TRANSFORM_CONSTRAINT_BASE_HPP_
+#define _RIVE_TRANSFORM_CONSTRAINT_BASE_HPP_
+#include "rive/constraints/transform_space_constraint.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class TransformConstraintBase : public TransformSpaceConstraint
+{
+protected:
+    typedef TransformSpaceConstraint Super;
+
+public:
+    static const uint16_t typeKey = 83;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransformConstraintBase::typeKey:
+            case TransformSpaceConstraintBase::typeKey:
+            case TargetedConstraintBase::typeKey:
+            case ConstraintBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t originXPropertyKey = 372;
+    static const uint16_t originYPropertyKey = 373;
+
+private:
+    float m_OriginX = 0.0f;
+    float m_OriginY = 0.0f;
+
+public:
+    inline float originX() const { return m_OriginX; }
+    void originX(float value)
+    {
+        if (m_OriginX == value)
+        {
+            return;
+        }
+        m_OriginX = value;
+        originXChanged();
+    }
+
+    inline float originY() const { return m_OriginY; }
+    void originY(float value)
+    {
+        if (m_OriginY == value)
+        {
+            return;
+        }
+        m_OriginY = value;
+        originYChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const TransformConstraintBase& object)
+    {
+        m_OriginX = object.m_OriginX;
+        m_OriginY = object.m_OriginY;
+        TransformSpaceConstraint::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case originXPropertyKey:
+                m_OriginX = CoreDoubleType::deserialize(reader);
+                return true;
+            case originYPropertyKey:
+                m_OriginY = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return TransformSpaceConstraint::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void originXChanged() {}
+    virtual void originYChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/constraints/transform_space_constraint_base.hpp b/include/rive/generated/constraints/transform_space_constraint_base.hpp
new file mode 100644
index 0000000..29f4466
--- /dev/null
+++ b/include/rive/generated/constraints/transform_space_constraint_base.hpp
@@ -0,0 +1,90 @@
+#ifndef _RIVE_TRANSFORM_SPACE_CONSTRAINT_BASE_HPP_
+#define _RIVE_TRANSFORM_SPACE_CONSTRAINT_BASE_HPP_
+#include "rive/constraints/targeted_constraint.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class TransformSpaceConstraintBase : public TargetedConstraint
+{
+protected:
+    typedef TargetedConstraint Super;
+
+public:
+    static const uint16_t typeKey = 90;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransformSpaceConstraintBase::typeKey:
+            case TargetedConstraintBase::typeKey:
+            case ConstraintBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t sourceSpaceValuePropertyKey = 179;
+    static const uint16_t destSpaceValuePropertyKey = 180;
+
+private:
+    uint32_t m_SourceSpaceValue = 0;
+    uint32_t m_DestSpaceValue = 0;
+
+public:
+    inline uint32_t sourceSpaceValue() const { return m_SourceSpaceValue; }
+    void sourceSpaceValue(uint32_t value)
+    {
+        if (m_SourceSpaceValue == value)
+        {
+            return;
+        }
+        m_SourceSpaceValue = value;
+        sourceSpaceValueChanged();
+    }
+
+    inline uint32_t destSpaceValue() const { return m_DestSpaceValue; }
+    void destSpaceValue(uint32_t value)
+    {
+        if (m_DestSpaceValue == value)
+        {
+            return;
+        }
+        m_DestSpaceValue = value;
+        destSpaceValueChanged();
+    }
+
+    void copy(const TransformSpaceConstraintBase& object)
+    {
+        m_SourceSpaceValue = object.m_SourceSpaceValue;
+        m_DestSpaceValue = object.m_DestSpaceValue;
+        TargetedConstraint::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case sourceSpaceValuePropertyKey:
+                m_SourceSpaceValue = CoreUintType::deserialize(reader);
+                return true;
+            case destSpaceValuePropertyKey:
+                m_DestSpaceValue = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return TargetedConstraint::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void sourceSpaceValueChanged() {}
+    virtual void destSpaceValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/constraints/translation_constraint_base.hpp b/include/rive/generated/constraints/translation_constraint_base.hpp
new file mode 100644
index 0000000..26c9751
--- /dev/null
+++ b/include/rive/generated/constraints/translation_constraint_base.hpp
@@ -0,0 +1,41 @@
+#ifndef _RIVE_TRANSLATION_CONSTRAINT_BASE_HPP_
+#define _RIVE_TRANSLATION_CONSTRAINT_BASE_HPP_
+#include "rive/constraints/transform_component_constraint_y.hpp"
+namespace rive
+{
+class TranslationConstraintBase : public TransformComponentConstraintY
+{
+protected:
+    typedef TransformComponentConstraintY Super;
+
+public:
+    static const uint16_t typeKey = 87;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TranslationConstraintBase::typeKey:
+            case TransformComponentConstraintYBase::typeKey:
+            case TransformComponentConstraintBase::typeKey:
+            case TransformSpaceConstraintBase::typeKey:
+            case TargetedConstraintBase::typeKey:
+            case ConstraintBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/container_component_base.hpp b/include/rive/generated/container_component_base.hpp
new file mode 100644
index 0000000..7bdb624
--- /dev/null
+++ b/include/rive/generated/container_component_base.hpp
@@ -0,0 +1,34 @@
+#ifndef _RIVE_CONTAINER_COMPONENT_BASE_HPP_
+#define _RIVE_CONTAINER_COMPONENT_BASE_HPP_
+#include "rive/component.hpp"
+namespace rive
+{
+class ContainerComponentBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 11;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/core_registry.hpp b/include/rive/generated/core_registry.hpp
new file mode 100644
index 0000000..bb2d399
--- /dev/null
+++ b/include/rive/generated/core_registry.hpp
@@ -0,0 +1,3794 @@
+#ifndef _RIVE_CORE_REGISTRY_HPP_
+#define _RIVE_CORE_REGISTRY_HPP_
+#include "rive/animation/advanceable_state.hpp"
+#include "rive/animation/animation.hpp"
+#include "rive/animation/animation_state.hpp"
+#include "rive/animation/any_state.hpp"
+#include "rive/animation/blend_animation.hpp"
+#include "rive/animation/blend_animation_1d.hpp"
+#include "rive/animation/blend_animation_direct.hpp"
+#include "rive/animation/blend_state.hpp"
+#include "rive/animation/blend_state_1d.hpp"
+#include "rive/animation/blend_state_direct.hpp"
+#include "rive/animation/blend_state_transition.hpp"
+#include "rive/animation/cubic_ease_interpolator.hpp"
+#include "rive/animation/cubic_interpolator.hpp"
+#include "rive/animation/cubic_interpolator_component.hpp"
+#include "rive/animation/cubic_value_interpolator.hpp"
+#include "rive/animation/elastic_interpolator.hpp"
+#include "rive/animation/entry_state.hpp"
+#include "rive/animation/exit_state.hpp"
+#include "rive/animation/interpolating_keyframe.hpp"
+#include "rive/animation/keyed_object.hpp"
+#include "rive/animation/keyed_property.hpp"
+#include "rive/animation/keyframe.hpp"
+#include "rive/animation/keyframe_bool.hpp"
+#include "rive/animation/keyframe_callback.hpp"
+#include "rive/animation/keyframe_color.hpp"
+#include "rive/animation/keyframe_double.hpp"
+#include "rive/animation/keyframe_id.hpp"
+#include "rive/animation/keyframe_interpolator.hpp"
+#include "rive/animation/keyframe_string.hpp"
+#include "rive/animation/keyframe_uint.hpp"
+#include "rive/animation/layer_state.hpp"
+#include "rive/animation/linear_animation.hpp"
+#include "rive/animation/listener_action.hpp"
+#include "rive/animation/listener_align_target.hpp"
+#include "rive/animation/listener_bool_change.hpp"
+#include "rive/animation/listener_fire_event.hpp"
+#include "rive/animation/listener_input_change.hpp"
+#include "rive/animation/listener_number_change.hpp"
+#include "rive/animation/listener_trigger_change.hpp"
+#include "rive/animation/listener_viewmodel_change.hpp"
+#include "rive/animation/nested_bool.hpp"
+#include "rive/animation/nested_input.hpp"
+#include "rive/animation/nested_linear_animation.hpp"
+#include "rive/animation/nested_number.hpp"
+#include "rive/animation/nested_remap_animation.hpp"
+#include "rive/animation/nested_simple_animation.hpp"
+#include "rive/animation/nested_state_machine.hpp"
+#include "rive/animation/nested_trigger.hpp"
+#include "rive/animation/state_machine.hpp"
+#include "rive/animation/state_machine_bool.hpp"
+#include "rive/animation/state_machine_component.hpp"
+#include "rive/animation/state_machine_fire_event.hpp"
+#include "rive/animation/state_machine_input.hpp"
+#include "rive/animation/state_machine_layer.hpp"
+#include "rive/animation/state_machine_layer_component.hpp"
+#include "rive/animation/state_machine_listener.hpp"
+#include "rive/animation/state_machine_number.hpp"
+#include "rive/animation/state_machine_trigger.hpp"
+#include "rive/animation/state_transition.hpp"
+#include "rive/animation/transition_artboard_condition.hpp"
+#include "rive/animation/transition_bool_condition.hpp"
+#include "rive/animation/transition_comparator.hpp"
+#include "rive/animation/transition_condition.hpp"
+#include "rive/animation/transition_input_condition.hpp"
+#include "rive/animation/transition_number_condition.hpp"
+#include "rive/animation/transition_property_artboard_comparator.hpp"
+#include "rive/animation/transition_property_comparator.hpp"
+#include "rive/animation/transition_property_viewmodel_comparator.hpp"
+#include "rive/animation/transition_trigger_condition.hpp"
+#include "rive/animation/transition_value_boolean_comparator.hpp"
+#include "rive/animation/transition_value_color_comparator.hpp"
+#include "rive/animation/transition_value_comparator.hpp"
+#include "rive/animation/transition_value_condition.hpp"
+#include "rive/animation/transition_value_enum_comparator.hpp"
+#include "rive/animation/transition_value_number_comparator.hpp"
+#include "rive/animation/transition_value_string_comparator.hpp"
+#include "rive/animation/transition_viewmodel_condition.hpp"
+#include "rive/artboard.hpp"
+#include "rive/assets/asset.hpp"
+#include "rive/assets/audio_asset.hpp"
+#include "rive/assets/drawable_asset.hpp"
+#include "rive/assets/export_audio.hpp"
+#include "rive/assets/file_asset.hpp"
+#include "rive/assets/file_asset_contents.hpp"
+#include "rive/assets/folder.hpp"
+#include "rive/assets/font_asset.hpp"
+#include "rive/assets/image_asset.hpp"
+#include "rive/audio_event.hpp"
+#include "rive/backboard.hpp"
+#include "rive/bones/bone.hpp"
+#include "rive/bones/cubic_weight.hpp"
+#include "rive/bones/root_bone.hpp"
+#include "rive/bones/skeletal_component.hpp"
+#include "rive/bones/skin.hpp"
+#include "rive/bones/tendon.hpp"
+#include "rive/bones/weight.hpp"
+#include "rive/component.hpp"
+#include "rive/constraints/constraint.hpp"
+#include "rive/constraints/distance_constraint.hpp"
+#include "rive/constraints/follow_path_constraint.hpp"
+#include "rive/constraints/ik_constraint.hpp"
+#include "rive/constraints/rotation_constraint.hpp"
+#include "rive/constraints/scale_constraint.hpp"
+#include "rive/constraints/targeted_constraint.hpp"
+#include "rive/constraints/transform_component_constraint.hpp"
+#include "rive/constraints/transform_component_constraint_y.hpp"
+#include "rive/constraints/transform_constraint.hpp"
+#include "rive/constraints/transform_space_constraint.hpp"
+#include "rive/constraints/translation_constraint.hpp"
+#include "rive/container_component.hpp"
+#include "rive/custom_property.hpp"
+#include "rive/custom_property_boolean.hpp"
+#include "rive/custom_property_number.hpp"
+#include "rive/custom_property_string.hpp"
+#include "rive/data_bind/bindable_property.hpp"
+#include "rive/data_bind/bindable_property_boolean.hpp"
+#include "rive/data_bind/bindable_property_color.hpp"
+#include "rive/data_bind/bindable_property_enum.hpp"
+#include "rive/data_bind/bindable_property_number.hpp"
+#include "rive/data_bind/bindable_property_string.hpp"
+#include "rive/data_bind/converters/data_converter.hpp"
+#include "rive/data_bind/converters/data_converter_group.hpp"
+#include "rive/data_bind/converters/data_converter_group_item.hpp"
+#include "rive/data_bind/converters/data_converter_operation.hpp"
+#include "rive/data_bind/converters/data_converter_rounder.hpp"
+#include "rive/data_bind/converters/data_converter_to_string.hpp"
+#include "rive/data_bind/data_bind.hpp"
+#include "rive/data_bind/data_bind_context.hpp"
+#include "rive/draw_rules.hpp"
+#include "rive/draw_target.hpp"
+#include "rive/drawable.hpp"
+#include "rive/event.hpp"
+#include "rive/joystick.hpp"
+#include "rive/layout/axis.hpp"
+#include "rive/layout/axis_x.hpp"
+#include "rive/layout/axis_y.hpp"
+#include "rive/layout/layout_component_style.hpp"
+#include "rive/layout/n_slicer.hpp"
+#include "rive/layout/n_slicer_tile_mode.hpp"
+#include "rive/layout_component.hpp"
+#include "rive/nested_animation.hpp"
+#include "rive/nested_artboard.hpp"
+#include "rive/nested_artboard_layout.hpp"
+#include "rive/nested_artboard_leaf.hpp"
+#include "rive/node.hpp"
+#include "rive/open_url_event.hpp"
+#include "rive/shapes/clipping_shape.hpp"
+#include "rive/shapes/contour_mesh_vertex.hpp"
+#include "rive/shapes/cubic_asymmetric_vertex.hpp"
+#include "rive/shapes/cubic_detached_vertex.hpp"
+#include "rive/shapes/cubic_mirrored_vertex.hpp"
+#include "rive/shapes/cubic_vertex.hpp"
+#include "rive/shapes/ellipse.hpp"
+#include "rive/shapes/image.hpp"
+#include "rive/shapes/mesh.hpp"
+#include "rive/shapes/mesh_vertex.hpp"
+#include "rive/shapes/paint/fill.hpp"
+#include "rive/shapes/paint/gradient_stop.hpp"
+#include "rive/shapes/paint/linear_gradient.hpp"
+#include "rive/shapes/paint/radial_gradient.hpp"
+#include "rive/shapes/paint/shape_paint.hpp"
+#include "rive/shapes/paint/solid_color.hpp"
+#include "rive/shapes/paint/stroke.hpp"
+#include "rive/shapes/paint/trim_path.hpp"
+#include "rive/shapes/parametric_path.hpp"
+#include "rive/shapes/path.hpp"
+#include "rive/shapes/path_vertex.hpp"
+#include "rive/shapes/points_path.hpp"
+#include "rive/shapes/polygon.hpp"
+#include "rive/shapes/rectangle.hpp"
+#include "rive/shapes/shape.hpp"
+#include "rive/shapes/star.hpp"
+#include "rive/shapes/straight_vertex.hpp"
+#include "rive/shapes/triangle.hpp"
+#include "rive/shapes/vertex.hpp"
+#include "rive/solo.hpp"
+#include "rive/text/text.hpp"
+#include "rive/text/text_modifier.hpp"
+#include "rive/text/text_modifier_group.hpp"
+#include "rive/text/text_modifier_range.hpp"
+#include "rive/text/text_shape_modifier.hpp"
+#include "rive/text/text_style.hpp"
+#include "rive/text/text_style_axis.hpp"
+#include "rive/text/text_style_feature.hpp"
+#include "rive/text/text_value_run.hpp"
+#include "rive/text/text_variation_modifier.hpp"
+#include "rive/transform_component.hpp"
+#include "rive/viewmodel/data_enum.hpp"
+#include "rive/viewmodel/data_enum_value.hpp"
+#include "rive/viewmodel/viewmodel.hpp"
+#include "rive/viewmodel/viewmodel_component.hpp"
+#include "rive/viewmodel/viewmodel_instance.hpp"
+#include "rive/viewmodel/viewmodel_instance_boolean.hpp"
+#include "rive/viewmodel/viewmodel_instance_color.hpp"
+#include "rive/viewmodel/viewmodel_instance_enum.hpp"
+#include "rive/viewmodel/viewmodel_instance_list.hpp"
+#include "rive/viewmodel/viewmodel_instance_list_item.hpp"
+#include "rive/viewmodel/viewmodel_instance_number.hpp"
+#include "rive/viewmodel/viewmodel_instance_string.hpp"
+#include "rive/viewmodel/viewmodel_instance_value.hpp"
+#include "rive/viewmodel/viewmodel_instance_viewmodel.hpp"
+#include "rive/viewmodel/viewmodel_property.hpp"
+#include "rive/viewmodel/viewmodel_property_boolean.hpp"
+#include "rive/viewmodel/viewmodel_property_color.hpp"
+#include "rive/viewmodel/viewmodel_property_enum.hpp"
+#include "rive/viewmodel/viewmodel_property_list.hpp"
+#include "rive/viewmodel/viewmodel_property_number.hpp"
+#include "rive/viewmodel/viewmodel_property_string.hpp"
+#include "rive/viewmodel/viewmodel_property_viewmodel.hpp"
+#include "rive/world_transform_component.hpp"
+namespace rive
+{
+class CoreRegistry
+{
+public:
+    static Core* makeCoreInstance(int typeKey)
+    {
+        switch (typeKey)
+        {
+            case ViewModelInstanceListItemBase::typeKey:
+                return new ViewModelInstanceListItem();
+            case ViewModelInstanceColorBase::typeKey:
+                return new ViewModelInstanceColor();
+            case ViewModelComponentBase::typeKey:
+                return new ViewModelComponent();
+            case ViewModelPropertyBase::typeKey:
+                return new ViewModelProperty();
+            case ViewModelPropertyNumberBase::typeKey:
+                return new ViewModelPropertyNumber();
+            case ViewModelInstanceEnumBase::typeKey:
+                return new ViewModelInstanceEnum();
+            case ViewModelInstanceStringBase::typeKey:
+                return new ViewModelInstanceString();
+            case ViewModelPropertyListBase::typeKey:
+                return new ViewModelPropertyList();
+            case ViewModelBase::typeKey:
+                return new ViewModel();
+            case ViewModelPropertyViewModelBase::typeKey:
+                return new ViewModelPropertyViewModel();
+            case ViewModelInstanceBase::typeKey:
+                return new ViewModelInstance();
+            case ViewModelPropertyBooleanBase::typeKey:
+                return new ViewModelPropertyBoolean();
+            case DataEnumBase::typeKey:
+                return new DataEnum();
+            case ViewModelPropertyEnumBase::typeKey:
+                return new ViewModelPropertyEnum();
+            case ViewModelPropertyColorBase::typeKey:
+                return new ViewModelPropertyColor();
+            case ViewModelInstanceBooleanBase::typeKey:
+                return new ViewModelInstanceBoolean();
+            case ViewModelInstanceListBase::typeKey:
+                return new ViewModelInstanceList();
+            case ViewModelInstanceNumberBase::typeKey:
+                return new ViewModelInstanceNumber();
+            case ViewModelPropertyStringBase::typeKey:
+                return new ViewModelPropertyString();
+            case ViewModelInstanceViewModelBase::typeKey:
+                return new ViewModelInstanceViewModel();
+            case DataEnumValueBase::typeKey:
+                return new DataEnumValue();
+            case DrawTargetBase::typeKey:
+                return new DrawTarget();
+            case CustomPropertyNumberBase::typeKey:
+                return new CustomPropertyNumber();
+            case DistanceConstraintBase::typeKey:
+                return new DistanceConstraint();
+            case IKConstraintBase::typeKey:
+                return new IKConstraint();
+            case FollowPathConstraintBase::typeKey:
+                return new FollowPathConstraint();
+            case TranslationConstraintBase::typeKey:
+                return new TranslationConstraint();
+            case TransformConstraintBase::typeKey:
+                return new TransformConstraint();
+            case ScaleConstraintBase::typeKey:
+                return new ScaleConstraint();
+            case RotationConstraintBase::typeKey:
+                return new RotationConstraint();
+            case NodeBase::typeKey:
+                return new Node();
+            case NestedArtboardBase::typeKey:
+                return new NestedArtboard();
+            case SoloBase::typeKey:
+                return new Solo();
+            case NestedArtboardLayoutBase::typeKey:
+                return new NestedArtboardLayout();
+            case NSlicerTileModeBase::typeKey:
+                return new NSlicerTileMode();
+            case AxisYBase::typeKey:
+                return new AxisY();
+            case LayoutComponentStyleBase::typeKey:
+                return new LayoutComponentStyle();
+            case AxisXBase::typeKey:
+                return new AxisX();
+            case NSlicerBase::typeKey:
+                return new NSlicer();
+            case ListenerFireEventBase::typeKey:
+                return new ListenerFireEvent();
+            case KeyFrameUintBase::typeKey:
+                return new KeyFrameUint();
+            case NestedSimpleAnimationBase::typeKey:
+                return new NestedSimpleAnimation();
+            case AnimationStateBase::typeKey:
+                return new AnimationState();
+            case NestedTriggerBase::typeKey:
+                return new NestedTrigger();
+            case KeyedObjectBase::typeKey:
+                return new KeyedObject();
+            case AnimationBase::typeKey:
+                return new Animation();
+            case BlendAnimationDirectBase::typeKey:
+                return new BlendAnimationDirect();
+            case StateMachineNumberBase::typeKey:
+                return new StateMachineNumber();
+            case CubicValueInterpolatorBase::typeKey:
+                return new CubicValueInterpolator();
+            case TransitionTriggerConditionBase::typeKey:
+                return new TransitionTriggerCondition();
+            case KeyedPropertyBase::typeKey:
+                return new KeyedProperty();
+            case StateMachineListenerBase::typeKey:
+                return new StateMachineListener();
+            case TransitionPropertyArtboardComparatorBase::typeKey:
+                return new TransitionPropertyArtboardComparator();
+            case TransitionPropertyViewModelComparatorBase::typeKey:
+                return new TransitionPropertyViewModelComparator();
+            case KeyFrameIdBase::typeKey:
+                return new KeyFrameId();
+            case KeyFrameBoolBase::typeKey:
+                return new KeyFrameBool();
+            case ListenerBoolChangeBase::typeKey:
+                return new ListenerBoolChange();
+            case ListenerAlignTargetBase::typeKey:
+                return new ListenerAlignTarget();
+            case TransitionNumberConditionBase::typeKey:
+                return new TransitionNumberCondition();
+            case TransitionValueBooleanComparatorBase::typeKey:
+                return new TransitionValueBooleanComparator();
+            case TransitionViewModelConditionBase::typeKey:
+                return new TransitionViewModelCondition();
+            case TransitionArtboardConditionBase::typeKey:
+                return new TransitionArtboardCondition();
+            case AnyStateBase::typeKey:
+                return new AnyState();
+            case CubicInterpolatorComponentBase::typeKey:
+                return new CubicInterpolatorComponent();
+            case StateMachineLayerBase::typeKey:
+                return new StateMachineLayer();
+            case KeyFrameStringBase::typeKey:
+                return new KeyFrameString();
+            case ListenerNumberChangeBase::typeKey:
+                return new ListenerNumberChange();
+            case CubicEaseInterpolatorBase::typeKey:
+                return new CubicEaseInterpolator();
+            case StateTransitionBase::typeKey:
+                return new StateTransition();
+            case NestedBoolBase::typeKey:
+                return new NestedBool();
+            case KeyFrameDoubleBase::typeKey:
+                return new KeyFrameDouble();
+            case KeyFrameColorBase::typeKey:
+                return new KeyFrameColor();
+            case StateMachineBase::typeKey:
+                return new StateMachine();
+            case StateMachineFireEventBase::typeKey:
+                return new StateMachineFireEvent();
+            case EntryStateBase::typeKey:
+                return new EntryState();
+            case LinearAnimationBase::typeKey:
+                return new LinearAnimation();
+            case StateMachineTriggerBase::typeKey:
+                return new StateMachineTrigger();
+            case TransitionValueColorComparatorBase::typeKey:
+                return new TransitionValueColorComparator();
+            case ListenerTriggerChangeBase::typeKey:
+                return new ListenerTriggerChange();
+            case BlendStateDirectBase::typeKey:
+                return new BlendStateDirect();
+            case ListenerViewModelChangeBase::typeKey:
+                return new ListenerViewModelChange();
+            case TransitionValueNumberComparatorBase::typeKey:
+                return new TransitionValueNumberComparator();
+            case NestedStateMachineBase::typeKey:
+                return new NestedStateMachine();
+            case ElasticInterpolatorBase::typeKey:
+                return new ElasticInterpolator();
+            case ExitStateBase::typeKey:
+                return new ExitState();
+            case NestedNumberBase::typeKey:
+                return new NestedNumber();
+            case BlendState1DBase::typeKey:
+                return new BlendState1D();
+            case TransitionValueEnumComparatorBase::typeKey:
+                return new TransitionValueEnumComparator();
+            case KeyFrameCallbackBase::typeKey:
+                return new KeyFrameCallback();
+            case TransitionValueStringComparatorBase::typeKey:
+                return new TransitionValueStringComparator();
+            case NestedRemapAnimationBase::typeKey:
+                return new NestedRemapAnimation();
+            case TransitionBoolConditionBase::typeKey:
+                return new TransitionBoolCondition();
+            case BlendStateTransitionBase::typeKey:
+                return new BlendStateTransition();
+            case StateMachineBoolBase::typeKey:
+                return new StateMachineBool();
+            case BlendAnimation1DBase::typeKey:
+                return new BlendAnimation1D();
+            case LinearGradientBase::typeKey:
+                return new LinearGradient();
+            case RadialGradientBase::typeKey:
+                return new RadialGradient();
+            case StrokeBase::typeKey:
+                return new Stroke();
+            case SolidColorBase::typeKey:
+                return new SolidColor();
+            case GradientStopBase::typeKey:
+                return new GradientStop();
+            case TrimPathBase::typeKey:
+                return new TrimPath();
+            case FillBase::typeKey:
+                return new Fill();
+            case MeshVertexBase::typeKey:
+                return new MeshVertex();
+            case ShapeBase::typeKey:
+                return new Shape();
+            case StraightVertexBase::typeKey:
+                return new StraightVertex();
+            case CubicAsymmetricVertexBase::typeKey:
+                return new CubicAsymmetricVertex();
+            case MeshBase::typeKey:
+                return new Mesh();
+            case PointsPathBase::typeKey:
+                return new PointsPath();
+            case ContourMeshVertexBase::typeKey:
+                return new ContourMeshVertex();
+            case RectangleBase::typeKey:
+                return new Rectangle();
+            case CubicMirroredVertexBase::typeKey:
+                return new CubicMirroredVertex();
+            case TriangleBase::typeKey:
+                return new Triangle();
+            case EllipseBase::typeKey:
+                return new Ellipse();
+            case ClippingShapeBase::typeKey:
+                return new ClippingShape();
+            case PolygonBase::typeKey:
+                return new Polygon();
+            case StarBase::typeKey:
+                return new Star();
+            case ImageBase::typeKey:
+                return new Image();
+            case CubicDetachedVertexBase::typeKey:
+                return new CubicDetachedVertex();
+            case EventBase::typeKey:
+                return new Event();
+            case DrawRulesBase::typeKey:
+                return new DrawRules();
+            case CustomPropertyBooleanBase::typeKey:
+                return new CustomPropertyBoolean();
+            case LayoutComponentBase::typeKey:
+                return new LayoutComponent();
+            case ArtboardBase::typeKey:
+                return new Artboard();
+            case JoystickBase::typeKey:
+                return new Joystick();
+            case BackboardBase::typeKey:
+                return new Backboard();
+            case OpenUrlEventBase::typeKey:
+                return new OpenUrlEvent();
+            case BindablePropertyBooleanBase::typeKey:
+                return new BindablePropertyBoolean();
+            case DataBindBase::typeKey:
+                return new DataBind();
+            case DataConverterGroupItemBase::typeKey:
+                return new DataConverterGroupItem();
+            case DataConverterGroupBase::typeKey:
+                return new DataConverterGroup();
+            case DataConverterRounderBase::typeKey:
+                return new DataConverterRounder();
+            case DataConverterOperationBase::typeKey:
+                return new DataConverterOperation();
+            case DataConverterToStringBase::typeKey:
+                return new DataConverterToString();
+            case DataBindContextBase::typeKey:
+                return new DataBindContext();
+            case BindablePropertyStringBase::typeKey:
+                return new BindablePropertyString();
+            case BindablePropertyNumberBase::typeKey:
+                return new BindablePropertyNumber();
+            case BindablePropertyEnumBase::typeKey:
+                return new BindablePropertyEnum();
+            case BindablePropertyColorBase::typeKey:
+                return new BindablePropertyColor();
+            case NestedArtboardLeafBase::typeKey:
+                return new NestedArtboardLeaf();
+            case WeightBase::typeKey:
+                return new Weight();
+            case BoneBase::typeKey:
+                return new Bone();
+            case RootBoneBase::typeKey:
+                return new RootBone();
+            case SkinBase::typeKey:
+                return new Skin();
+            case TendonBase::typeKey:
+                return new Tendon();
+            case CubicWeightBase::typeKey:
+                return new CubicWeight();
+            case TextModifierRangeBase::typeKey:
+                return new TextModifierRange();
+            case TextStyleFeatureBase::typeKey:
+                return new TextStyleFeature();
+            case TextVariationModifierBase::typeKey:
+                return new TextVariationModifier();
+            case TextModifierGroupBase::typeKey:
+                return new TextModifierGroup();
+            case TextStyleBase::typeKey:
+                return new TextStyle();
+            case TextStyleAxisBase::typeKey:
+                return new TextStyleAxis();
+            case TextBase::typeKey:
+                return new Text();
+            case TextValueRunBase::typeKey:
+                return new TextValueRun();
+            case CustomPropertyStringBase::typeKey:
+                return new CustomPropertyString();
+            case FolderBase::typeKey:
+                return new Folder();
+            case ImageAssetBase::typeKey:
+                return new ImageAsset();
+            case FontAssetBase::typeKey:
+                return new FontAsset();
+            case AudioAssetBase::typeKey:
+                return new AudioAsset();
+            case FileAssetContentsBase::typeKey:
+                return new FileAssetContents();
+            case AudioEventBase::typeKey:
+                return new AudioEvent();
+        }
+        return nullptr;
+    }
+    static void setBool(Core* object, int propertyKey, bool value)
+    {
+        switch (propertyKey)
+        {
+            case ViewModelInstanceListItemBase::useLinkedArtboardPropertyKey:
+                object->as<ViewModelInstanceListItemBase>()->useLinkedArtboard(value);
+                break;
+            case ViewModelInstanceBooleanBase::propertyValuePropertyKey:
+                object->as<ViewModelInstanceBooleanBase>()->propertyValue(value);
+                break;
+            case TransformComponentConstraintBase::offsetPropertyKey:
+                object->as<TransformComponentConstraintBase>()->offset(value);
+                break;
+            case TransformComponentConstraintBase::doesCopyPropertyKey:
+                object->as<TransformComponentConstraintBase>()->doesCopy(value);
+                break;
+            case TransformComponentConstraintBase::minPropertyKey:
+                object->as<TransformComponentConstraintBase>()->min(value);
+                break;
+            case TransformComponentConstraintBase::maxPropertyKey:
+                object->as<TransformComponentConstraintBase>()->max(value);
+                break;
+            case TransformComponentConstraintYBase::doesCopyYPropertyKey:
+                object->as<TransformComponentConstraintYBase>()->doesCopyY(value);
+                break;
+            case TransformComponentConstraintYBase::minYPropertyKey:
+                object->as<TransformComponentConstraintYBase>()->minY(value);
+                break;
+            case TransformComponentConstraintYBase::maxYPropertyKey:
+                object->as<TransformComponentConstraintYBase>()->maxY(value);
+                break;
+            case IKConstraintBase::invertDirectionPropertyKey:
+                object->as<IKConstraintBase>()->invertDirection(value);
+                break;
+            case FollowPathConstraintBase::orientPropertyKey:
+                object->as<FollowPathConstraintBase>()->orient(value);
+                break;
+            case FollowPathConstraintBase::offsetPropertyKey:
+                object->as<FollowPathConstraintBase>()->offset(value);
+                break;
+            case AxisBase::normalizedPropertyKey:
+                object->as<AxisBase>()->normalized(value);
+                break;
+            case LayoutComponentStyleBase::intrinsicallySizedValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->intrinsicallySizedValue(value);
+                break;
+            case LayoutComponentStyleBase::linkCornerRadiusPropertyKey:
+                object->as<LayoutComponentStyleBase>()->linkCornerRadius(value);
+                break;
+            case NestedSimpleAnimationBase::isPlayingPropertyKey:
+                object->as<NestedSimpleAnimationBase>()->isPlaying(value);
+                break;
+            case KeyFrameBoolBase::valuePropertyKey:
+                object->as<KeyFrameBoolBase>()->value(value);
+                break;
+            case ListenerAlignTargetBase::preserveOffsetPropertyKey:
+                object->as<ListenerAlignTargetBase>()->preserveOffset(value);
+                break;
+            case TransitionValueBooleanComparatorBase::valuePropertyKey:
+                object->as<TransitionValueBooleanComparatorBase>()->value(value);
+                break;
+            case NestedBoolBase::nestedValuePropertyKey:
+                object->as<NestedBoolBase>()->nestedValue(value);
+                break;
+            case LinearAnimationBase::enableWorkAreaPropertyKey:
+                object->as<LinearAnimationBase>()->enableWorkArea(value);
+                break;
+            case LinearAnimationBase::quantizePropertyKey:
+                object->as<LinearAnimationBase>()->quantize(value);
+                break;
+            case StateMachineBoolBase::valuePropertyKey:
+                object->as<StateMachineBoolBase>()->value(value);
+                break;
+            case ShapePaintBase::isVisiblePropertyKey:
+                object->as<ShapePaintBase>()->isVisible(value);
+                break;
+            case StrokeBase::transformAffectsStrokePropertyKey:
+                object->as<StrokeBase>()->transformAffectsStroke(value);
+                break;
+            case PointsPathBase::isClosedPropertyKey:
+                object->as<PointsPathBase>()->isClosed(value);
+                break;
+            case RectangleBase::linkCornerRadiusPropertyKey:
+                object->as<RectangleBase>()->linkCornerRadius(value);
+                break;
+            case ClippingShapeBase::isVisiblePropertyKey:
+                object->as<ClippingShapeBase>()->isVisible(value);
+                break;
+            case CustomPropertyBooleanBase::propertyValuePropertyKey:
+                object->as<CustomPropertyBooleanBase>()->propertyValue(value);
+                break;
+            case LayoutComponentBase::clipPropertyKey:
+                object->as<LayoutComponentBase>()->clip(value);
+                break;
+            case BindablePropertyBooleanBase::propertyValuePropertyKey:
+                object->as<BindablePropertyBooleanBase>()->propertyValue(value);
+                break;
+            case TextModifierRangeBase::clampPropertyKey:
+                object->as<TextModifierRangeBase>()->clamp(value);
+                break;
+        }
+    }
+    static void setUint(Core* object, int propertyKey, uint32_t value)
+    {
+        switch (propertyKey)
+        {
+            case ViewModelInstanceListItemBase::viewModelIdPropertyKey:
+                object->as<ViewModelInstanceListItemBase>()->viewModelId(value);
+                break;
+            case ViewModelInstanceListItemBase::viewModelInstanceIdPropertyKey:
+                object->as<ViewModelInstanceListItemBase>()->viewModelInstanceId(value);
+                break;
+            case ViewModelInstanceListItemBase::artboardIdPropertyKey:
+                object->as<ViewModelInstanceListItemBase>()->artboardId(value);
+                break;
+            case ViewModelInstanceValueBase::viewModelPropertyIdPropertyKey:
+                object->as<ViewModelInstanceValueBase>()->viewModelPropertyId(value);
+                break;
+            case ViewModelInstanceEnumBase::propertyValuePropertyKey:
+                object->as<ViewModelInstanceEnumBase>()->propertyValue(value);
+                break;
+            case ViewModelBase::defaultInstanceIdPropertyKey:
+                object->as<ViewModelBase>()->defaultInstanceId(value);
+                break;
+            case ViewModelPropertyViewModelBase::viewModelReferenceIdPropertyKey:
+                object->as<ViewModelPropertyViewModelBase>()->viewModelReferenceId(value);
+                break;
+            case ComponentBase::parentIdPropertyKey:
+                object->as<ComponentBase>()->parentId(value);
+                break;
+            case ViewModelInstanceBase::viewModelIdPropertyKey:
+                object->as<ViewModelInstanceBase>()->viewModelId(value);
+                break;
+            case ViewModelPropertyEnumBase::enumIdPropertyKey:
+                object->as<ViewModelPropertyEnumBase>()->enumId(value);
+                break;
+            case ViewModelInstanceViewModelBase::propertyValuePropertyKey:
+                object->as<ViewModelInstanceViewModelBase>()->propertyValue(value);
+                break;
+            case DrawTargetBase::drawableIdPropertyKey:
+                object->as<DrawTargetBase>()->drawableId(value);
+                break;
+            case DrawTargetBase::placementValuePropertyKey:
+                object->as<DrawTargetBase>()->placementValue(value);
+                break;
+            case TargetedConstraintBase::targetIdPropertyKey:
+                object->as<TargetedConstraintBase>()->targetId(value);
+                break;
+            case DistanceConstraintBase::modeValuePropertyKey:
+                object->as<DistanceConstraintBase>()->modeValue(value);
+                break;
+            case TransformSpaceConstraintBase::sourceSpaceValuePropertyKey:
+                object->as<TransformSpaceConstraintBase>()->sourceSpaceValue(value);
+                break;
+            case TransformSpaceConstraintBase::destSpaceValuePropertyKey:
+                object->as<TransformSpaceConstraintBase>()->destSpaceValue(value);
+                break;
+            case TransformComponentConstraintBase::minMaxSpaceValuePropertyKey:
+                object->as<TransformComponentConstraintBase>()->minMaxSpaceValue(value);
+                break;
+            case IKConstraintBase::parentBoneCountPropertyKey:
+                object->as<IKConstraintBase>()->parentBoneCount(value);
+                break;
+            case DrawableBase::blendModeValuePropertyKey:
+                object->as<DrawableBase>()->blendModeValue(value);
+                break;
+            case DrawableBase::drawableFlagsPropertyKey:
+                object->as<DrawableBase>()->drawableFlags(value);
+                break;
+            case NestedArtboardBase::artboardIdPropertyKey:
+                object->as<NestedArtboardBase>()->artboardId(value);
+                break;
+            case NestedAnimationBase::animationIdPropertyKey:
+                object->as<NestedAnimationBase>()->animationId(value);
+                break;
+            case SoloBase::activeComponentIdPropertyKey:
+                object->as<SoloBase>()->activeComponentId(value);
+                break;
+            case NestedArtboardLayoutBase::instanceWidthUnitsValuePropertyKey:
+                object->as<NestedArtboardLayoutBase>()->instanceWidthUnitsValue(value);
+                break;
+            case NestedArtboardLayoutBase::instanceHeightUnitsValuePropertyKey:
+                object->as<NestedArtboardLayoutBase>()->instanceHeightUnitsValue(value);
+                break;
+            case NestedArtboardLayoutBase::instanceWidthScaleTypePropertyKey:
+                object->as<NestedArtboardLayoutBase>()->instanceWidthScaleType(value);
+                break;
+            case NestedArtboardLayoutBase::instanceHeightScaleTypePropertyKey:
+                object->as<NestedArtboardLayoutBase>()->instanceHeightScaleType(value);
+                break;
+            case NSlicerTileModeBase::patchIndexPropertyKey:
+                object->as<NSlicerTileModeBase>()->patchIndex(value);
+                break;
+            case NSlicerTileModeBase::stylePropertyKey:
+                object->as<NSlicerTileModeBase>()->style(value);
+                break;
+            case LayoutComponentStyleBase::layoutWidthScaleTypePropertyKey:
+                object->as<LayoutComponentStyleBase>()->layoutWidthScaleType(value);
+                break;
+            case LayoutComponentStyleBase::layoutHeightScaleTypePropertyKey:
+                object->as<LayoutComponentStyleBase>()->layoutHeightScaleType(value);
+                break;
+            case LayoutComponentStyleBase::layoutAlignmentTypePropertyKey:
+                object->as<LayoutComponentStyleBase>()->layoutAlignmentType(value);
+                break;
+            case LayoutComponentStyleBase::animationStyleTypePropertyKey:
+                object->as<LayoutComponentStyleBase>()->animationStyleType(value);
+                break;
+            case LayoutComponentStyleBase::interpolationTypePropertyKey:
+                object->as<LayoutComponentStyleBase>()->interpolationType(value);
+                break;
+            case LayoutComponentStyleBase::interpolatorIdPropertyKey:
+                object->as<LayoutComponentStyleBase>()->interpolatorId(value);
+                break;
+            case LayoutComponentStyleBase::displayValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->displayValue(value);
+                break;
+            case LayoutComponentStyleBase::positionTypeValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->positionTypeValue(value);
+                break;
+            case LayoutComponentStyleBase::flexDirectionValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->flexDirectionValue(value);
+                break;
+            case LayoutComponentStyleBase::directionValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->directionValue(value);
+                break;
+            case LayoutComponentStyleBase::alignContentValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->alignContentValue(value);
+                break;
+            case LayoutComponentStyleBase::alignItemsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->alignItemsValue(value);
+                break;
+            case LayoutComponentStyleBase::alignSelfValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->alignSelfValue(value);
+                break;
+            case LayoutComponentStyleBase::justifyContentValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->justifyContentValue(value);
+                break;
+            case LayoutComponentStyleBase::flexWrapValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->flexWrapValue(value);
+                break;
+            case LayoutComponentStyleBase::overflowValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->overflowValue(value);
+                break;
+            case LayoutComponentStyleBase::widthUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->widthUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::heightUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->heightUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::borderLeftUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->borderLeftUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::borderRightUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->borderRightUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::borderTopUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->borderTopUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::borderBottomUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->borderBottomUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::marginLeftUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->marginLeftUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::marginRightUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->marginRightUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::marginTopUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->marginTopUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::marginBottomUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->marginBottomUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::paddingLeftUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->paddingLeftUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::paddingRightUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->paddingRightUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::paddingTopUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->paddingTopUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::paddingBottomUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->paddingBottomUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::positionLeftUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->positionLeftUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::positionRightUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->positionRightUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::positionTopUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->positionTopUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::positionBottomUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->positionBottomUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::gapHorizontalUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->gapHorizontalUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::gapVerticalUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->gapVerticalUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::minWidthUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->minWidthUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::minHeightUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->minHeightUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::maxWidthUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->maxWidthUnitsValue(value);
+                break;
+            case LayoutComponentStyleBase::maxHeightUnitsValuePropertyKey:
+                object->as<LayoutComponentStyleBase>()->maxHeightUnitsValue(value);
+                break;
+            case ListenerFireEventBase::eventIdPropertyKey:
+                object->as<ListenerFireEventBase>()->eventId(value);
+                break;
+            case LayerStateBase::flagsPropertyKey:
+                object->as<LayerStateBase>()->flags(value);
+                break;
+            case KeyFrameBase::framePropertyKey:
+                object->as<KeyFrameBase>()->frame(value);
+                break;
+            case InterpolatingKeyFrameBase::interpolationTypePropertyKey:
+                object->as<InterpolatingKeyFrameBase>()->interpolationType(value);
+                break;
+            case InterpolatingKeyFrameBase::interpolatorIdPropertyKey:
+                object->as<InterpolatingKeyFrameBase>()->interpolatorId(value);
+                break;
+            case KeyFrameUintBase::valuePropertyKey:
+                object->as<KeyFrameUintBase>()->value(value);
+                break;
+            case ListenerInputChangeBase::inputIdPropertyKey:
+                object->as<ListenerInputChangeBase>()->inputId(value);
+                break;
+            case ListenerInputChangeBase::nestedInputIdPropertyKey:
+                object->as<ListenerInputChangeBase>()->nestedInputId(value);
+                break;
+            case AnimationStateBase::animationIdPropertyKey:
+                object->as<AnimationStateBase>()->animationId(value);
+                break;
+            case NestedInputBase::inputIdPropertyKey:
+                object->as<NestedInputBase>()->inputId(value);
+                break;
+            case KeyedObjectBase::objectIdPropertyKey:
+                object->as<KeyedObjectBase>()->objectId(value);
+                break;
+            case BlendAnimationBase::animationIdPropertyKey:
+                object->as<BlendAnimationBase>()->animationId(value);
+                break;
+            case BlendAnimationDirectBase::inputIdPropertyKey:
+                object->as<BlendAnimationDirectBase>()->inputId(value);
+                break;
+            case BlendAnimationDirectBase::blendSourcePropertyKey:
+                object->as<BlendAnimationDirectBase>()->blendSource(value);
+                break;
+            case TransitionInputConditionBase::inputIdPropertyKey:
+                object->as<TransitionInputConditionBase>()->inputId(value);
+                break;
+            case KeyedPropertyBase::propertyKeyPropertyKey:
+                object->as<KeyedPropertyBase>()->propertyKey(value);
+                break;
+            case StateMachineListenerBase::targetIdPropertyKey:
+                object->as<StateMachineListenerBase>()->targetId(value);
+                break;
+            case StateMachineListenerBase::listenerTypeValuePropertyKey:
+                object->as<StateMachineListenerBase>()->listenerTypeValue(value);
+                break;
+            case StateMachineListenerBase::eventIdPropertyKey:
+                object->as<StateMachineListenerBase>()->eventId(value);
+                break;
+            case TransitionPropertyArtboardComparatorBase::propertyTypePropertyKey:
+                object->as<TransitionPropertyArtboardComparatorBase>()->propertyType(value);
+                break;
+            case KeyFrameIdBase::valuePropertyKey:
+                object->as<KeyFrameIdBase>()->value(value);
+                break;
+            case ListenerBoolChangeBase::valuePropertyKey:
+                object->as<ListenerBoolChangeBase>()->value(value);
+                break;
+            case ListenerAlignTargetBase::targetIdPropertyKey:
+                object->as<ListenerAlignTargetBase>()->targetId(value);
+                break;
+            case TransitionValueConditionBase::opValuePropertyKey:
+                object->as<TransitionValueConditionBase>()->opValue(value);
+                break;
+            case TransitionViewModelConditionBase::leftComparatorIdPropertyKey:
+                object->as<TransitionViewModelConditionBase>()->leftComparatorId(value);
+                break;
+            case TransitionViewModelConditionBase::rightComparatorIdPropertyKey:
+                object->as<TransitionViewModelConditionBase>()->rightComparatorId(value);
+                break;
+            case TransitionViewModelConditionBase::opValuePropertyKey:
+                object->as<TransitionViewModelConditionBase>()->opValue(value);
+                break;
+            case StateTransitionBase::stateToIdPropertyKey:
+                object->as<StateTransitionBase>()->stateToId(value);
+                break;
+            case StateTransitionBase::flagsPropertyKey:
+                object->as<StateTransitionBase>()->flags(value);
+                break;
+            case StateTransitionBase::durationPropertyKey:
+                object->as<StateTransitionBase>()->duration(value);
+                break;
+            case StateTransitionBase::exitTimePropertyKey:
+                object->as<StateTransitionBase>()->exitTime(value);
+                break;
+            case StateTransitionBase::interpolationTypePropertyKey:
+                object->as<StateTransitionBase>()->interpolationType(value);
+                break;
+            case StateTransitionBase::interpolatorIdPropertyKey:
+                object->as<StateTransitionBase>()->interpolatorId(value);
+                break;
+            case StateTransitionBase::randomWeightPropertyKey:
+                object->as<StateTransitionBase>()->randomWeight(value);
+                break;
+            case StateMachineFireEventBase::eventIdPropertyKey:
+                object->as<StateMachineFireEventBase>()->eventId(value);
+                break;
+            case StateMachineFireEventBase::occursValuePropertyKey:
+                object->as<StateMachineFireEventBase>()->occursValue(value);
+                break;
+            case LinearAnimationBase::fpsPropertyKey:
+                object->as<LinearAnimationBase>()->fps(value);
+                break;
+            case LinearAnimationBase::durationPropertyKey:
+                object->as<LinearAnimationBase>()->duration(value);
+                break;
+            case LinearAnimationBase::loopValuePropertyKey:
+                object->as<LinearAnimationBase>()->loopValue(value);
+                break;
+            case LinearAnimationBase::workStartPropertyKey:
+                object->as<LinearAnimationBase>()->workStart(value);
+                break;
+            case LinearAnimationBase::workEndPropertyKey:
+                object->as<LinearAnimationBase>()->workEnd(value);
+                break;
+            case ElasticInterpolatorBase::easingValuePropertyKey:
+                object->as<ElasticInterpolatorBase>()->easingValue(value);
+                break;
+            case BlendState1DBase::inputIdPropertyKey:
+                object->as<BlendState1DBase>()->inputId(value);
+                break;
+            case TransitionValueEnumComparatorBase::valuePropertyKey:
+                object->as<TransitionValueEnumComparatorBase>()->value(value);
+                break;
+            case BlendStateTransitionBase::exitBlendAnimationIdPropertyKey:
+                object->as<BlendStateTransitionBase>()->exitBlendAnimationId(value);
+                break;
+            case StrokeBase::capPropertyKey:
+                object->as<StrokeBase>()->cap(value);
+                break;
+            case StrokeBase::joinPropertyKey:
+                object->as<StrokeBase>()->join(value);
+                break;
+            case TrimPathBase::modeValuePropertyKey:
+                object->as<TrimPathBase>()->modeValue(value);
+                break;
+            case FillBase::fillRulePropertyKey:
+                object->as<FillBase>()->fillRule(value);
+                break;
+            case PathBase::pathFlagsPropertyKey:
+                object->as<PathBase>()->pathFlags(value);
+                break;
+            case ClippingShapeBase::sourceIdPropertyKey:
+                object->as<ClippingShapeBase>()->sourceId(value);
+                break;
+            case ClippingShapeBase::fillRulePropertyKey:
+                object->as<ClippingShapeBase>()->fillRule(value);
+                break;
+            case PolygonBase::pointsPropertyKey:
+                object->as<PolygonBase>()->points(value);
+                break;
+            case ImageBase::assetIdPropertyKey:
+                object->as<ImageBase>()->assetId(value);
+                break;
+            case DrawRulesBase::drawTargetIdPropertyKey:
+                object->as<DrawRulesBase>()->drawTargetId(value);
+                break;
+            case LayoutComponentBase::styleIdPropertyKey:
+                object->as<LayoutComponentBase>()->styleId(value);
+                break;
+            case ArtboardBase::defaultStateMachineIdPropertyKey:
+                object->as<ArtboardBase>()->defaultStateMachineId(value);
+                break;
+            case ArtboardBase::viewModelIdPropertyKey:
+                object->as<ArtboardBase>()->viewModelId(value);
+                break;
+            case JoystickBase::xIdPropertyKey:
+                object->as<JoystickBase>()->xId(value);
+                break;
+            case JoystickBase::yIdPropertyKey:
+                object->as<JoystickBase>()->yId(value);
+                break;
+            case JoystickBase::joystickFlagsPropertyKey:
+                object->as<JoystickBase>()->joystickFlags(value);
+                break;
+            case JoystickBase::handleSourceIdPropertyKey:
+                object->as<JoystickBase>()->handleSourceId(value);
+                break;
+            case OpenUrlEventBase::targetValuePropertyKey:
+                object->as<OpenUrlEventBase>()->targetValue(value);
+                break;
+            case DataBindBase::propertyKeyPropertyKey:
+                object->as<DataBindBase>()->propertyKey(value);
+                break;
+            case DataBindBase::flagsPropertyKey:
+                object->as<DataBindBase>()->flags(value);
+                break;
+            case DataBindBase::converterIdPropertyKey:
+                object->as<DataBindBase>()->converterId(value);
+                break;
+            case DataConverterGroupItemBase::converterIdPropertyKey:
+                object->as<DataConverterGroupItemBase>()->converterId(value);
+                break;
+            case DataConverterRounderBase::decimalsPropertyKey:
+                object->as<DataConverterRounderBase>()->decimals(value);
+                break;
+            case DataConverterOperationBase::operationTypePropertyKey:
+                object->as<DataConverterOperationBase>()->operationType(value);
+                break;
+            case BindablePropertyEnumBase::propertyValuePropertyKey:
+                object->as<BindablePropertyEnumBase>()->propertyValue(value);
+                break;
+            case NestedArtboardLeafBase::fitPropertyKey:
+                object->as<NestedArtboardLeafBase>()->fit(value);
+                break;
+            case WeightBase::valuesPropertyKey:
+                object->as<WeightBase>()->values(value);
+                break;
+            case WeightBase::indicesPropertyKey:
+                object->as<WeightBase>()->indices(value);
+                break;
+            case TendonBase::boneIdPropertyKey:
+                object->as<TendonBase>()->boneId(value);
+                break;
+            case CubicWeightBase::inValuesPropertyKey:
+                object->as<CubicWeightBase>()->inValues(value);
+                break;
+            case CubicWeightBase::inIndicesPropertyKey:
+                object->as<CubicWeightBase>()->inIndices(value);
+                break;
+            case CubicWeightBase::outValuesPropertyKey:
+                object->as<CubicWeightBase>()->outValues(value);
+                break;
+            case CubicWeightBase::outIndicesPropertyKey:
+                object->as<CubicWeightBase>()->outIndices(value);
+                break;
+            case TextModifierRangeBase::unitsValuePropertyKey:
+                object->as<TextModifierRangeBase>()->unitsValue(value);
+                break;
+            case TextModifierRangeBase::typeValuePropertyKey:
+                object->as<TextModifierRangeBase>()->typeValue(value);
+                break;
+            case TextModifierRangeBase::modeValuePropertyKey:
+                object->as<TextModifierRangeBase>()->modeValue(value);
+                break;
+            case TextModifierRangeBase::runIdPropertyKey:
+                object->as<TextModifierRangeBase>()->runId(value);
+                break;
+            case TextStyleFeatureBase::tagPropertyKey:
+                object->as<TextStyleFeatureBase>()->tag(value);
+                break;
+            case TextStyleFeatureBase::featureValuePropertyKey:
+                object->as<TextStyleFeatureBase>()->featureValue(value);
+                break;
+            case TextVariationModifierBase::axisTagPropertyKey:
+                object->as<TextVariationModifierBase>()->axisTag(value);
+                break;
+            case TextModifierGroupBase::modifierFlagsPropertyKey:
+                object->as<TextModifierGroupBase>()->modifierFlags(value);
+                break;
+            case TextStyleBase::fontAssetIdPropertyKey:
+                object->as<TextStyleBase>()->fontAssetId(value);
+                break;
+            case TextStyleAxisBase::tagPropertyKey:
+                object->as<TextStyleAxisBase>()->tag(value);
+                break;
+            case TextBase::alignValuePropertyKey:
+                object->as<TextBase>()->alignValue(value);
+                break;
+            case TextBase::sizingValuePropertyKey:
+                object->as<TextBase>()->sizingValue(value);
+                break;
+            case TextBase::overflowValuePropertyKey:
+                object->as<TextBase>()->overflowValue(value);
+                break;
+            case TextBase::originValuePropertyKey:
+                object->as<TextBase>()->originValue(value);
+                break;
+            case TextValueRunBase::styleIdPropertyKey:
+                object->as<TextValueRunBase>()->styleId(value);
+                break;
+            case FileAssetBase::assetIdPropertyKey:
+                object->as<FileAssetBase>()->assetId(value);
+                break;
+            case AudioEventBase::assetIdPropertyKey:
+                object->as<AudioEventBase>()->assetId(value);
+                break;
+        }
+    }
+    static void setColor(Core* object, int propertyKey, int value)
+    {
+        switch (propertyKey)
+        {
+            case ViewModelInstanceColorBase::propertyValuePropertyKey:
+                object->as<ViewModelInstanceColorBase>()->propertyValue(value);
+                break;
+            case KeyFrameColorBase::valuePropertyKey:
+                object->as<KeyFrameColorBase>()->value(value);
+                break;
+            case TransitionValueColorComparatorBase::valuePropertyKey:
+                object->as<TransitionValueColorComparatorBase>()->value(value);
+                break;
+            case SolidColorBase::colorValuePropertyKey:
+                object->as<SolidColorBase>()->colorValue(value);
+                break;
+            case GradientStopBase::colorValuePropertyKey:
+                object->as<GradientStopBase>()->colorValue(value);
+                break;
+            case BindablePropertyColorBase::propertyValuePropertyKey:
+                object->as<BindablePropertyColorBase>()->propertyValue(value);
+                break;
+        }
+    }
+    static void setString(Core* object, int propertyKey, std::string value)
+    {
+        switch (propertyKey)
+        {
+            case ViewModelComponentBase::namePropertyKey:
+                object->as<ViewModelComponentBase>()->name(value);
+                break;
+            case ViewModelInstanceStringBase::propertyValuePropertyKey:
+                object->as<ViewModelInstanceStringBase>()->propertyValue(value);
+                break;
+            case ComponentBase::namePropertyKey:
+                object->as<ComponentBase>()->name(value);
+                break;
+            case DataEnumValueBase::keyPropertyKey:
+                object->as<DataEnumValueBase>()->key(value);
+                break;
+            case DataEnumValueBase::valuePropertyKey:
+                object->as<DataEnumValueBase>()->value(value);
+                break;
+            case AnimationBase::namePropertyKey:
+                object->as<AnimationBase>()->name(value);
+                break;
+            case StateMachineComponentBase::namePropertyKey:
+                object->as<StateMachineComponentBase>()->name(value);
+                break;
+            case KeyFrameStringBase::valuePropertyKey:
+                object->as<KeyFrameStringBase>()->value(value);
+                break;
+            case TransitionValueStringComparatorBase::valuePropertyKey:
+                object->as<TransitionValueStringComparatorBase>()->value(value);
+                break;
+            case OpenUrlEventBase::urlPropertyKey:
+                object->as<OpenUrlEventBase>()->url(value);
+                break;
+            case DataConverterBase::namePropertyKey:
+                object->as<DataConverterBase>()->name(value);
+                break;
+            case BindablePropertyStringBase::propertyValuePropertyKey:
+                object->as<BindablePropertyStringBase>()->propertyValue(value);
+                break;
+            case TextValueRunBase::textPropertyKey:
+                object->as<TextValueRunBase>()->text(value);
+                break;
+            case CustomPropertyStringBase::propertyValuePropertyKey:
+                object->as<CustomPropertyStringBase>()->propertyValue(value);
+                break;
+            case AssetBase::namePropertyKey:
+                object->as<AssetBase>()->name(value);
+                break;
+            case FileAssetBase::cdnBaseUrlPropertyKey:
+                object->as<FileAssetBase>()->cdnBaseUrl(value);
+                break;
+        }
+    }
+    static void setDouble(Core* object, int propertyKey, float value)
+    {
+        switch (propertyKey)
+        {
+            case ViewModelInstanceNumberBase::propertyValuePropertyKey:
+                object->as<ViewModelInstanceNumberBase>()->propertyValue(value);
+                break;
+            case CustomPropertyNumberBase::propertyValuePropertyKey:
+                object->as<CustomPropertyNumberBase>()->propertyValue(value);
+                break;
+            case ConstraintBase::strengthPropertyKey:
+                object->as<ConstraintBase>()->strength(value);
+                break;
+            case DistanceConstraintBase::distancePropertyKey:
+                object->as<DistanceConstraintBase>()->distance(value);
+                break;
+            case TransformComponentConstraintBase::copyFactorPropertyKey:
+                object->as<TransformComponentConstraintBase>()->copyFactor(value);
+                break;
+            case TransformComponentConstraintBase::minValuePropertyKey:
+                object->as<TransformComponentConstraintBase>()->minValue(value);
+                break;
+            case TransformComponentConstraintBase::maxValuePropertyKey:
+                object->as<TransformComponentConstraintBase>()->maxValue(value);
+                break;
+            case TransformComponentConstraintYBase::copyFactorYPropertyKey:
+                object->as<TransformComponentConstraintYBase>()->copyFactorY(value);
+                break;
+            case TransformComponentConstraintYBase::minValueYPropertyKey:
+                object->as<TransformComponentConstraintYBase>()->minValueY(value);
+                break;
+            case TransformComponentConstraintYBase::maxValueYPropertyKey:
+                object->as<TransformComponentConstraintYBase>()->maxValueY(value);
+                break;
+            case FollowPathConstraintBase::distancePropertyKey:
+                object->as<FollowPathConstraintBase>()->distance(value);
+                break;
+            case TransformConstraintBase::originXPropertyKey:
+                object->as<TransformConstraintBase>()->originX(value);
+                break;
+            case TransformConstraintBase::originYPropertyKey:
+                object->as<TransformConstraintBase>()->originY(value);
+                break;
+            case WorldTransformComponentBase::opacityPropertyKey:
+                object->as<WorldTransformComponentBase>()->opacity(value);
+                break;
+            case TransformComponentBase::rotationPropertyKey:
+                object->as<TransformComponentBase>()->rotation(value);
+                break;
+            case TransformComponentBase::scaleXPropertyKey:
+                object->as<TransformComponentBase>()->scaleX(value);
+                break;
+            case TransformComponentBase::scaleYPropertyKey:
+                object->as<TransformComponentBase>()->scaleY(value);
+                break;
+            case NodeBase::xPropertyKey:
+            case NodeBase::xArtboardPropertyKey:
+                object->as<NodeBase>()->x(value);
+                break;
+            case NodeBase::yPropertyKey:
+            case NodeBase::yArtboardPropertyKey:
+                object->as<NodeBase>()->y(value);
+                break;
+            case NestedArtboardLayoutBase::instanceWidthPropertyKey:
+                object->as<NestedArtboardLayoutBase>()->instanceWidth(value);
+                break;
+            case NestedArtboardLayoutBase::instanceHeightPropertyKey:
+                object->as<NestedArtboardLayoutBase>()->instanceHeight(value);
+                break;
+            case AxisBase::offsetPropertyKey:
+                object->as<AxisBase>()->offset(value);
+                break;
+            case LayoutComponentStyleBase::gapHorizontalPropertyKey:
+                object->as<LayoutComponentStyleBase>()->gapHorizontal(value);
+                break;
+            case LayoutComponentStyleBase::gapVerticalPropertyKey:
+                object->as<LayoutComponentStyleBase>()->gapVertical(value);
+                break;
+            case LayoutComponentStyleBase::maxWidthPropertyKey:
+                object->as<LayoutComponentStyleBase>()->maxWidth(value);
+                break;
+            case LayoutComponentStyleBase::maxHeightPropertyKey:
+                object->as<LayoutComponentStyleBase>()->maxHeight(value);
+                break;
+            case LayoutComponentStyleBase::minWidthPropertyKey:
+                object->as<LayoutComponentStyleBase>()->minWidth(value);
+                break;
+            case LayoutComponentStyleBase::minHeightPropertyKey:
+                object->as<LayoutComponentStyleBase>()->minHeight(value);
+                break;
+            case LayoutComponentStyleBase::borderLeftPropertyKey:
+                object->as<LayoutComponentStyleBase>()->borderLeft(value);
+                break;
+            case LayoutComponentStyleBase::borderRightPropertyKey:
+                object->as<LayoutComponentStyleBase>()->borderRight(value);
+                break;
+            case LayoutComponentStyleBase::borderTopPropertyKey:
+                object->as<LayoutComponentStyleBase>()->borderTop(value);
+                break;
+            case LayoutComponentStyleBase::borderBottomPropertyKey:
+                object->as<LayoutComponentStyleBase>()->borderBottom(value);
+                break;
+            case LayoutComponentStyleBase::marginLeftPropertyKey:
+                object->as<LayoutComponentStyleBase>()->marginLeft(value);
+                break;
+            case LayoutComponentStyleBase::marginRightPropertyKey:
+                object->as<LayoutComponentStyleBase>()->marginRight(value);
+                break;
+            case LayoutComponentStyleBase::marginTopPropertyKey:
+                object->as<LayoutComponentStyleBase>()->marginTop(value);
+                break;
+            case LayoutComponentStyleBase::marginBottomPropertyKey:
+                object->as<LayoutComponentStyleBase>()->marginBottom(value);
+                break;
+            case LayoutComponentStyleBase::paddingLeftPropertyKey:
+                object->as<LayoutComponentStyleBase>()->paddingLeft(value);
+                break;
+            case LayoutComponentStyleBase::paddingRightPropertyKey:
+                object->as<LayoutComponentStyleBase>()->paddingRight(value);
+                break;
+            case LayoutComponentStyleBase::paddingTopPropertyKey:
+                object->as<LayoutComponentStyleBase>()->paddingTop(value);
+                break;
+            case LayoutComponentStyleBase::paddingBottomPropertyKey:
+                object->as<LayoutComponentStyleBase>()->paddingBottom(value);
+                break;
+            case LayoutComponentStyleBase::positionLeftPropertyKey:
+                object->as<LayoutComponentStyleBase>()->positionLeft(value);
+                break;
+            case LayoutComponentStyleBase::positionRightPropertyKey:
+                object->as<LayoutComponentStyleBase>()->positionRight(value);
+                break;
+            case LayoutComponentStyleBase::positionTopPropertyKey:
+                object->as<LayoutComponentStyleBase>()->positionTop(value);
+                break;
+            case LayoutComponentStyleBase::positionBottomPropertyKey:
+                object->as<LayoutComponentStyleBase>()->positionBottom(value);
+                break;
+            case LayoutComponentStyleBase::flexPropertyKey:
+                object->as<LayoutComponentStyleBase>()->flex(value);
+                break;
+            case LayoutComponentStyleBase::flexGrowPropertyKey:
+                object->as<LayoutComponentStyleBase>()->flexGrow(value);
+                break;
+            case LayoutComponentStyleBase::flexShrinkPropertyKey:
+                object->as<LayoutComponentStyleBase>()->flexShrink(value);
+                break;
+            case LayoutComponentStyleBase::flexBasisPropertyKey:
+                object->as<LayoutComponentStyleBase>()->flexBasis(value);
+                break;
+            case LayoutComponentStyleBase::aspectRatioPropertyKey:
+                object->as<LayoutComponentStyleBase>()->aspectRatio(value);
+                break;
+            case LayoutComponentStyleBase::interpolationTimePropertyKey:
+                object->as<LayoutComponentStyleBase>()->interpolationTime(value);
+                break;
+            case LayoutComponentStyleBase::cornerRadiusTLPropertyKey:
+                object->as<LayoutComponentStyleBase>()->cornerRadiusTL(value);
+                break;
+            case LayoutComponentStyleBase::cornerRadiusTRPropertyKey:
+                object->as<LayoutComponentStyleBase>()->cornerRadiusTR(value);
+                break;
+            case LayoutComponentStyleBase::cornerRadiusBLPropertyKey:
+                object->as<LayoutComponentStyleBase>()->cornerRadiusBL(value);
+                break;
+            case LayoutComponentStyleBase::cornerRadiusBRPropertyKey:
+                object->as<LayoutComponentStyleBase>()->cornerRadiusBR(value);
+                break;
+            case NestedLinearAnimationBase::mixPropertyKey:
+                object->as<NestedLinearAnimationBase>()->mix(value);
+                break;
+            case NestedSimpleAnimationBase::speedPropertyKey:
+                object->as<NestedSimpleAnimationBase>()->speed(value);
+                break;
+            case AdvanceableStateBase::speedPropertyKey:
+                object->as<AdvanceableStateBase>()->speed(value);
+                break;
+            case BlendAnimationDirectBase::mixValuePropertyKey:
+                object->as<BlendAnimationDirectBase>()->mixValue(value);
+                break;
+            case StateMachineNumberBase::valuePropertyKey:
+                object->as<StateMachineNumberBase>()->value(value);
+                break;
+            case CubicInterpolatorBase::x1PropertyKey:
+                object->as<CubicInterpolatorBase>()->x1(value);
+                break;
+            case CubicInterpolatorBase::y1PropertyKey:
+                object->as<CubicInterpolatorBase>()->y1(value);
+                break;
+            case CubicInterpolatorBase::x2PropertyKey:
+                object->as<CubicInterpolatorBase>()->x2(value);
+                break;
+            case CubicInterpolatorBase::y2PropertyKey:
+                object->as<CubicInterpolatorBase>()->y2(value);
+                break;
+            case TransitionNumberConditionBase::valuePropertyKey:
+                object->as<TransitionNumberConditionBase>()->value(value);
+                break;
+            case CubicInterpolatorComponentBase::x1PropertyKey:
+                object->as<CubicInterpolatorComponentBase>()->x1(value);
+                break;
+            case CubicInterpolatorComponentBase::y1PropertyKey:
+                object->as<CubicInterpolatorComponentBase>()->y1(value);
+                break;
+            case CubicInterpolatorComponentBase::x2PropertyKey:
+                object->as<CubicInterpolatorComponentBase>()->x2(value);
+                break;
+            case CubicInterpolatorComponentBase::y2PropertyKey:
+                object->as<CubicInterpolatorComponentBase>()->y2(value);
+                break;
+            case ListenerNumberChangeBase::valuePropertyKey:
+                object->as<ListenerNumberChangeBase>()->value(value);
+                break;
+            case KeyFrameDoubleBase::valuePropertyKey:
+                object->as<KeyFrameDoubleBase>()->value(value);
+                break;
+            case LinearAnimationBase::speedPropertyKey:
+                object->as<LinearAnimationBase>()->speed(value);
+                break;
+            case TransitionValueNumberComparatorBase::valuePropertyKey:
+                object->as<TransitionValueNumberComparatorBase>()->value(value);
+                break;
+            case ElasticInterpolatorBase::amplitudePropertyKey:
+                object->as<ElasticInterpolatorBase>()->amplitude(value);
+                break;
+            case ElasticInterpolatorBase::periodPropertyKey:
+                object->as<ElasticInterpolatorBase>()->period(value);
+                break;
+            case NestedNumberBase::nestedValuePropertyKey:
+                object->as<NestedNumberBase>()->nestedValue(value);
+                break;
+            case NestedRemapAnimationBase::timePropertyKey:
+                object->as<NestedRemapAnimationBase>()->time(value);
+                break;
+            case BlendAnimation1DBase::valuePropertyKey:
+                object->as<BlendAnimation1DBase>()->value(value);
+                break;
+            case LinearGradientBase::startXPropertyKey:
+                object->as<LinearGradientBase>()->startX(value);
+                break;
+            case LinearGradientBase::startYPropertyKey:
+                object->as<LinearGradientBase>()->startY(value);
+                break;
+            case LinearGradientBase::endXPropertyKey:
+                object->as<LinearGradientBase>()->endX(value);
+                break;
+            case LinearGradientBase::endYPropertyKey:
+                object->as<LinearGradientBase>()->endY(value);
+                break;
+            case LinearGradientBase::opacityPropertyKey:
+                object->as<LinearGradientBase>()->opacity(value);
+                break;
+            case StrokeBase::thicknessPropertyKey:
+                object->as<StrokeBase>()->thickness(value);
+                break;
+            case GradientStopBase::positionPropertyKey:
+                object->as<GradientStopBase>()->position(value);
+                break;
+            case TrimPathBase::startPropertyKey:
+                object->as<TrimPathBase>()->start(value);
+                break;
+            case TrimPathBase::endPropertyKey:
+                object->as<TrimPathBase>()->end(value);
+                break;
+            case TrimPathBase::offsetPropertyKey:
+                object->as<TrimPathBase>()->offset(value);
+                break;
+            case VertexBase::xPropertyKey:
+                object->as<VertexBase>()->x(value);
+                break;
+            case VertexBase::yPropertyKey:
+                object->as<VertexBase>()->y(value);
+                break;
+            case MeshVertexBase::uPropertyKey:
+                object->as<MeshVertexBase>()->u(value);
+                break;
+            case MeshVertexBase::vPropertyKey:
+                object->as<MeshVertexBase>()->v(value);
+                break;
+            case StraightVertexBase::radiusPropertyKey:
+                object->as<StraightVertexBase>()->radius(value);
+                break;
+            case CubicAsymmetricVertexBase::rotationPropertyKey:
+                object->as<CubicAsymmetricVertexBase>()->rotation(value);
+                break;
+            case CubicAsymmetricVertexBase::inDistancePropertyKey:
+                object->as<CubicAsymmetricVertexBase>()->inDistance(value);
+                break;
+            case CubicAsymmetricVertexBase::outDistancePropertyKey:
+                object->as<CubicAsymmetricVertexBase>()->outDistance(value);
+                break;
+            case ParametricPathBase::widthPropertyKey:
+                object->as<ParametricPathBase>()->width(value);
+                break;
+            case ParametricPathBase::heightPropertyKey:
+                object->as<ParametricPathBase>()->height(value);
+                break;
+            case ParametricPathBase::originXPropertyKey:
+                object->as<ParametricPathBase>()->originX(value);
+                break;
+            case ParametricPathBase::originYPropertyKey:
+                object->as<ParametricPathBase>()->originY(value);
+                break;
+            case RectangleBase::cornerRadiusTLPropertyKey:
+                object->as<RectangleBase>()->cornerRadiusTL(value);
+                break;
+            case RectangleBase::cornerRadiusTRPropertyKey:
+                object->as<RectangleBase>()->cornerRadiusTR(value);
+                break;
+            case RectangleBase::cornerRadiusBLPropertyKey:
+                object->as<RectangleBase>()->cornerRadiusBL(value);
+                break;
+            case RectangleBase::cornerRadiusBRPropertyKey:
+                object->as<RectangleBase>()->cornerRadiusBR(value);
+                break;
+            case CubicMirroredVertexBase::rotationPropertyKey:
+                object->as<CubicMirroredVertexBase>()->rotation(value);
+                break;
+            case CubicMirroredVertexBase::distancePropertyKey:
+                object->as<CubicMirroredVertexBase>()->distance(value);
+                break;
+            case PolygonBase::cornerRadiusPropertyKey:
+                object->as<PolygonBase>()->cornerRadius(value);
+                break;
+            case StarBase::innerRadiusPropertyKey:
+                object->as<StarBase>()->innerRadius(value);
+                break;
+            case ImageBase::originXPropertyKey:
+                object->as<ImageBase>()->originX(value);
+                break;
+            case ImageBase::originYPropertyKey:
+                object->as<ImageBase>()->originY(value);
+                break;
+            case CubicDetachedVertexBase::inRotationPropertyKey:
+                object->as<CubicDetachedVertexBase>()->inRotation(value);
+                break;
+            case CubicDetachedVertexBase::inDistancePropertyKey:
+                object->as<CubicDetachedVertexBase>()->inDistance(value);
+                break;
+            case CubicDetachedVertexBase::outRotationPropertyKey:
+                object->as<CubicDetachedVertexBase>()->outRotation(value);
+                break;
+            case CubicDetachedVertexBase::outDistancePropertyKey:
+                object->as<CubicDetachedVertexBase>()->outDistance(value);
+                break;
+            case LayoutComponentBase::widthPropertyKey:
+                object->as<LayoutComponentBase>()->width(value);
+                break;
+            case LayoutComponentBase::heightPropertyKey:
+                object->as<LayoutComponentBase>()->height(value);
+                break;
+            case ArtboardBase::originXPropertyKey:
+                object->as<ArtboardBase>()->originX(value);
+                break;
+            case ArtboardBase::originYPropertyKey:
+                object->as<ArtboardBase>()->originY(value);
+                break;
+            case JoystickBase::xPropertyKey:
+                object->as<JoystickBase>()->x(value);
+                break;
+            case JoystickBase::yPropertyKey:
+                object->as<JoystickBase>()->y(value);
+                break;
+            case JoystickBase::posXPropertyKey:
+                object->as<JoystickBase>()->posX(value);
+                break;
+            case JoystickBase::posYPropertyKey:
+                object->as<JoystickBase>()->posY(value);
+                break;
+            case JoystickBase::originXPropertyKey:
+                object->as<JoystickBase>()->originX(value);
+                break;
+            case JoystickBase::originYPropertyKey:
+                object->as<JoystickBase>()->originY(value);
+                break;
+            case JoystickBase::widthPropertyKey:
+                object->as<JoystickBase>()->width(value);
+                break;
+            case JoystickBase::heightPropertyKey:
+                object->as<JoystickBase>()->height(value);
+                break;
+            case DataConverterOperationBase::valuePropertyKey:
+                object->as<DataConverterOperationBase>()->value(value);
+                break;
+            case BindablePropertyNumberBase::propertyValuePropertyKey:
+                object->as<BindablePropertyNumberBase>()->propertyValue(value);
+                break;
+            case NestedArtboardLeafBase::alignmentXPropertyKey:
+                object->as<NestedArtboardLeafBase>()->alignmentX(value);
+                break;
+            case NestedArtboardLeafBase::alignmentYPropertyKey:
+                object->as<NestedArtboardLeafBase>()->alignmentY(value);
+                break;
+            case BoneBase::lengthPropertyKey:
+                object->as<BoneBase>()->length(value);
+                break;
+            case RootBoneBase::xPropertyKey:
+                object->as<RootBoneBase>()->x(value);
+                break;
+            case RootBoneBase::yPropertyKey:
+                object->as<RootBoneBase>()->y(value);
+                break;
+            case SkinBase::xxPropertyKey:
+                object->as<SkinBase>()->xx(value);
+                break;
+            case SkinBase::yxPropertyKey:
+                object->as<SkinBase>()->yx(value);
+                break;
+            case SkinBase::xyPropertyKey:
+                object->as<SkinBase>()->xy(value);
+                break;
+            case SkinBase::yyPropertyKey:
+                object->as<SkinBase>()->yy(value);
+                break;
+            case SkinBase::txPropertyKey:
+                object->as<SkinBase>()->tx(value);
+                break;
+            case SkinBase::tyPropertyKey:
+                object->as<SkinBase>()->ty(value);
+                break;
+            case TendonBase::xxPropertyKey:
+                object->as<TendonBase>()->xx(value);
+                break;
+            case TendonBase::yxPropertyKey:
+                object->as<TendonBase>()->yx(value);
+                break;
+            case TendonBase::xyPropertyKey:
+                object->as<TendonBase>()->xy(value);
+                break;
+            case TendonBase::yyPropertyKey:
+                object->as<TendonBase>()->yy(value);
+                break;
+            case TendonBase::txPropertyKey:
+                object->as<TendonBase>()->tx(value);
+                break;
+            case TendonBase::tyPropertyKey:
+                object->as<TendonBase>()->ty(value);
+                break;
+            case TextModifierRangeBase::modifyFromPropertyKey:
+                object->as<TextModifierRangeBase>()->modifyFrom(value);
+                break;
+            case TextModifierRangeBase::modifyToPropertyKey:
+                object->as<TextModifierRangeBase>()->modifyTo(value);
+                break;
+            case TextModifierRangeBase::strengthPropertyKey:
+                object->as<TextModifierRangeBase>()->strength(value);
+                break;
+            case TextModifierRangeBase::falloffFromPropertyKey:
+                object->as<TextModifierRangeBase>()->falloffFrom(value);
+                break;
+            case TextModifierRangeBase::falloffToPropertyKey:
+                object->as<TextModifierRangeBase>()->falloffTo(value);
+                break;
+            case TextModifierRangeBase::offsetPropertyKey:
+                object->as<TextModifierRangeBase>()->offset(value);
+                break;
+            case TextVariationModifierBase::axisValuePropertyKey:
+                object->as<TextVariationModifierBase>()->axisValue(value);
+                break;
+            case TextModifierGroupBase::originXPropertyKey:
+                object->as<TextModifierGroupBase>()->originX(value);
+                break;
+            case TextModifierGroupBase::originYPropertyKey:
+                object->as<TextModifierGroupBase>()->originY(value);
+                break;
+            case TextModifierGroupBase::opacityPropertyKey:
+                object->as<TextModifierGroupBase>()->opacity(value);
+                break;
+            case TextModifierGroupBase::xPropertyKey:
+                object->as<TextModifierGroupBase>()->x(value);
+                break;
+            case TextModifierGroupBase::yPropertyKey:
+                object->as<TextModifierGroupBase>()->y(value);
+                break;
+            case TextModifierGroupBase::rotationPropertyKey:
+                object->as<TextModifierGroupBase>()->rotation(value);
+                break;
+            case TextModifierGroupBase::scaleXPropertyKey:
+                object->as<TextModifierGroupBase>()->scaleX(value);
+                break;
+            case TextModifierGroupBase::scaleYPropertyKey:
+                object->as<TextModifierGroupBase>()->scaleY(value);
+                break;
+            case TextStyleBase::fontSizePropertyKey:
+                object->as<TextStyleBase>()->fontSize(value);
+                break;
+            case TextStyleBase::lineHeightPropertyKey:
+                object->as<TextStyleBase>()->lineHeight(value);
+                break;
+            case TextStyleBase::letterSpacingPropertyKey:
+                object->as<TextStyleBase>()->letterSpacing(value);
+                break;
+            case TextStyleAxisBase::axisValuePropertyKey:
+                object->as<TextStyleAxisBase>()->axisValue(value);
+                break;
+            case TextBase::widthPropertyKey:
+                object->as<TextBase>()->width(value);
+                break;
+            case TextBase::heightPropertyKey:
+                object->as<TextBase>()->height(value);
+                break;
+            case TextBase::originXPropertyKey:
+                object->as<TextBase>()->originX(value);
+                break;
+            case TextBase::originYPropertyKey:
+                object->as<TextBase>()->originY(value);
+                break;
+            case TextBase::paragraphSpacingPropertyKey:
+                object->as<TextBase>()->paragraphSpacing(value);
+                break;
+            case DrawableAssetBase::heightPropertyKey:
+                object->as<DrawableAssetBase>()->height(value);
+                break;
+            case DrawableAssetBase::widthPropertyKey:
+                object->as<DrawableAssetBase>()->width(value);
+                break;
+            case ExportAudioBase::volumePropertyKey:
+                object->as<ExportAudioBase>()->volume(value);
+                break;
+        }
+    }
+    static void setCallback(Core* object, int propertyKey, CallbackData value)
+    {
+        switch (propertyKey)
+        {
+            case NestedTriggerBase::firePropertyKey:
+                object->as<NestedTriggerBase>()->fire(value);
+                break;
+            case EventBase::triggerPropertyKey:
+                object->as<EventBase>()->trigger(value);
+                break;
+        }
+    }
+    static bool getBool(Core* object, int propertyKey)
+    {
+        switch (propertyKey)
+        {
+            case ViewModelInstanceListItemBase::useLinkedArtboardPropertyKey:
+                return object->as<ViewModelInstanceListItemBase>()->useLinkedArtboard();
+            case ViewModelInstanceBooleanBase::propertyValuePropertyKey:
+                return object->as<ViewModelInstanceBooleanBase>()->propertyValue();
+            case TransformComponentConstraintBase::offsetPropertyKey:
+                return object->as<TransformComponentConstraintBase>()->offset();
+            case TransformComponentConstraintBase::doesCopyPropertyKey:
+                return object->as<TransformComponentConstraintBase>()->doesCopy();
+            case TransformComponentConstraintBase::minPropertyKey:
+                return object->as<TransformComponentConstraintBase>()->min();
+            case TransformComponentConstraintBase::maxPropertyKey:
+                return object->as<TransformComponentConstraintBase>()->max();
+            case TransformComponentConstraintYBase::doesCopyYPropertyKey:
+                return object->as<TransformComponentConstraintYBase>()->doesCopyY();
+            case TransformComponentConstraintYBase::minYPropertyKey:
+                return object->as<TransformComponentConstraintYBase>()->minY();
+            case TransformComponentConstraintYBase::maxYPropertyKey:
+                return object->as<TransformComponentConstraintYBase>()->maxY();
+            case IKConstraintBase::invertDirectionPropertyKey:
+                return object->as<IKConstraintBase>()->invertDirection();
+            case FollowPathConstraintBase::orientPropertyKey:
+                return object->as<FollowPathConstraintBase>()->orient();
+            case FollowPathConstraintBase::offsetPropertyKey:
+                return object->as<FollowPathConstraintBase>()->offset();
+            case AxisBase::normalizedPropertyKey:
+                return object->as<AxisBase>()->normalized();
+            case LayoutComponentStyleBase::intrinsicallySizedValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->intrinsicallySizedValue();
+            case LayoutComponentStyleBase::linkCornerRadiusPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->linkCornerRadius();
+            case NestedSimpleAnimationBase::isPlayingPropertyKey:
+                return object->as<NestedSimpleAnimationBase>()->isPlaying();
+            case KeyFrameBoolBase::valuePropertyKey:
+                return object->as<KeyFrameBoolBase>()->value();
+            case ListenerAlignTargetBase::preserveOffsetPropertyKey:
+                return object->as<ListenerAlignTargetBase>()->preserveOffset();
+            case TransitionValueBooleanComparatorBase::valuePropertyKey:
+                return object->as<TransitionValueBooleanComparatorBase>()->value();
+            case NestedBoolBase::nestedValuePropertyKey:
+                return object->as<NestedBoolBase>()->nestedValue();
+            case LinearAnimationBase::enableWorkAreaPropertyKey:
+                return object->as<LinearAnimationBase>()->enableWorkArea();
+            case LinearAnimationBase::quantizePropertyKey:
+                return object->as<LinearAnimationBase>()->quantize();
+            case StateMachineBoolBase::valuePropertyKey:
+                return object->as<StateMachineBoolBase>()->value();
+            case ShapePaintBase::isVisiblePropertyKey:
+                return object->as<ShapePaintBase>()->isVisible();
+            case StrokeBase::transformAffectsStrokePropertyKey:
+                return object->as<StrokeBase>()->transformAffectsStroke();
+            case PointsPathBase::isClosedPropertyKey:
+                return object->as<PointsPathBase>()->isClosed();
+            case RectangleBase::linkCornerRadiusPropertyKey:
+                return object->as<RectangleBase>()->linkCornerRadius();
+            case ClippingShapeBase::isVisiblePropertyKey:
+                return object->as<ClippingShapeBase>()->isVisible();
+            case CustomPropertyBooleanBase::propertyValuePropertyKey:
+                return object->as<CustomPropertyBooleanBase>()->propertyValue();
+            case LayoutComponentBase::clipPropertyKey:
+                return object->as<LayoutComponentBase>()->clip();
+            case BindablePropertyBooleanBase::propertyValuePropertyKey:
+                return object->as<BindablePropertyBooleanBase>()->propertyValue();
+            case TextModifierRangeBase::clampPropertyKey:
+                return object->as<TextModifierRangeBase>()->clamp();
+        }
+        return false;
+    }
+    static uint32_t getUint(Core* object, int propertyKey)
+    {
+        switch (propertyKey)
+        {
+            case ViewModelInstanceListItemBase::viewModelIdPropertyKey:
+                return object->as<ViewModelInstanceListItemBase>()->viewModelId();
+            case ViewModelInstanceListItemBase::viewModelInstanceIdPropertyKey:
+                return object->as<ViewModelInstanceListItemBase>()->viewModelInstanceId();
+            case ViewModelInstanceListItemBase::artboardIdPropertyKey:
+                return object->as<ViewModelInstanceListItemBase>()->artboardId();
+            case ViewModelInstanceValueBase::viewModelPropertyIdPropertyKey:
+                return object->as<ViewModelInstanceValueBase>()->viewModelPropertyId();
+            case ViewModelInstanceEnumBase::propertyValuePropertyKey:
+                return object->as<ViewModelInstanceEnumBase>()->propertyValue();
+            case ViewModelBase::defaultInstanceIdPropertyKey:
+                return object->as<ViewModelBase>()->defaultInstanceId();
+            case ViewModelPropertyViewModelBase::viewModelReferenceIdPropertyKey:
+                return object->as<ViewModelPropertyViewModelBase>()->viewModelReferenceId();
+            case ComponentBase::parentIdPropertyKey:
+                return object->as<ComponentBase>()->parentId();
+            case ViewModelInstanceBase::viewModelIdPropertyKey:
+                return object->as<ViewModelInstanceBase>()->viewModelId();
+            case ViewModelPropertyEnumBase::enumIdPropertyKey:
+                return object->as<ViewModelPropertyEnumBase>()->enumId();
+            case ViewModelInstanceViewModelBase::propertyValuePropertyKey:
+                return object->as<ViewModelInstanceViewModelBase>()->propertyValue();
+            case DrawTargetBase::drawableIdPropertyKey:
+                return object->as<DrawTargetBase>()->drawableId();
+            case DrawTargetBase::placementValuePropertyKey:
+                return object->as<DrawTargetBase>()->placementValue();
+            case TargetedConstraintBase::targetIdPropertyKey:
+                return object->as<TargetedConstraintBase>()->targetId();
+            case DistanceConstraintBase::modeValuePropertyKey:
+                return object->as<DistanceConstraintBase>()->modeValue();
+            case TransformSpaceConstraintBase::sourceSpaceValuePropertyKey:
+                return object->as<TransformSpaceConstraintBase>()->sourceSpaceValue();
+            case TransformSpaceConstraintBase::destSpaceValuePropertyKey:
+                return object->as<TransformSpaceConstraintBase>()->destSpaceValue();
+            case TransformComponentConstraintBase::minMaxSpaceValuePropertyKey:
+                return object->as<TransformComponentConstraintBase>()->minMaxSpaceValue();
+            case IKConstraintBase::parentBoneCountPropertyKey:
+                return object->as<IKConstraintBase>()->parentBoneCount();
+            case DrawableBase::blendModeValuePropertyKey:
+                return object->as<DrawableBase>()->blendModeValue();
+            case DrawableBase::drawableFlagsPropertyKey:
+                return object->as<DrawableBase>()->drawableFlags();
+            case NestedArtboardBase::artboardIdPropertyKey:
+                return object->as<NestedArtboardBase>()->artboardId();
+            case NestedAnimationBase::animationIdPropertyKey:
+                return object->as<NestedAnimationBase>()->animationId();
+            case SoloBase::activeComponentIdPropertyKey:
+                return object->as<SoloBase>()->activeComponentId();
+            case NestedArtboardLayoutBase::instanceWidthUnitsValuePropertyKey:
+                return object->as<NestedArtboardLayoutBase>()->instanceWidthUnitsValue();
+            case NestedArtboardLayoutBase::instanceHeightUnitsValuePropertyKey:
+                return object->as<NestedArtboardLayoutBase>()->instanceHeightUnitsValue();
+            case NestedArtboardLayoutBase::instanceWidthScaleTypePropertyKey:
+                return object->as<NestedArtboardLayoutBase>()->instanceWidthScaleType();
+            case NestedArtboardLayoutBase::instanceHeightScaleTypePropertyKey:
+                return object->as<NestedArtboardLayoutBase>()->instanceHeightScaleType();
+            case NSlicerTileModeBase::patchIndexPropertyKey:
+                return object->as<NSlicerTileModeBase>()->patchIndex();
+            case NSlicerTileModeBase::stylePropertyKey:
+                return object->as<NSlicerTileModeBase>()->style();
+            case LayoutComponentStyleBase::layoutWidthScaleTypePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->layoutWidthScaleType();
+            case LayoutComponentStyleBase::layoutHeightScaleTypePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->layoutHeightScaleType();
+            case LayoutComponentStyleBase::layoutAlignmentTypePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->layoutAlignmentType();
+            case LayoutComponentStyleBase::animationStyleTypePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->animationStyleType();
+            case LayoutComponentStyleBase::interpolationTypePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->interpolationType();
+            case LayoutComponentStyleBase::interpolatorIdPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->interpolatorId();
+            case LayoutComponentStyleBase::displayValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->displayValue();
+            case LayoutComponentStyleBase::positionTypeValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->positionTypeValue();
+            case LayoutComponentStyleBase::flexDirectionValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->flexDirectionValue();
+            case LayoutComponentStyleBase::directionValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->directionValue();
+            case LayoutComponentStyleBase::alignContentValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->alignContentValue();
+            case LayoutComponentStyleBase::alignItemsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->alignItemsValue();
+            case LayoutComponentStyleBase::alignSelfValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->alignSelfValue();
+            case LayoutComponentStyleBase::justifyContentValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->justifyContentValue();
+            case LayoutComponentStyleBase::flexWrapValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->flexWrapValue();
+            case LayoutComponentStyleBase::overflowValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->overflowValue();
+            case LayoutComponentStyleBase::widthUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->widthUnitsValue();
+            case LayoutComponentStyleBase::heightUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->heightUnitsValue();
+            case LayoutComponentStyleBase::borderLeftUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->borderLeftUnitsValue();
+            case LayoutComponentStyleBase::borderRightUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->borderRightUnitsValue();
+            case LayoutComponentStyleBase::borderTopUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->borderTopUnitsValue();
+            case LayoutComponentStyleBase::borderBottomUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->borderBottomUnitsValue();
+            case LayoutComponentStyleBase::marginLeftUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->marginLeftUnitsValue();
+            case LayoutComponentStyleBase::marginRightUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->marginRightUnitsValue();
+            case LayoutComponentStyleBase::marginTopUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->marginTopUnitsValue();
+            case LayoutComponentStyleBase::marginBottomUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->marginBottomUnitsValue();
+            case LayoutComponentStyleBase::paddingLeftUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->paddingLeftUnitsValue();
+            case LayoutComponentStyleBase::paddingRightUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->paddingRightUnitsValue();
+            case LayoutComponentStyleBase::paddingTopUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->paddingTopUnitsValue();
+            case LayoutComponentStyleBase::paddingBottomUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->paddingBottomUnitsValue();
+            case LayoutComponentStyleBase::positionLeftUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->positionLeftUnitsValue();
+            case LayoutComponentStyleBase::positionRightUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->positionRightUnitsValue();
+            case LayoutComponentStyleBase::positionTopUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->positionTopUnitsValue();
+            case LayoutComponentStyleBase::positionBottomUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->positionBottomUnitsValue();
+            case LayoutComponentStyleBase::gapHorizontalUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->gapHorizontalUnitsValue();
+            case LayoutComponentStyleBase::gapVerticalUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->gapVerticalUnitsValue();
+            case LayoutComponentStyleBase::minWidthUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->minWidthUnitsValue();
+            case LayoutComponentStyleBase::minHeightUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->minHeightUnitsValue();
+            case LayoutComponentStyleBase::maxWidthUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->maxWidthUnitsValue();
+            case LayoutComponentStyleBase::maxHeightUnitsValuePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->maxHeightUnitsValue();
+            case ListenerFireEventBase::eventIdPropertyKey:
+                return object->as<ListenerFireEventBase>()->eventId();
+            case LayerStateBase::flagsPropertyKey:
+                return object->as<LayerStateBase>()->flags();
+            case KeyFrameBase::framePropertyKey:
+                return object->as<KeyFrameBase>()->frame();
+            case InterpolatingKeyFrameBase::interpolationTypePropertyKey:
+                return object->as<InterpolatingKeyFrameBase>()->interpolationType();
+            case InterpolatingKeyFrameBase::interpolatorIdPropertyKey:
+                return object->as<InterpolatingKeyFrameBase>()->interpolatorId();
+            case KeyFrameUintBase::valuePropertyKey:
+                return object->as<KeyFrameUintBase>()->value();
+            case ListenerInputChangeBase::inputIdPropertyKey:
+                return object->as<ListenerInputChangeBase>()->inputId();
+            case ListenerInputChangeBase::nestedInputIdPropertyKey:
+                return object->as<ListenerInputChangeBase>()->nestedInputId();
+            case AnimationStateBase::animationIdPropertyKey:
+                return object->as<AnimationStateBase>()->animationId();
+            case NestedInputBase::inputIdPropertyKey:
+                return object->as<NestedInputBase>()->inputId();
+            case KeyedObjectBase::objectIdPropertyKey:
+                return object->as<KeyedObjectBase>()->objectId();
+            case BlendAnimationBase::animationIdPropertyKey:
+                return object->as<BlendAnimationBase>()->animationId();
+            case BlendAnimationDirectBase::inputIdPropertyKey:
+                return object->as<BlendAnimationDirectBase>()->inputId();
+            case BlendAnimationDirectBase::blendSourcePropertyKey:
+                return object->as<BlendAnimationDirectBase>()->blendSource();
+            case TransitionInputConditionBase::inputIdPropertyKey:
+                return object->as<TransitionInputConditionBase>()->inputId();
+            case KeyedPropertyBase::propertyKeyPropertyKey:
+                return object->as<KeyedPropertyBase>()->propertyKey();
+            case StateMachineListenerBase::targetIdPropertyKey:
+                return object->as<StateMachineListenerBase>()->targetId();
+            case StateMachineListenerBase::listenerTypeValuePropertyKey:
+                return object->as<StateMachineListenerBase>()->listenerTypeValue();
+            case StateMachineListenerBase::eventIdPropertyKey:
+                return object->as<StateMachineListenerBase>()->eventId();
+            case TransitionPropertyArtboardComparatorBase::propertyTypePropertyKey:
+                return object->as<TransitionPropertyArtboardComparatorBase>()->propertyType();
+            case KeyFrameIdBase::valuePropertyKey:
+                return object->as<KeyFrameIdBase>()->value();
+            case ListenerBoolChangeBase::valuePropertyKey:
+                return object->as<ListenerBoolChangeBase>()->value();
+            case ListenerAlignTargetBase::targetIdPropertyKey:
+                return object->as<ListenerAlignTargetBase>()->targetId();
+            case TransitionValueConditionBase::opValuePropertyKey:
+                return object->as<TransitionValueConditionBase>()->opValue();
+            case TransitionViewModelConditionBase::leftComparatorIdPropertyKey:
+                return object->as<TransitionViewModelConditionBase>()->leftComparatorId();
+            case TransitionViewModelConditionBase::rightComparatorIdPropertyKey:
+                return object->as<TransitionViewModelConditionBase>()->rightComparatorId();
+            case TransitionViewModelConditionBase::opValuePropertyKey:
+                return object->as<TransitionViewModelConditionBase>()->opValue();
+            case StateTransitionBase::stateToIdPropertyKey:
+                return object->as<StateTransitionBase>()->stateToId();
+            case StateTransitionBase::flagsPropertyKey:
+                return object->as<StateTransitionBase>()->flags();
+            case StateTransitionBase::durationPropertyKey:
+                return object->as<StateTransitionBase>()->duration();
+            case StateTransitionBase::exitTimePropertyKey:
+                return object->as<StateTransitionBase>()->exitTime();
+            case StateTransitionBase::interpolationTypePropertyKey:
+                return object->as<StateTransitionBase>()->interpolationType();
+            case StateTransitionBase::interpolatorIdPropertyKey:
+                return object->as<StateTransitionBase>()->interpolatorId();
+            case StateTransitionBase::randomWeightPropertyKey:
+                return object->as<StateTransitionBase>()->randomWeight();
+            case StateMachineFireEventBase::eventIdPropertyKey:
+                return object->as<StateMachineFireEventBase>()->eventId();
+            case StateMachineFireEventBase::occursValuePropertyKey:
+                return object->as<StateMachineFireEventBase>()->occursValue();
+            case LinearAnimationBase::fpsPropertyKey:
+                return object->as<LinearAnimationBase>()->fps();
+            case LinearAnimationBase::durationPropertyKey:
+                return object->as<LinearAnimationBase>()->duration();
+            case LinearAnimationBase::loopValuePropertyKey:
+                return object->as<LinearAnimationBase>()->loopValue();
+            case LinearAnimationBase::workStartPropertyKey:
+                return object->as<LinearAnimationBase>()->workStart();
+            case LinearAnimationBase::workEndPropertyKey:
+                return object->as<LinearAnimationBase>()->workEnd();
+            case ElasticInterpolatorBase::easingValuePropertyKey:
+                return object->as<ElasticInterpolatorBase>()->easingValue();
+            case BlendState1DBase::inputIdPropertyKey:
+                return object->as<BlendState1DBase>()->inputId();
+            case TransitionValueEnumComparatorBase::valuePropertyKey:
+                return object->as<TransitionValueEnumComparatorBase>()->value();
+            case BlendStateTransitionBase::exitBlendAnimationIdPropertyKey:
+                return object->as<BlendStateTransitionBase>()->exitBlendAnimationId();
+            case StrokeBase::capPropertyKey:
+                return object->as<StrokeBase>()->cap();
+            case StrokeBase::joinPropertyKey:
+                return object->as<StrokeBase>()->join();
+            case TrimPathBase::modeValuePropertyKey:
+                return object->as<TrimPathBase>()->modeValue();
+            case FillBase::fillRulePropertyKey:
+                return object->as<FillBase>()->fillRule();
+            case PathBase::pathFlagsPropertyKey:
+                return object->as<PathBase>()->pathFlags();
+            case ClippingShapeBase::sourceIdPropertyKey:
+                return object->as<ClippingShapeBase>()->sourceId();
+            case ClippingShapeBase::fillRulePropertyKey:
+                return object->as<ClippingShapeBase>()->fillRule();
+            case PolygonBase::pointsPropertyKey:
+                return object->as<PolygonBase>()->points();
+            case ImageBase::assetIdPropertyKey:
+                return object->as<ImageBase>()->assetId();
+            case DrawRulesBase::drawTargetIdPropertyKey:
+                return object->as<DrawRulesBase>()->drawTargetId();
+            case LayoutComponentBase::styleIdPropertyKey:
+                return object->as<LayoutComponentBase>()->styleId();
+            case ArtboardBase::defaultStateMachineIdPropertyKey:
+                return object->as<ArtboardBase>()->defaultStateMachineId();
+            case ArtboardBase::viewModelIdPropertyKey:
+                return object->as<ArtboardBase>()->viewModelId();
+            case JoystickBase::xIdPropertyKey:
+                return object->as<JoystickBase>()->xId();
+            case JoystickBase::yIdPropertyKey:
+                return object->as<JoystickBase>()->yId();
+            case JoystickBase::joystickFlagsPropertyKey:
+                return object->as<JoystickBase>()->joystickFlags();
+            case JoystickBase::handleSourceIdPropertyKey:
+                return object->as<JoystickBase>()->handleSourceId();
+            case OpenUrlEventBase::targetValuePropertyKey:
+                return object->as<OpenUrlEventBase>()->targetValue();
+            case DataBindBase::propertyKeyPropertyKey:
+                return object->as<DataBindBase>()->propertyKey();
+            case DataBindBase::flagsPropertyKey:
+                return object->as<DataBindBase>()->flags();
+            case DataBindBase::converterIdPropertyKey:
+                return object->as<DataBindBase>()->converterId();
+            case DataConverterGroupItemBase::converterIdPropertyKey:
+                return object->as<DataConverterGroupItemBase>()->converterId();
+            case DataConverterRounderBase::decimalsPropertyKey:
+                return object->as<DataConverterRounderBase>()->decimals();
+            case DataConverterOperationBase::operationTypePropertyKey:
+                return object->as<DataConverterOperationBase>()->operationType();
+            case BindablePropertyEnumBase::propertyValuePropertyKey:
+                return object->as<BindablePropertyEnumBase>()->propertyValue();
+            case NestedArtboardLeafBase::fitPropertyKey:
+                return object->as<NestedArtboardLeafBase>()->fit();
+            case WeightBase::valuesPropertyKey:
+                return object->as<WeightBase>()->values();
+            case WeightBase::indicesPropertyKey:
+                return object->as<WeightBase>()->indices();
+            case TendonBase::boneIdPropertyKey:
+                return object->as<TendonBase>()->boneId();
+            case CubicWeightBase::inValuesPropertyKey:
+                return object->as<CubicWeightBase>()->inValues();
+            case CubicWeightBase::inIndicesPropertyKey:
+                return object->as<CubicWeightBase>()->inIndices();
+            case CubicWeightBase::outValuesPropertyKey:
+                return object->as<CubicWeightBase>()->outValues();
+            case CubicWeightBase::outIndicesPropertyKey:
+                return object->as<CubicWeightBase>()->outIndices();
+            case TextModifierRangeBase::unitsValuePropertyKey:
+                return object->as<TextModifierRangeBase>()->unitsValue();
+            case TextModifierRangeBase::typeValuePropertyKey:
+                return object->as<TextModifierRangeBase>()->typeValue();
+            case TextModifierRangeBase::modeValuePropertyKey:
+                return object->as<TextModifierRangeBase>()->modeValue();
+            case TextModifierRangeBase::runIdPropertyKey:
+                return object->as<TextModifierRangeBase>()->runId();
+            case TextStyleFeatureBase::tagPropertyKey:
+                return object->as<TextStyleFeatureBase>()->tag();
+            case TextStyleFeatureBase::featureValuePropertyKey:
+                return object->as<TextStyleFeatureBase>()->featureValue();
+            case TextVariationModifierBase::axisTagPropertyKey:
+                return object->as<TextVariationModifierBase>()->axisTag();
+            case TextModifierGroupBase::modifierFlagsPropertyKey:
+                return object->as<TextModifierGroupBase>()->modifierFlags();
+            case TextStyleBase::fontAssetIdPropertyKey:
+                return object->as<TextStyleBase>()->fontAssetId();
+            case TextStyleAxisBase::tagPropertyKey:
+                return object->as<TextStyleAxisBase>()->tag();
+            case TextBase::alignValuePropertyKey:
+                return object->as<TextBase>()->alignValue();
+            case TextBase::sizingValuePropertyKey:
+                return object->as<TextBase>()->sizingValue();
+            case TextBase::overflowValuePropertyKey:
+                return object->as<TextBase>()->overflowValue();
+            case TextBase::originValuePropertyKey:
+                return object->as<TextBase>()->originValue();
+            case TextValueRunBase::styleIdPropertyKey:
+                return object->as<TextValueRunBase>()->styleId();
+            case FileAssetBase::assetIdPropertyKey:
+                return object->as<FileAssetBase>()->assetId();
+            case AudioEventBase::assetIdPropertyKey:
+                return object->as<AudioEventBase>()->assetId();
+        }
+        return 0;
+    }
+    static int getColor(Core* object, int propertyKey)
+    {
+        switch (propertyKey)
+        {
+            case ViewModelInstanceColorBase::propertyValuePropertyKey:
+                return object->as<ViewModelInstanceColorBase>()->propertyValue();
+            case KeyFrameColorBase::valuePropertyKey:
+                return object->as<KeyFrameColorBase>()->value();
+            case TransitionValueColorComparatorBase::valuePropertyKey:
+                return object->as<TransitionValueColorComparatorBase>()->value();
+            case SolidColorBase::colorValuePropertyKey:
+                return object->as<SolidColorBase>()->colorValue();
+            case GradientStopBase::colorValuePropertyKey:
+                return object->as<GradientStopBase>()->colorValue();
+            case BindablePropertyColorBase::propertyValuePropertyKey:
+                return object->as<BindablePropertyColorBase>()->propertyValue();
+        }
+        return 0;
+    }
+    static std::string getString(Core* object, int propertyKey)
+    {
+        switch (propertyKey)
+        {
+            case ViewModelComponentBase::namePropertyKey:
+                return object->as<ViewModelComponentBase>()->name();
+            case ViewModelInstanceStringBase::propertyValuePropertyKey:
+                return object->as<ViewModelInstanceStringBase>()->propertyValue();
+            case ComponentBase::namePropertyKey:
+                return object->as<ComponentBase>()->name();
+            case DataEnumValueBase::keyPropertyKey:
+                return object->as<DataEnumValueBase>()->key();
+            case DataEnumValueBase::valuePropertyKey:
+                return object->as<DataEnumValueBase>()->value();
+            case AnimationBase::namePropertyKey:
+                return object->as<AnimationBase>()->name();
+            case StateMachineComponentBase::namePropertyKey:
+                return object->as<StateMachineComponentBase>()->name();
+            case KeyFrameStringBase::valuePropertyKey:
+                return object->as<KeyFrameStringBase>()->value();
+            case TransitionValueStringComparatorBase::valuePropertyKey:
+                return object->as<TransitionValueStringComparatorBase>()->value();
+            case OpenUrlEventBase::urlPropertyKey:
+                return object->as<OpenUrlEventBase>()->url();
+            case DataConverterBase::namePropertyKey:
+                return object->as<DataConverterBase>()->name();
+            case BindablePropertyStringBase::propertyValuePropertyKey:
+                return object->as<BindablePropertyStringBase>()->propertyValue();
+            case TextValueRunBase::textPropertyKey:
+                return object->as<TextValueRunBase>()->text();
+            case CustomPropertyStringBase::propertyValuePropertyKey:
+                return object->as<CustomPropertyStringBase>()->propertyValue();
+            case AssetBase::namePropertyKey:
+                return object->as<AssetBase>()->name();
+            case FileAssetBase::cdnBaseUrlPropertyKey:
+                return object->as<FileAssetBase>()->cdnBaseUrl();
+        }
+        return "";
+    }
+    static float getDouble(Core* object, int propertyKey)
+    {
+        switch (propertyKey)
+        {
+            case ViewModelInstanceNumberBase::propertyValuePropertyKey:
+                return object->as<ViewModelInstanceNumberBase>()->propertyValue();
+            case CustomPropertyNumberBase::propertyValuePropertyKey:
+                return object->as<CustomPropertyNumberBase>()->propertyValue();
+            case ConstraintBase::strengthPropertyKey:
+                return object->as<ConstraintBase>()->strength();
+            case DistanceConstraintBase::distancePropertyKey:
+                return object->as<DistanceConstraintBase>()->distance();
+            case TransformComponentConstraintBase::copyFactorPropertyKey:
+                return object->as<TransformComponentConstraintBase>()->copyFactor();
+            case TransformComponentConstraintBase::minValuePropertyKey:
+                return object->as<TransformComponentConstraintBase>()->minValue();
+            case TransformComponentConstraintBase::maxValuePropertyKey:
+                return object->as<TransformComponentConstraintBase>()->maxValue();
+            case TransformComponentConstraintYBase::copyFactorYPropertyKey:
+                return object->as<TransformComponentConstraintYBase>()->copyFactorY();
+            case TransformComponentConstraintYBase::minValueYPropertyKey:
+                return object->as<TransformComponentConstraintYBase>()->minValueY();
+            case TransformComponentConstraintYBase::maxValueYPropertyKey:
+                return object->as<TransformComponentConstraintYBase>()->maxValueY();
+            case FollowPathConstraintBase::distancePropertyKey:
+                return object->as<FollowPathConstraintBase>()->distance();
+            case TransformConstraintBase::originXPropertyKey:
+                return object->as<TransformConstraintBase>()->originX();
+            case TransformConstraintBase::originYPropertyKey:
+                return object->as<TransformConstraintBase>()->originY();
+            case WorldTransformComponentBase::opacityPropertyKey:
+                return object->as<WorldTransformComponentBase>()->opacity();
+            case TransformComponentBase::rotationPropertyKey:
+                return object->as<TransformComponentBase>()->rotation();
+            case TransformComponentBase::scaleXPropertyKey:
+                return object->as<TransformComponentBase>()->scaleX();
+            case TransformComponentBase::scaleYPropertyKey:
+                return object->as<TransformComponentBase>()->scaleY();
+            case NodeBase::xPropertyKey:
+            case NodeBase::xArtboardPropertyKey:
+                return object->as<NodeBase>()->x();
+            case NodeBase::yPropertyKey:
+            case NodeBase::yArtboardPropertyKey:
+                return object->as<NodeBase>()->y();
+            case NestedArtboardLayoutBase::instanceWidthPropertyKey:
+                return object->as<NestedArtboardLayoutBase>()->instanceWidth();
+            case NestedArtboardLayoutBase::instanceHeightPropertyKey:
+                return object->as<NestedArtboardLayoutBase>()->instanceHeight();
+            case AxisBase::offsetPropertyKey:
+                return object->as<AxisBase>()->offset();
+            case LayoutComponentStyleBase::gapHorizontalPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->gapHorizontal();
+            case LayoutComponentStyleBase::gapVerticalPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->gapVertical();
+            case LayoutComponentStyleBase::maxWidthPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->maxWidth();
+            case LayoutComponentStyleBase::maxHeightPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->maxHeight();
+            case LayoutComponentStyleBase::minWidthPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->minWidth();
+            case LayoutComponentStyleBase::minHeightPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->minHeight();
+            case LayoutComponentStyleBase::borderLeftPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->borderLeft();
+            case LayoutComponentStyleBase::borderRightPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->borderRight();
+            case LayoutComponentStyleBase::borderTopPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->borderTop();
+            case LayoutComponentStyleBase::borderBottomPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->borderBottom();
+            case LayoutComponentStyleBase::marginLeftPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->marginLeft();
+            case LayoutComponentStyleBase::marginRightPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->marginRight();
+            case LayoutComponentStyleBase::marginTopPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->marginTop();
+            case LayoutComponentStyleBase::marginBottomPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->marginBottom();
+            case LayoutComponentStyleBase::paddingLeftPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->paddingLeft();
+            case LayoutComponentStyleBase::paddingRightPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->paddingRight();
+            case LayoutComponentStyleBase::paddingTopPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->paddingTop();
+            case LayoutComponentStyleBase::paddingBottomPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->paddingBottom();
+            case LayoutComponentStyleBase::positionLeftPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->positionLeft();
+            case LayoutComponentStyleBase::positionRightPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->positionRight();
+            case LayoutComponentStyleBase::positionTopPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->positionTop();
+            case LayoutComponentStyleBase::positionBottomPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->positionBottom();
+            case LayoutComponentStyleBase::flexPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->flex();
+            case LayoutComponentStyleBase::flexGrowPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->flexGrow();
+            case LayoutComponentStyleBase::flexShrinkPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->flexShrink();
+            case LayoutComponentStyleBase::flexBasisPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->flexBasis();
+            case LayoutComponentStyleBase::aspectRatioPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->aspectRatio();
+            case LayoutComponentStyleBase::interpolationTimePropertyKey:
+                return object->as<LayoutComponentStyleBase>()->interpolationTime();
+            case LayoutComponentStyleBase::cornerRadiusTLPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->cornerRadiusTL();
+            case LayoutComponentStyleBase::cornerRadiusTRPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->cornerRadiusTR();
+            case LayoutComponentStyleBase::cornerRadiusBLPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->cornerRadiusBL();
+            case LayoutComponentStyleBase::cornerRadiusBRPropertyKey:
+                return object->as<LayoutComponentStyleBase>()->cornerRadiusBR();
+            case NestedLinearAnimationBase::mixPropertyKey:
+                return object->as<NestedLinearAnimationBase>()->mix();
+            case NestedSimpleAnimationBase::speedPropertyKey:
+                return object->as<NestedSimpleAnimationBase>()->speed();
+            case AdvanceableStateBase::speedPropertyKey:
+                return object->as<AdvanceableStateBase>()->speed();
+            case BlendAnimationDirectBase::mixValuePropertyKey:
+                return object->as<BlendAnimationDirectBase>()->mixValue();
+            case StateMachineNumberBase::valuePropertyKey:
+                return object->as<StateMachineNumberBase>()->value();
+            case CubicInterpolatorBase::x1PropertyKey:
+                return object->as<CubicInterpolatorBase>()->x1();
+            case CubicInterpolatorBase::y1PropertyKey:
+                return object->as<CubicInterpolatorBase>()->y1();
+            case CubicInterpolatorBase::x2PropertyKey:
+                return object->as<CubicInterpolatorBase>()->x2();
+            case CubicInterpolatorBase::y2PropertyKey:
+                return object->as<CubicInterpolatorBase>()->y2();
+            case TransitionNumberConditionBase::valuePropertyKey:
+                return object->as<TransitionNumberConditionBase>()->value();
+            case CubicInterpolatorComponentBase::x1PropertyKey:
+                return object->as<CubicInterpolatorComponentBase>()->x1();
+            case CubicInterpolatorComponentBase::y1PropertyKey:
+                return object->as<CubicInterpolatorComponentBase>()->y1();
+            case CubicInterpolatorComponentBase::x2PropertyKey:
+                return object->as<CubicInterpolatorComponentBase>()->x2();
+            case CubicInterpolatorComponentBase::y2PropertyKey:
+                return object->as<CubicInterpolatorComponentBase>()->y2();
+            case ListenerNumberChangeBase::valuePropertyKey:
+                return object->as<ListenerNumberChangeBase>()->value();
+            case KeyFrameDoubleBase::valuePropertyKey:
+                return object->as<KeyFrameDoubleBase>()->value();
+            case LinearAnimationBase::speedPropertyKey:
+                return object->as<LinearAnimationBase>()->speed();
+            case TransitionValueNumberComparatorBase::valuePropertyKey:
+                return object->as<TransitionValueNumberComparatorBase>()->value();
+            case ElasticInterpolatorBase::amplitudePropertyKey:
+                return object->as<ElasticInterpolatorBase>()->amplitude();
+            case ElasticInterpolatorBase::periodPropertyKey:
+                return object->as<ElasticInterpolatorBase>()->period();
+            case NestedNumberBase::nestedValuePropertyKey:
+                return object->as<NestedNumberBase>()->nestedValue();
+            case NestedRemapAnimationBase::timePropertyKey:
+                return object->as<NestedRemapAnimationBase>()->time();
+            case BlendAnimation1DBase::valuePropertyKey:
+                return object->as<BlendAnimation1DBase>()->value();
+            case LinearGradientBase::startXPropertyKey:
+                return object->as<LinearGradientBase>()->startX();
+            case LinearGradientBase::startYPropertyKey:
+                return object->as<LinearGradientBase>()->startY();
+            case LinearGradientBase::endXPropertyKey:
+                return object->as<LinearGradientBase>()->endX();
+            case LinearGradientBase::endYPropertyKey:
+                return object->as<LinearGradientBase>()->endY();
+            case LinearGradientBase::opacityPropertyKey:
+                return object->as<LinearGradientBase>()->opacity();
+            case StrokeBase::thicknessPropertyKey:
+                return object->as<StrokeBase>()->thickness();
+            case GradientStopBase::positionPropertyKey:
+                return object->as<GradientStopBase>()->position();
+            case TrimPathBase::startPropertyKey:
+                return object->as<TrimPathBase>()->start();
+            case TrimPathBase::endPropertyKey:
+                return object->as<TrimPathBase>()->end();
+            case TrimPathBase::offsetPropertyKey:
+                return object->as<TrimPathBase>()->offset();
+            case VertexBase::xPropertyKey:
+                return object->as<VertexBase>()->x();
+            case VertexBase::yPropertyKey:
+                return object->as<VertexBase>()->y();
+            case MeshVertexBase::uPropertyKey:
+                return object->as<MeshVertexBase>()->u();
+            case MeshVertexBase::vPropertyKey:
+                return object->as<MeshVertexBase>()->v();
+            case StraightVertexBase::radiusPropertyKey:
+                return object->as<StraightVertexBase>()->radius();
+            case CubicAsymmetricVertexBase::rotationPropertyKey:
+                return object->as<CubicAsymmetricVertexBase>()->rotation();
+            case CubicAsymmetricVertexBase::inDistancePropertyKey:
+                return object->as<CubicAsymmetricVertexBase>()->inDistance();
+            case CubicAsymmetricVertexBase::outDistancePropertyKey:
+                return object->as<CubicAsymmetricVertexBase>()->outDistance();
+            case ParametricPathBase::widthPropertyKey:
+                return object->as<ParametricPathBase>()->width();
+            case ParametricPathBase::heightPropertyKey:
+                return object->as<ParametricPathBase>()->height();
+            case ParametricPathBase::originXPropertyKey:
+                return object->as<ParametricPathBase>()->originX();
+            case ParametricPathBase::originYPropertyKey:
+                return object->as<ParametricPathBase>()->originY();
+            case RectangleBase::cornerRadiusTLPropertyKey:
+                return object->as<RectangleBase>()->cornerRadiusTL();
+            case RectangleBase::cornerRadiusTRPropertyKey:
+                return object->as<RectangleBase>()->cornerRadiusTR();
+            case RectangleBase::cornerRadiusBLPropertyKey:
+                return object->as<RectangleBase>()->cornerRadiusBL();
+            case RectangleBase::cornerRadiusBRPropertyKey:
+                return object->as<RectangleBase>()->cornerRadiusBR();
+            case CubicMirroredVertexBase::rotationPropertyKey:
+                return object->as<CubicMirroredVertexBase>()->rotation();
+            case CubicMirroredVertexBase::distancePropertyKey:
+                return object->as<CubicMirroredVertexBase>()->distance();
+            case PolygonBase::cornerRadiusPropertyKey:
+                return object->as<PolygonBase>()->cornerRadius();
+            case StarBase::innerRadiusPropertyKey:
+                return object->as<StarBase>()->innerRadius();
+            case ImageBase::originXPropertyKey:
+                return object->as<ImageBase>()->originX();
+            case ImageBase::originYPropertyKey:
+                return object->as<ImageBase>()->originY();
+            case CubicDetachedVertexBase::inRotationPropertyKey:
+                return object->as<CubicDetachedVertexBase>()->inRotation();
+            case CubicDetachedVertexBase::inDistancePropertyKey:
+                return object->as<CubicDetachedVertexBase>()->inDistance();
+            case CubicDetachedVertexBase::outRotationPropertyKey:
+                return object->as<CubicDetachedVertexBase>()->outRotation();
+            case CubicDetachedVertexBase::outDistancePropertyKey:
+                return object->as<CubicDetachedVertexBase>()->outDistance();
+            case LayoutComponentBase::widthPropertyKey:
+                return object->as<LayoutComponentBase>()->width();
+            case LayoutComponentBase::heightPropertyKey:
+                return object->as<LayoutComponentBase>()->height();
+            case ArtboardBase::originXPropertyKey:
+                return object->as<ArtboardBase>()->originX();
+            case ArtboardBase::originYPropertyKey:
+                return object->as<ArtboardBase>()->originY();
+            case JoystickBase::xPropertyKey:
+                return object->as<JoystickBase>()->x();
+            case JoystickBase::yPropertyKey:
+                return object->as<JoystickBase>()->y();
+            case JoystickBase::posXPropertyKey:
+                return object->as<JoystickBase>()->posX();
+            case JoystickBase::posYPropertyKey:
+                return object->as<JoystickBase>()->posY();
+            case JoystickBase::originXPropertyKey:
+                return object->as<JoystickBase>()->originX();
+            case JoystickBase::originYPropertyKey:
+                return object->as<JoystickBase>()->originY();
+            case JoystickBase::widthPropertyKey:
+                return object->as<JoystickBase>()->width();
+            case JoystickBase::heightPropertyKey:
+                return object->as<JoystickBase>()->height();
+            case DataConverterOperationBase::valuePropertyKey:
+                return object->as<DataConverterOperationBase>()->value();
+            case BindablePropertyNumberBase::propertyValuePropertyKey:
+                return object->as<BindablePropertyNumberBase>()->propertyValue();
+            case NestedArtboardLeafBase::alignmentXPropertyKey:
+                return object->as<NestedArtboardLeafBase>()->alignmentX();
+            case NestedArtboardLeafBase::alignmentYPropertyKey:
+                return object->as<NestedArtboardLeafBase>()->alignmentY();
+            case BoneBase::lengthPropertyKey:
+                return object->as<BoneBase>()->length();
+            case RootBoneBase::xPropertyKey:
+                return object->as<RootBoneBase>()->x();
+            case RootBoneBase::yPropertyKey:
+                return object->as<RootBoneBase>()->y();
+            case SkinBase::xxPropertyKey:
+                return object->as<SkinBase>()->xx();
+            case SkinBase::yxPropertyKey:
+                return object->as<SkinBase>()->yx();
+            case SkinBase::xyPropertyKey:
+                return object->as<SkinBase>()->xy();
+            case SkinBase::yyPropertyKey:
+                return object->as<SkinBase>()->yy();
+            case SkinBase::txPropertyKey:
+                return object->as<SkinBase>()->tx();
+            case SkinBase::tyPropertyKey:
+                return object->as<SkinBase>()->ty();
+            case TendonBase::xxPropertyKey:
+                return object->as<TendonBase>()->xx();
+            case TendonBase::yxPropertyKey:
+                return object->as<TendonBase>()->yx();
+            case TendonBase::xyPropertyKey:
+                return object->as<TendonBase>()->xy();
+            case TendonBase::yyPropertyKey:
+                return object->as<TendonBase>()->yy();
+            case TendonBase::txPropertyKey:
+                return object->as<TendonBase>()->tx();
+            case TendonBase::tyPropertyKey:
+                return object->as<TendonBase>()->ty();
+            case TextModifierRangeBase::modifyFromPropertyKey:
+                return object->as<TextModifierRangeBase>()->modifyFrom();
+            case TextModifierRangeBase::modifyToPropertyKey:
+                return object->as<TextModifierRangeBase>()->modifyTo();
+            case TextModifierRangeBase::strengthPropertyKey:
+                return object->as<TextModifierRangeBase>()->strength();
+            case TextModifierRangeBase::falloffFromPropertyKey:
+                return object->as<TextModifierRangeBase>()->falloffFrom();
+            case TextModifierRangeBase::falloffToPropertyKey:
+                return object->as<TextModifierRangeBase>()->falloffTo();
+            case TextModifierRangeBase::offsetPropertyKey:
+                return object->as<TextModifierRangeBase>()->offset();
+            case TextVariationModifierBase::axisValuePropertyKey:
+                return object->as<TextVariationModifierBase>()->axisValue();
+            case TextModifierGroupBase::originXPropertyKey:
+                return object->as<TextModifierGroupBase>()->originX();
+            case TextModifierGroupBase::originYPropertyKey:
+                return object->as<TextModifierGroupBase>()->originY();
+            case TextModifierGroupBase::opacityPropertyKey:
+                return object->as<TextModifierGroupBase>()->opacity();
+            case TextModifierGroupBase::xPropertyKey:
+                return object->as<TextModifierGroupBase>()->x();
+            case TextModifierGroupBase::yPropertyKey:
+                return object->as<TextModifierGroupBase>()->y();
+            case TextModifierGroupBase::rotationPropertyKey:
+                return object->as<TextModifierGroupBase>()->rotation();
+            case TextModifierGroupBase::scaleXPropertyKey:
+                return object->as<TextModifierGroupBase>()->scaleX();
+            case TextModifierGroupBase::scaleYPropertyKey:
+                return object->as<TextModifierGroupBase>()->scaleY();
+            case TextStyleBase::fontSizePropertyKey:
+                return object->as<TextStyleBase>()->fontSize();
+            case TextStyleBase::lineHeightPropertyKey:
+                return object->as<TextStyleBase>()->lineHeight();
+            case TextStyleBase::letterSpacingPropertyKey:
+                return object->as<TextStyleBase>()->letterSpacing();
+            case TextStyleAxisBase::axisValuePropertyKey:
+                return object->as<TextStyleAxisBase>()->axisValue();
+            case TextBase::widthPropertyKey:
+                return object->as<TextBase>()->width();
+            case TextBase::heightPropertyKey:
+                return object->as<TextBase>()->height();
+            case TextBase::originXPropertyKey:
+                return object->as<TextBase>()->originX();
+            case TextBase::originYPropertyKey:
+                return object->as<TextBase>()->originY();
+            case TextBase::paragraphSpacingPropertyKey:
+                return object->as<TextBase>()->paragraphSpacing();
+            case DrawableAssetBase::heightPropertyKey:
+                return object->as<DrawableAssetBase>()->height();
+            case DrawableAssetBase::widthPropertyKey:
+                return object->as<DrawableAssetBase>()->width();
+            case ExportAudioBase::volumePropertyKey:
+                return object->as<ExportAudioBase>()->volume();
+        }
+        return 0.0f;
+    }
+    static int propertyFieldId(int propertyKey)
+    {
+        switch (propertyKey)
+        {
+            case ViewModelInstanceListItemBase::useLinkedArtboardPropertyKey:
+            case ViewModelInstanceBooleanBase::propertyValuePropertyKey:
+            case TransformComponentConstraintBase::offsetPropertyKey:
+            case TransformComponentConstraintBase::doesCopyPropertyKey:
+            case TransformComponentConstraintBase::minPropertyKey:
+            case TransformComponentConstraintBase::maxPropertyKey:
+            case TransformComponentConstraintYBase::doesCopyYPropertyKey:
+            case TransformComponentConstraintYBase::minYPropertyKey:
+            case TransformComponentConstraintYBase::maxYPropertyKey:
+            case IKConstraintBase::invertDirectionPropertyKey:
+            case FollowPathConstraintBase::orientPropertyKey:
+            case FollowPathConstraintBase::offsetPropertyKey:
+            case AxisBase::normalizedPropertyKey:
+            case LayoutComponentStyleBase::intrinsicallySizedValuePropertyKey:
+            case LayoutComponentStyleBase::linkCornerRadiusPropertyKey:
+            case NestedSimpleAnimationBase::isPlayingPropertyKey:
+            case KeyFrameBoolBase::valuePropertyKey:
+            case ListenerAlignTargetBase::preserveOffsetPropertyKey:
+            case TransitionValueBooleanComparatorBase::valuePropertyKey:
+            case NestedBoolBase::nestedValuePropertyKey:
+            case LinearAnimationBase::enableWorkAreaPropertyKey:
+            case LinearAnimationBase::quantizePropertyKey:
+            case StateMachineBoolBase::valuePropertyKey:
+            case ShapePaintBase::isVisiblePropertyKey:
+            case StrokeBase::transformAffectsStrokePropertyKey:
+            case PointsPathBase::isClosedPropertyKey:
+            case RectangleBase::linkCornerRadiusPropertyKey:
+            case ClippingShapeBase::isVisiblePropertyKey:
+            case CustomPropertyBooleanBase::propertyValuePropertyKey:
+            case LayoutComponentBase::clipPropertyKey:
+            case BindablePropertyBooleanBase::propertyValuePropertyKey:
+            case TextModifierRangeBase::clampPropertyKey:
+                return CoreBoolType::id;
+            case ViewModelInstanceListItemBase::viewModelIdPropertyKey:
+            case ViewModelInstanceListItemBase::viewModelInstanceIdPropertyKey:
+            case ViewModelInstanceListItemBase::artboardIdPropertyKey:
+            case ViewModelInstanceValueBase::viewModelPropertyIdPropertyKey:
+            case ViewModelInstanceEnumBase::propertyValuePropertyKey:
+            case ViewModelBase::defaultInstanceIdPropertyKey:
+            case ViewModelPropertyViewModelBase::viewModelReferenceIdPropertyKey:
+            case ComponentBase::parentIdPropertyKey:
+            case ViewModelInstanceBase::viewModelIdPropertyKey:
+            case ViewModelPropertyEnumBase::enumIdPropertyKey:
+            case ViewModelInstanceViewModelBase::propertyValuePropertyKey:
+            case DrawTargetBase::drawableIdPropertyKey:
+            case DrawTargetBase::placementValuePropertyKey:
+            case TargetedConstraintBase::targetIdPropertyKey:
+            case DistanceConstraintBase::modeValuePropertyKey:
+            case TransformSpaceConstraintBase::sourceSpaceValuePropertyKey:
+            case TransformSpaceConstraintBase::destSpaceValuePropertyKey:
+            case TransformComponentConstraintBase::minMaxSpaceValuePropertyKey:
+            case IKConstraintBase::parentBoneCountPropertyKey:
+            case DrawableBase::blendModeValuePropertyKey:
+            case DrawableBase::drawableFlagsPropertyKey:
+            case NestedArtboardBase::artboardIdPropertyKey:
+            case NestedAnimationBase::animationIdPropertyKey:
+            case SoloBase::activeComponentIdPropertyKey:
+            case NestedArtboardLayoutBase::instanceWidthUnitsValuePropertyKey:
+            case NestedArtboardLayoutBase::instanceHeightUnitsValuePropertyKey:
+            case NestedArtboardLayoutBase::instanceWidthScaleTypePropertyKey:
+            case NestedArtboardLayoutBase::instanceHeightScaleTypePropertyKey:
+            case NSlicerTileModeBase::patchIndexPropertyKey:
+            case NSlicerTileModeBase::stylePropertyKey:
+            case LayoutComponentStyleBase::layoutWidthScaleTypePropertyKey:
+            case LayoutComponentStyleBase::layoutHeightScaleTypePropertyKey:
+            case LayoutComponentStyleBase::layoutAlignmentTypePropertyKey:
+            case LayoutComponentStyleBase::animationStyleTypePropertyKey:
+            case LayoutComponentStyleBase::interpolationTypePropertyKey:
+            case LayoutComponentStyleBase::interpolatorIdPropertyKey:
+            case LayoutComponentStyleBase::displayValuePropertyKey:
+            case LayoutComponentStyleBase::positionTypeValuePropertyKey:
+            case LayoutComponentStyleBase::flexDirectionValuePropertyKey:
+            case LayoutComponentStyleBase::directionValuePropertyKey:
+            case LayoutComponentStyleBase::alignContentValuePropertyKey:
+            case LayoutComponentStyleBase::alignItemsValuePropertyKey:
+            case LayoutComponentStyleBase::alignSelfValuePropertyKey:
+            case LayoutComponentStyleBase::justifyContentValuePropertyKey:
+            case LayoutComponentStyleBase::flexWrapValuePropertyKey:
+            case LayoutComponentStyleBase::overflowValuePropertyKey:
+            case LayoutComponentStyleBase::widthUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::heightUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::borderLeftUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::borderRightUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::borderTopUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::borderBottomUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::marginLeftUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::marginRightUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::marginTopUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::marginBottomUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::paddingLeftUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::paddingRightUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::paddingTopUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::paddingBottomUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::positionLeftUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::positionRightUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::positionTopUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::positionBottomUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::gapHorizontalUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::gapVerticalUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::minWidthUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::minHeightUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::maxWidthUnitsValuePropertyKey:
+            case LayoutComponentStyleBase::maxHeightUnitsValuePropertyKey:
+            case ListenerFireEventBase::eventIdPropertyKey:
+            case LayerStateBase::flagsPropertyKey:
+            case KeyFrameBase::framePropertyKey:
+            case InterpolatingKeyFrameBase::interpolationTypePropertyKey:
+            case InterpolatingKeyFrameBase::interpolatorIdPropertyKey:
+            case KeyFrameUintBase::valuePropertyKey:
+            case ListenerInputChangeBase::inputIdPropertyKey:
+            case ListenerInputChangeBase::nestedInputIdPropertyKey:
+            case AnimationStateBase::animationIdPropertyKey:
+            case NestedInputBase::inputIdPropertyKey:
+            case KeyedObjectBase::objectIdPropertyKey:
+            case BlendAnimationBase::animationIdPropertyKey:
+            case BlendAnimationDirectBase::inputIdPropertyKey:
+            case BlendAnimationDirectBase::blendSourcePropertyKey:
+            case TransitionInputConditionBase::inputIdPropertyKey:
+            case KeyedPropertyBase::propertyKeyPropertyKey:
+            case StateMachineListenerBase::targetIdPropertyKey:
+            case StateMachineListenerBase::listenerTypeValuePropertyKey:
+            case StateMachineListenerBase::eventIdPropertyKey:
+            case TransitionPropertyArtboardComparatorBase::propertyTypePropertyKey:
+            case KeyFrameIdBase::valuePropertyKey:
+            case ListenerBoolChangeBase::valuePropertyKey:
+            case ListenerAlignTargetBase::targetIdPropertyKey:
+            case TransitionValueConditionBase::opValuePropertyKey:
+            case TransitionViewModelConditionBase::leftComparatorIdPropertyKey:
+            case TransitionViewModelConditionBase::rightComparatorIdPropertyKey:
+            case TransitionViewModelConditionBase::opValuePropertyKey:
+            case StateTransitionBase::stateToIdPropertyKey:
+            case StateTransitionBase::flagsPropertyKey:
+            case StateTransitionBase::durationPropertyKey:
+            case StateTransitionBase::exitTimePropertyKey:
+            case StateTransitionBase::interpolationTypePropertyKey:
+            case StateTransitionBase::interpolatorIdPropertyKey:
+            case StateTransitionBase::randomWeightPropertyKey:
+            case StateMachineFireEventBase::eventIdPropertyKey:
+            case StateMachineFireEventBase::occursValuePropertyKey:
+            case LinearAnimationBase::fpsPropertyKey:
+            case LinearAnimationBase::durationPropertyKey:
+            case LinearAnimationBase::loopValuePropertyKey:
+            case LinearAnimationBase::workStartPropertyKey:
+            case LinearAnimationBase::workEndPropertyKey:
+            case ElasticInterpolatorBase::easingValuePropertyKey:
+            case BlendState1DBase::inputIdPropertyKey:
+            case TransitionValueEnumComparatorBase::valuePropertyKey:
+            case BlendStateTransitionBase::exitBlendAnimationIdPropertyKey:
+            case StrokeBase::capPropertyKey:
+            case StrokeBase::joinPropertyKey:
+            case TrimPathBase::modeValuePropertyKey:
+            case FillBase::fillRulePropertyKey:
+            case PathBase::pathFlagsPropertyKey:
+            case ClippingShapeBase::sourceIdPropertyKey:
+            case ClippingShapeBase::fillRulePropertyKey:
+            case PolygonBase::pointsPropertyKey:
+            case ImageBase::assetIdPropertyKey:
+            case DrawRulesBase::drawTargetIdPropertyKey:
+            case LayoutComponentBase::styleIdPropertyKey:
+            case ArtboardBase::defaultStateMachineIdPropertyKey:
+            case ArtboardBase::viewModelIdPropertyKey:
+            case JoystickBase::xIdPropertyKey:
+            case JoystickBase::yIdPropertyKey:
+            case JoystickBase::joystickFlagsPropertyKey:
+            case JoystickBase::handleSourceIdPropertyKey:
+            case OpenUrlEventBase::targetValuePropertyKey:
+            case DataBindBase::propertyKeyPropertyKey:
+            case DataBindBase::flagsPropertyKey:
+            case DataBindBase::converterIdPropertyKey:
+            case DataConverterGroupItemBase::converterIdPropertyKey:
+            case DataConverterRounderBase::decimalsPropertyKey:
+            case DataConverterOperationBase::operationTypePropertyKey:
+            case BindablePropertyEnumBase::propertyValuePropertyKey:
+            case NestedArtboardLeafBase::fitPropertyKey:
+            case WeightBase::valuesPropertyKey:
+            case WeightBase::indicesPropertyKey:
+            case TendonBase::boneIdPropertyKey:
+            case CubicWeightBase::inValuesPropertyKey:
+            case CubicWeightBase::inIndicesPropertyKey:
+            case CubicWeightBase::outValuesPropertyKey:
+            case CubicWeightBase::outIndicesPropertyKey:
+            case TextModifierRangeBase::unitsValuePropertyKey:
+            case TextModifierRangeBase::typeValuePropertyKey:
+            case TextModifierRangeBase::modeValuePropertyKey:
+            case TextModifierRangeBase::runIdPropertyKey:
+            case TextStyleFeatureBase::tagPropertyKey:
+            case TextStyleFeatureBase::featureValuePropertyKey:
+            case TextVariationModifierBase::axisTagPropertyKey:
+            case TextModifierGroupBase::modifierFlagsPropertyKey:
+            case TextStyleBase::fontAssetIdPropertyKey:
+            case TextStyleAxisBase::tagPropertyKey:
+            case TextBase::alignValuePropertyKey:
+            case TextBase::sizingValuePropertyKey:
+            case TextBase::overflowValuePropertyKey:
+            case TextBase::originValuePropertyKey:
+            case TextValueRunBase::styleIdPropertyKey:
+            case FileAssetBase::assetIdPropertyKey:
+            case AudioEventBase::assetIdPropertyKey:
+                return CoreUintType::id;
+            case ViewModelInstanceColorBase::propertyValuePropertyKey:
+            case KeyFrameColorBase::valuePropertyKey:
+            case TransitionValueColorComparatorBase::valuePropertyKey:
+            case SolidColorBase::colorValuePropertyKey:
+            case GradientStopBase::colorValuePropertyKey:
+            case BindablePropertyColorBase::propertyValuePropertyKey:
+                return CoreColorType::id;
+            case ViewModelComponentBase::namePropertyKey:
+            case ViewModelInstanceStringBase::propertyValuePropertyKey:
+            case ComponentBase::namePropertyKey:
+            case DataEnumValueBase::keyPropertyKey:
+            case DataEnumValueBase::valuePropertyKey:
+            case AnimationBase::namePropertyKey:
+            case StateMachineComponentBase::namePropertyKey:
+            case KeyFrameStringBase::valuePropertyKey:
+            case TransitionValueStringComparatorBase::valuePropertyKey:
+            case OpenUrlEventBase::urlPropertyKey:
+            case DataConverterBase::namePropertyKey:
+            case BindablePropertyStringBase::propertyValuePropertyKey:
+            case TextValueRunBase::textPropertyKey:
+            case CustomPropertyStringBase::propertyValuePropertyKey:
+            case AssetBase::namePropertyKey:
+            case FileAssetBase::cdnBaseUrlPropertyKey:
+                return CoreStringType::id;
+            case ViewModelInstanceNumberBase::propertyValuePropertyKey:
+            case CustomPropertyNumberBase::propertyValuePropertyKey:
+            case ConstraintBase::strengthPropertyKey:
+            case DistanceConstraintBase::distancePropertyKey:
+            case TransformComponentConstraintBase::copyFactorPropertyKey:
+            case TransformComponentConstraintBase::minValuePropertyKey:
+            case TransformComponentConstraintBase::maxValuePropertyKey:
+            case TransformComponentConstraintYBase::copyFactorYPropertyKey:
+            case TransformComponentConstraintYBase::minValueYPropertyKey:
+            case TransformComponentConstraintYBase::maxValueYPropertyKey:
+            case FollowPathConstraintBase::distancePropertyKey:
+            case TransformConstraintBase::originXPropertyKey:
+            case TransformConstraintBase::originYPropertyKey:
+            case WorldTransformComponentBase::opacityPropertyKey:
+            case TransformComponentBase::rotationPropertyKey:
+            case TransformComponentBase::scaleXPropertyKey:
+            case TransformComponentBase::scaleYPropertyKey:
+            case NodeBase::xPropertyKey:
+            case NodeBase::xArtboardPropertyKey:
+            case NodeBase::yPropertyKey:
+            case NodeBase::yArtboardPropertyKey:
+            case NestedArtboardLayoutBase::instanceWidthPropertyKey:
+            case NestedArtboardLayoutBase::instanceHeightPropertyKey:
+            case AxisBase::offsetPropertyKey:
+            case LayoutComponentStyleBase::gapHorizontalPropertyKey:
+            case LayoutComponentStyleBase::gapVerticalPropertyKey:
+            case LayoutComponentStyleBase::maxWidthPropertyKey:
+            case LayoutComponentStyleBase::maxHeightPropertyKey:
+            case LayoutComponentStyleBase::minWidthPropertyKey:
+            case LayoutComponentStyleBase::minHeightPropertyKey:
+            case LayoutComponentStyleBase::borderLeftPropertyKey:
+            case LayoutComponentStyleBase::borderRightPropertyKey:
+            case LayoutComponentStyleBase::borderTopPropertyKey:
+            case LayoutComponentStyleBase::borderBottomPropertyKey:
+            case LayoutComponentStyleBase::marginLeftPropertyKey:
+            case LayoutComponentStyleBase::marginRightPropertyKey:
+            case LayoutComponentStyleBase::marginTopPropertyKey:
+            case LayoutComponentStyleBase::marginBottomPropertyKey:
+            case LayoutComponentStyleBase::paddingLeftPropertyKey:
+            case LayoutComponentStyleBase::paddingRightPropertyKey:
+            case LayoutComponentStyleBase::paddingTopPropertyKey:
+            case LayoutComponentStyleBase::paddingBottomPropertyKey:
+            case LayoutComponentStyleBase::positionLeftPropertyKey:
+            case LayoutComponentStyleBase::positionRightPropertyKey:
+            case LayoutComponentStyleBase::positionTopPropertyKey:
+            case LayoutComponentStyleBase::positionBottomPropertyKey:
+            case LayoutComponentStyleBase::flexPropertyKey:
+            case LayoutComponentStyleBase::flexGrowPropertyKey:
+            case LayoutComponentStyleBase::flexShrinkPropertyKey:
+            case LayoutComponentStyleBase::flexBasisPropertyKey:
+            case LayoutComponentStyleBase::aspectRatioPropertyKey:
+            case LayoutComponentStyleBase::interpolationTimePropertyKey:
+            case LayoutComponentStyleBase::cornerRadiusTLPropertyKey:
+            case LayoutComponentStyleBase::cornerRadiusTRPropertyKey:
+            case LayoutComponentStyleBase::cornerRadiusBLPropertyKey:
+            case LayoutComponentStyleBase::cornerRadiusBRPropertyKey:
+            case NestedLinearAnimationBase::mixPropertyKey:
+            case NestedSimpleAnimationBase::speedPropertyKey:
+            case AdvanceableStateBase::speedPropertyKey:
+            case BlendAnimationDirectBase::mixValuePropertyKey:
+            case StateMachineNumberBase::valuePropertyKey:
+            case CubicInterpolatorBase::x1PropertyKey:
+            case CubicInterpolatorBase::y1PropertyKey:
+            case CubicInterpolatorBase::x2PropertyKey:
+            case CubicInterpolatorBase::y2PropertyKey:
+            case TransitionNumberConditionBase::valuePropertyKey:
+            case CubicInterpolatorComponentBase::x1PropertyKey:
+            case CubicInterpolatorComponentBase::y1PropertyKey:
+            case CubicInterpolatorComponentBase::x2PropertyKey:
+            case CubicInterpolatorComponentBase::y2PropertyKey:
+            case ListenerNumberChangeBase::valuePropertyKey:
+            case KeyFrameDoubleBase::valuePropertyKey:
+            case LinearAnimationBase::speedPropertyKey:
+            case TransitionValueNumberComparatorBase::valuePropertyKey:
+            case ElasticInterpolatorBase::amplitudePropertyKey:
+            case ElasticInterpolatorBase::periodPropertyKey:
+            case NestedNumberBase::nestedValuePropertyKey:
+            case NestedRemapAnimationBase::timePropertyKey:
+            case BlendAnimation1DBase::valuePropertyKey:
+            case LinearGradientBase::startXPropertyKey:
+            case LinearGradientBase::startYPropertyKey:
+            case LinearGradientBase::endXPropertyKey:
+            case LinearGradientBase::endYPropertyKey:
+            case LinearGradientBase::opacityPropertyKey:
+            case StrokeBase::thicknessPropertyKey:
+            case GradientStopBase::positionPropertyKey:
+            case TrimPathBase::startPropertyKey:
+            case TrimPathBase::endPropertyKey:
+            case TrimPathBase::offsetPropertyKey:
+            case VertexBase::xPropertyKey:
+            case VertexBase::yPropertyKey:
+            case MeshVertexBase::uPropertyKey:
+            case MeshVertexBase::vPropertyKey:
+            case StraightVertexBase::radiusPropertyKey:
+            case CubicAsymmetricVertexBase::rotationPropertyKey:
+            case CubicAsymmetricVertexBase::inDistancePropertyKey:
+            case CubicAsymmetricVertexBase::outDistancePropertyKey:
+            case ParametricPathBase::widthPropertyKey:
+            case ParametricPathBase::heightPropertyKey:
+            case ParametricPathBase::originXPropertyKey:
+            case ParametricPathBase::originYPropertyKey:
+            case RectangleBase::cornerRadiusTLPropertyKey:
+            case RectangleBase::cornerRadiusTRPropertyKey:
+            case RectangleBase::cornerRadiusBLPropertyKey:
+            case RectangleBase::cornerRadiusBRPropertyKey:
+            case CubicMirroredVertexBase::rotationPropertyKey:
+            case CubicMirroredVertexBase::distancePropertyKey:
+            case PolygonBase::cornerRadiusPropertyKey:
+            case StarBase::innerRadiusPropertyKey:
+            case ImageBase::originXPropertyKey:
+            case ImageBase::originYPropertyKey:
+            case CubicDetachedVertexBase::inRotationPropertyKey:
+            case CubicDetachedVertexBase::inDistancePropertyKey:
+            case CubicDetachedVertexBase::outRotationPropertyKey:
+            case CubicDetachedVertexBase::outDistancePropertyKey:
+            case LayoutComponentBase::widthPropertyKey:
+            case LayoutComponentBase::heightPropertyKey:
+            case ArtboardBase::originXPropertyKey:
+            case ArtboardBase::originYPropertyKey:
+            case JoystickBase::xPropertyKey:
+            case JoystickBase::yPropertyKey:
+            case JoystickBase::posXPropertyKey:
+            case JoystickBase::posYPropertyKey:
+            case JoystickBase::originXPropertyKey:
+            case JoystickBase::originYPropertyKey:
+            case JoystickBase::widthPropertyKey:
+            case JoystickBase::heightPropertyKey:
+            case DataConverterOperationBase::valuePropertyKey:
+            case BindablePropertyNumberBase::propertyValuePropertyKey:
+            case NestedArtboardLeafBase::alignmentXPropertyKey:
+            case NestedArtboardLeafBase::alignmentYPropertyKey:
+            case BoneBase::lengthPropertyKey:
+            case RootBoneBase::xPropertyKey:
+            case RootBoneBase::yPropertyKey:
+            case SkinBase::xxPropertyKey:
+            case SkinBase::yxPropertyKey:
+            case SkinBase::xyPropertyKey:
+            case SkinBase::yyPropertyKey:
+            case SkinBase::txPropertyKey:
+            case SkinBase::tyPropertyKey:
+            case TendonBase::xxPropertyKey:
+            case TendonBase::yxPropertyKey:
+            case TendonBase::xyPropertyKey:
+            case TendonBase::yyPropertyKey:
+            case TendonBase::txPropertyKey:
+            case TendonBase::tyPropertyKey:
+            case TextModifierRangeBase::modifyFromPropertyKey:
+            case TextModifierRangeBase::modifyToPropertyKey:
+            case TextModifierRangeBase::strengthPropertyKey:
+            case TextModifierRangeBase::falloffFromPropertyKey:
+            case TextModifierRangeBase::falloffToPropertyKey:
+            case TextModifierRangeBase::offsetPropertyKey:
+            case TextVariationModifierBase::axisValuePropertyKey:
+            case TextModifierGroupBase::originXPropertyKey:
+            case TextModifierGroupBase::originYPropertyKey:
+            case TextModifierGroupBase::opacityPropertyKey:
+            case TextModifierGroupBase::xPropertyKey:
+            case TextModifierGroupBase::yPropertyKey:
+            case TextModifierGroupBase::rotationPropertyKey:
+            case TextModifierGroupBase::scaleXPropertyKey:
+            case TextModifierGroupBase::scaleYPropertyKey:
+            case TextStyleBase::fontSizePropertyKey:
+            case TextStyleBase::lineHeightPropertyKey:
+            case TextStyleBase::letterSpacingPropertyKey:
+            case TextStyleAxisBase::axisValuePropertyKey:
+            case TextBase::widthPropertyKey:
+            case TextBase::heightPropertyKey:
+            case TextBase::originXPropertyKey:
+            case TextBase::originYPropertyKey:
+            case TextBase::paragraphSpacingPropertyKey:
+            case DrawableAssetBase::heightPropertyKey:
+            case DrawableAssetBase::widthPropertyKey:
+            case ExportAudioBase::volumePropertyKey:
+                return CoreDoubleType::id;
+            case NestedArtboardBase::dataBindPathIdsPropertyKey:
+            case MeshBase::triangleIndexBytesPropertyKey:
+            case DataBindContextBase::sourcePathIdsPropertyKey:
+            case FileAssetBase::cdnUuidPropertyKey:
+            case FileAssetContentsBase::bytesPropertyKey:
+                return CoreBytesType::id;
+            default:
+                return -1;
+        }
+    }
+    static bool isCallback(uint32_t propertyKey)
+    {
+        switch (propertyKey)
+        {
+            case NestedTriggerBase::firePropertyKey:
+            case EventBase::triggerPropertyKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+    static bool objectSupportsProperty(Core* object, uint32_t propertyKey)
+    {
+        switch (propertyKey)
+        {
+            case ViewModelInstanceListItemBase::useLinkedArtboardPropertyKey:
+                return object->is<ViewModelInstanceListItemBase>();
+            case ViewModelInstanceBooleanBase::propertyValuePropertyKey:
+                return object->is<ViewModelInstanceBooleanBase>();
+            case TransformComponentConstraintBase::offsetPropertyKey:
+                return object->is<TransformComponentConstraintBase>();
+            case TransformComponentConstraintBase::doesCopyPropertyKey:
+                return object->is<TransformComponentConstraintBase>();
+            case TransformComponentConstraintBase::minPropertyKey:
+                return object->is<TransformComponentConstraintBase>();
+            case TransformComponentConstraintBase::maxPropertyKey:
+                return object->is<TransformComponentConstraintBase>();
+            case TransformComponentConstraintYBase::doesCopyYPropertyKey:
+                return object->is<TransformComponentConstraintYBase>();
+            case TransformComponentConstraintYBase::minYPropertyKey:
+                return object->is<TransformComponentConstraintYBase>();
+            case TransformComponentConstraintYBase::maxYPropertyKey:
+                return object->is<TransformComponentConstraintYBase>();
+            case IKConstraintBase::invertDirectionPropertyKey:
+                return object->is<IKConstraintBase>();
+            case FollowPathConstraintBase::orientPropertyKey:
+                return object->is<FollowPathConstraintBase>();
+            case FollowPathConstraintBase::offsetPropertyKey:
+                return object->is<FollowPathConstraintBase>();
+            case AxisBase::normalizedPropertyKey:
+                return object->is<AxisBase>();
+            case LayoutComponentStyleBase::intrinsicallySizedValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::linkCornerRadiusPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case NestedSimpleAnimationBase::isPlayingPropertyKey:
+                return object->is<NestedSimpleAnimationBase>();
+            case KeyFrameBoolBase::valuePropertyKey:
+                return object->is<KeyFrameBoolBase>();
+            case ListenerAlignTargetBase::preserveOffsetPropertyKey:
+                return object->is<ListenerAlignTargetBase>();
+            case TransitionValueBooleanComparatorBase::valuePropertyKey:
+                return object->is<TransitionValueBooleanComparatorBase>();
+            case NestedBoolBase::nestedValuePropertyKey:
+                return object->is<NestedBoolBase>();
+            case LinearAnimationBase::enableWorkAreaPropertyKey:
+                return object->is<LinearAnimationBase>();
+            case LinearAnimationBase::quantizePropertyKey:
+                return object->is<LinearAnimationBase>();
+            case StateMachineBoolBase::valuePropertyKey:
+                return object->is<StateMachineBoolBase>();
+            case ShapePaintBase::isVisiblePropertyKey:
+                return object->is<ShapePaintBase>();
+            case StrokeBase::transformAffectsStrokePropertyKey:
+                return object->is<StrokeBase>();
+            case PointsPathBase::isClosedPropertyKey:
+                return object->is<PointsPathBase>();
+            case RectangleBase::linkCornerRadiusPropertyKey:
+                return object->is<RectangleBase>();
+            case ClippingShapeBase::isVisiblePropertyKey:
+                return object->is<ClippingShapeBase>();
+            case CustomPropertyBooleanBase::propertyValuePropertyKey:
+                return object->is<CustomPropertyBooleanBase>();
+            case LayoutComponentBase::clipPropertyKey:
+                return object->is<LayoutComponentBase>();
+            case BindablePropertyBooleanBase::propertyValuePropertyKey:
+                return object->is<BindablePropertyBooleanBase>();
+            case TextModifierRangeBase::clampPropertyKey:
+                return object->is<TextModifierRangeBase>();
+            case ViewModelInstanceListItemBase::viewModelIdPropertyKey:
+                return object->is<ViewModelInstanceListItemBase>();
+            case ViewModelInstanceListItemBase::viewModelInstanceIdPropertyKey:
+                return object->is<ViewModelInstanceListItemBase>();
+            case ViewModelInstanceListItemBase::artboardIdPropertyKey:
+                return object->is<ViewModelInstanceListItemBase>();
+            case ViewModelInstanceValueBase::viewModelPropertyIdPropertyKey:
+                return object->is<ViewModelInstanceValueBase>();
+            case ViewModelInstanceEnumBase::propertyValuePropertyKey:
+                return object->is<ViewModelInstanceEnumBase>();
+            case ViewModelBase::defaultInstanceIdPropertyKey:
+                return object->is<ViewModelBase>();
+            case ViewModelPropertyViewModelBase::viewModelReferenceIdPropertyKey:
+                return object->is<ViewModelPropertyViewModelBase>();
+            case ComponentBase::parentIdPropertyKey:
+                return object->is<ComponentBase>();
+            case ViewModelInstanceBase::viewModelIdPropertyKey:
+                return object->is<ViewModelInstanceBase>();
+            case ViewModelPropertyEnumBase::enumIdPropertyKey:
+                return object->is<ViewModelPropertyEnumBase>();
+            case ViewModelInstanceViewModelBase::propertyValuePropertyKey:
+                return object->is<ViewModelInstanceViewModelBase>();
+            case DrawTargetBase::drawableIdPropertyKey:
+                return object->is<DrawTargetBase>();
+            case DrawTargetBase::placementValuePropertyKey:
+                return object->is<DrawTargetBase>();
+            case TargetedConstraintBase::targetIdPropertyKey:
+                return object->is<TargetedConstraintBase>();
+            case DistanceConstraintBase::modeValuePropertyKey:
+                return object->is<DistanceConstraintBase>();
+            case TransformSpaceConstraintBase::sourceSpaceValuePropertyKey:
+                return object->is<TransformSpaceConstraintBase>();
+            case TransformSpaceConstraintBase::destSpaceValuePropertyKey:
+                return object->is<TransformSpaceConstraintBase>();
+            case TransformComponentConstraintBase::minMaxSpaceValuePropertyKey:
+                return object->is<TransformComponentConstraintBase>();
+            case IKConstraintBase::parentBoneCountPropertyKey:
+                return object->is<IKConstraintBase>();
+            case DrawableBase::blendModeValuePropertyKey:
+                return object->is<DrawableBase>();
+            case DrawableBase::drawableFlagsPropertyKey:
+                return object->is<DrawableBase>();
+            case NestedArtboardBase::artboardIdPropertyKey:
+                return object->is<NestedArtboardBase>();
+            case NestedAnimationBase::animationIdPropertyKey:
+                return object->is<NestedAnimationBase>();
+            case SoloBase::activeComponentIdPropertyKey:
+                return object->is<SoloBase>();
+            case NestedArtboardLayoutBase::instanceWidthUnitsValuePropertyKey:
+                return object->is<NestedArtboardLayoutBase>();
+            case NestedArtboardLayoutBase::instanceHeightUnitsValuePropertyKey:
+                return object->is<NestedArtboardLayoutBase>();
+            case NestedArtboardLayoutBase::instanceWidthScaleTypePropertyKey:
+                return object->is<NestedArtboardLayoutBase>();
+            case NestedArtboardLayoutBase::instanceHeightScaleTypePropertyKey:
+                return object->is<NestedArtboardLayoutBase>();
+            case NSlicerTileModeBase::patchIndexPropertyKey:
+                return object->is<NSlicerTileModeBase>();
+            case NSlicerTileModeBase::stylePropertyKey:
+                return object->is<NSlicerTileModeBase>();
+            case LayoutComponentStyleBase::layoutWidthScaleTypePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::layoutHeightScaleTypePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::layoutAlignmentTypePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::animationStyleTypePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::interpolationTypePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::interpolatorIdPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::displayValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::positionTypeValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::flexDirectionValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::directionValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::alignContentValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::alignItemsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::alignSelfValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::justifyContentValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::flexWrapValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::overflowValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::widthUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::heightUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::borderLeftUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::borderRightUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::borderTopUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::borderBottomUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::marginLeftUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::marginRightUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::marginTopUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::marginBottomUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::paddingLeftUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::paddingRightUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::paddingTopUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::paddingBottomUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::positionLeftUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::positionRightUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::positionTopUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::positionBottomUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::gapHorizontalUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::gapVerticalUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::minWidthUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::minHeightUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::maxWidthUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::maxHeightUnitsValuePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case ListenerFireEventBase::eventIdPropertyKey:
+                return object->is<ListenerFireEventBase>();
+            case LayerStateBase::flagsPropertyKey:
+                return object->is<LayerStateBase>();
+            case KeyFrameBase::framePropertyKey:
+                return object->is<KeyFrameBase>();
+            case InterpolatingKeyFrameBase::interpolationTypePropertyKey:
+                return object->is<InterpolatingKeyFrameBase>();
+            case InterpolatingKeyFrameBase::interpolatorIdPropertyKey:
+                return object->is<InterpolatingKeyFrameBase>();
+            case KeyFrameUintBase::valuePropertyKey:
+                return object->is<KeyFrameUintBase>();
+            case ListenerInputChangeBase::inputIdPropertyKey:
+                return object->is<ListenerInputChangeBase>();
+            case ListenerInputChangeBase::nestedInputIdPropertyKey:
+                return object->is<ListenerInputChangeBase>();
+            case AnimationStateBase::animationIdPropertyKey:
+                return object->is<AnimationStateBase>();
+            case NestedInputBase::inputIdPropertyKey:
+                return object->is<NestedInputBase>();
+            case KeyedObjectBase::objectIdPropertyKey:
+                return object->is<KeyedObjectBase>();
+            case BlendAnimationBase::animationIdPropertyKey:
+                return object->is<BlendAnimationBase>();
+            case BlendAnimationDirectBase::inputIdPropertyKey:
+                return object->is<BlendAnimationDirectBase>();
+            case BlendAnimationDirectBase::blendSourcePropertyKey:
+                return object->is<BlendAnimationDirectBase>();
+            case TransitionInputConditionBase::inputIdPropertyKey:
+                return object->is<TransitionInputConditionBase>();
+            case KeyedPropertyBase::propertyKeyPropertyKey:
+                return object->is<KeyedPropertyBase>();
+            case StateMachineListenerBase::targetIdPropertyKey:
+                return object->is<StateMachineListenerBase>();
+            case StateMachineListenerBase::listenerTypeValuePropertyKey:
+                return object->is<StateMachineListenerBase>();
+            case StateMachineListenerBase::eventIdPropertyKey:
+                return object->is<StateMachineListenerBase>();
+            case TransitionPropertyArtboardComparatorBase::propertyTypePropertyKey:
+                return object->is<TransitionPropertyArtboardComparatorBase>();
+            case KeyFrameIdBase::valuePropertyKey:
+                return object->is<KeyFrameIdBase>();
+            case ListenerBoolChangeBase::valuePropertyKey:
+                return object->is<ListenerBoolChangeBase>();
+            case ListenerAlignTargetBase::targetIdPropertyKey:
+                return object->is<ListenerAlignTargetBase>();
+            case TransitionValueConditionBase::opValuePropertyKey:
+                return object->is<TransitionValueConditionBase>();
+            case TransitionViewModelConditionBase::leftComparatorIdPropertyKey:
+                return object->is<TransitionViewModelConditionBase>();
+            case TransitionViewModelConditionBase::rightComparatorIdPropertyKey:
+                return object->is<TransitionViewModelConditionBase>();
+            case TransitionViewModelConditionBase::opValuePropertyKey:
+                return object->is<TransitionViewModelConditionBase>();
+            case StateTransitionBase::stateToIdPropertyKey:
+                return object->is<StateTransitionBase>();
+            case StateTransitionBase::flagsPropertyKey:
+                return object->is<StateTransitionBase>();
+            case StateTransitionBase::durationPropertyKey:
+                return object->is<StateTransitionBase>();
+            case StateTransitionBase::exitTimePropertyKey:
+                return object->is<StateTransitionBase>();
+            case StateTransitionBase::interpolationTypePropertyKey:
+                return object->is<StateTransitionBase>();
+            case StateTransitionBase::interpolatorIdPropertyKey:
+                return object->is<StateTransitionBase>();
+            case StateTransitionBase::randomWeightPropertyKey:
+                return object->is<StateTransitionBase>();
+            case StateMachineFireEventBase::eventIdPropertyKey:
+                return object->is<StateMachineFireEventBase>();
+            case StateMachineFireEventBase::occursValuePropertyKey:
+                return object->is<StateMachineFireEventBase>();
+            case LinearAnimationBase::fpsPropertyKey:
+                return object->is<LinearAnimationBase>();
+            case LinearAnimationBase::durationPropertyKey:
+                return object->is<LinearAnimationBase>();
+            case LinearAnimationBase::loopValuePropertyKey:
+                return object->is<LinearAnimationBase>();
+            case LinearAnimationBase::workStartPropertyKey:
+                return object->is<LinearAnimationBase>();
+            case LinearAnimationBase::workEndPropertyKey:
+                return object->is<LinearAnimationBase>();
+            case ElasticInterpolatorBase::easingValuePropertyKey:
+                return object->is<ElasticInterpolatorBase>();
+            case BlendState1DBase::inputIdPropertyKey:
+                return object->is<BlendState1DBase>();
+            case TransitionValueEnumComparatorBase::valuePropertyKey:
+                return object->is<TransitionValueEnumComparatorBase>();
+            case BlendStateTransitionBase::exitBlendAnimationIdPropertyKey:
+                return object->is<BlendStateTransitionBase>();
+            case StrokeBase::capPropertyKey:
+                return object->is<StrokeBase>();
+            case StrokeBase::joinPropertyKey:
+                return object->is<StrokeBase>();
+            case TrimPathBase::modeValuePropertyKey:
+                return object->is<TrimPathBase>();
+            case FillBase::fillRulePropertyKey:
+                return object->is<FillBase>();
+            case PathBase::pathFlagsPropertyKey:
+                return object->is<PathBase>();
+            case ClippingShapeBase::sourceIdPropertyKey:
+                return object->is<ClippingShapeBase>();
+            case ClippingShapeBase::fillRulePropertyKey:
+                return object->is<ClippingShapeBase>();
+            case PolygonBase::pointsPropertyKey:
+                return object->is<PolygonBase>();
+            case ImageBase::assetIdPropertyKey:
+                return object->is<ImageBase>();
+            case DrawRulesBase::drawTargetIdPropertyKey:
+                return object->is<DrawRulesBase>();
+            case LayoutComponentBase::styleIdPropertyKey:
+                return object->is<LayoutComponentBase>();
+            case ArtboardBase::defaultStateMachineIdPropertyKey:
+                return object->is<ArtboardBase>();
+            case ArtboardBase::viewModelIdPropertyKey:
+                return object->is<ArtboardBase>();
+            case JoystickBase::xIdPropertyKey:
+                return object->is<JoystickBase>();
+            case JoystickBase::yIdPropertyKey:
+                return object->is<JoystickBase>();
+            case JoystickBase::joystickFlagsPropertyKey:
+                return object->is<JoystickBase>();
+            case JoystickBase::handleSourceIdPropertyKey:
+                return object->is<JoystickBase>();
+            case OpenUrlEventBase::targetValuePropertyKey:
+                return object->is<OpenUrlEventBase>();
+            case DataBindBase::propertyKeyPropertyKey:
+                return object->is<DataBindBase>();
+            case DataBindBase::flagsPropertyKey:
+                return object->is<DataBindBase>();
+            case DataBindBase::converterIdPropertyKey:
+                return object->is<DataBindBase>();
+            case DataConverterGroupItemBase::converterIdPropertyKey:
+                return object->is<DataConverterGroupItemBase>();
+            case DataConverterRounderBase::decimalsPropertyKey:
+                return object->is<DataConverterRounderBase>();
+            case DataConverterOperationBase::operationTypePropertyKey:
+                return object->is<DataConverterOperationBase>();
+            case BindablePropertyEnumBase::propertyValuePropertyKey:
+                return object->is<BindablePropertyEnumBase>();
+            case NestedArtboardLeafBase::fitPropertyKey:
+                return object->is<NestedArtboardLeafBase>();
+            case WeightBase::valuesPropertyKey:
+                return object->is<WeightBase>();
+            case WeightBase::indicesPropertyKey:
+                return object->is<WeightBase>();
+            case TendonBase::boneIdPropertyKey:
+                return object->is<TendonBase>();
+            case CubicWeightBase::inValuesPropertyKey:
+                return object->is<CubicWeightBase>();
+            case CubicWeightBase::inIndicesPropertyKey:
+                return object->is<CubicWeightBase>();
+            case CubicWeightBase::outValuesPropertyKey:
+                return object->is<CubicWeightBase>();
+            case CubicWeightBase::outIndicesPropertyKey:
+                return object->is<CubicWeightBase>();
+            case TextModifierRangeBase::unitsValuePropertyKey:
+                return object->is<TextModifierRangeBase>();
+            case TextModifierRangeBase::typeValuePropertyKey:
+                return object->is<TextModifierRangeBase>();
+            case TextModifierRangeBase::modeValuePropertyKey:
+                return object->is<TextModifierRangeBase>();
+            case TextModifierRangeBase::runIdPropertyKey:
+                return object->is<TextModifierRangeBase>();
+            case TextStyleFeatureBase::tagPropertyKey:
+                return object->is<TextStyleFeatureBase>();
+            case TextStyleFeatureBase::featureValuePropertyKey:
+                return object->is<TextStyleFeatureBase>();
+            case TextVariationModifierBase::axisTagPropertyKey:
+                return object->is<TextVariationModifierBase>();
+            case TextModifierGroupBase::modifierFlagsPropertyKey:
+                return object->is<TextModifierGroupBase>();
+            case TextStyleBase::fontAssetIdPropertyKey:
+                return object->is<TextStyleBase>();
+            case TextStyleAxisBase::tagPropertyKey:
+                return object->is<TextStyleAxisBase>();
+            case TextBase::alignValuePropertyKey:
+                return object->is<TextBase>();
+            case TextBase::sizingValuePropertyKey:
+                return object->is<TextBase>();
+            case TextBase::overflowValuePropertyKey:
+                return object->is<TextBase>();
+            case TextBase::originValuePropertyKey:
+                return object->is<TextBase>();
+            case TextValueRunBase::styleIdPropertyKey:
+                return object->is<TextValueRunBase>();
+            case FileAssetBase::assetIdPropertyKey:
+                return object->is<FileAssetBase>();
+            case AudioEventBase::assetIdPropertyKey:
+                return object->is<AudioEventBase>();
+            case ViewModelInstanceColorBase::propertyValuePropertyKey:
+                return object->is<ViewModelInstanceColorBase>();
+            case KeyFrameColorBase::valuePropertyKey:
+                return object->is<KeyFrameColorBase>();
+            case TransitionValueColorComparatorBase::valuePropertyKey:
+                return object->is<TransitionValueColorComparatorBase>();
+            case SolidColorBase::colorValuePropertyKey:
+                return object->is<SolidColorBase>();
+            case GradientStopBase::colorValuePropertyKey:
+                return object->is<GradientStopBase>();
+            case BindablePropertyColorBase::propertyValuePropertyKey:
+                return object->is<BindablePropertyColorBase>();
+            case ViewModelComponentBase::namePropertyKey:
+                return object->is<ViewModelComponentBase>();
+            case ViewModelInstanceStringBase::propertyValuePropertyKey:
+                return object->is<ViewModelInstanceStringBase>();
+            case ComponentBase::namePropertyKey:
+                return object->is<ComponentBase>();
+            case DataEnumValueBase::keyPropertyKey:
+                return object->is<DataEnumValueBase>();
+            case DataEnumValueBase::valuePropertyKey:
+                return object->is<DataEnumValueBase>();
+            case AnimationBase::namePropertyKey:
+                return object->is<AnimationBase>();
+            case StateMachineComponentBase::namePropertyKey:
+                return object->is<StateMachineComponentBase>();
+            case KeyFrameStringBase::valuePropertyKey:
+                return object->is<KeyFrameStringBase>();
+            case TransitionValueStringComparatorBase::valuePropertyKey:
+                return object->is<TransitionValueStringComparatorBase>();
+            case OpenUrlEventBase::urlPropertyKey:
+                return object->is<OpenUrlEventBase>();
+            case DataConverterBase::namePropertyKey:
+                return object->is<DataConverterBase>();
+            case BindablePropertyStringBase::propertyValuePropertyKey:
+                return object->is<BindablePropertyStringBase>();
+            case TextValueRunBase::textPropertyKey:
+                return object->is<TextValueRunBase>();
+            case CustomPropertyStringBase::propertyValuePropertyKey:
+                return object->is<CustomPropertyStringBase>();
+            case AssetBase::namePropertyKey:
+                return object->is<AssetBase>();
+            case FileAssetBase::cdnBaseUrlPropertyKey:
+                return object->is<FileAssetBase>();
+            case ViewModelInstanceNumberBase::propertyValuePropertyKey:
+                return object->is<ViewModelInstanceNumberBase>();
+            case CustomPropertyNumberBase::propertyValuePropertyKey:
+                return object->is<CustomPropertyNumberBase>();
+            case ConstraintBase::strengthPropertyKey:
+                return object->is<ConstraintBase>();
+            case DistanceConstraintBase::distancePropertyKey:
+                return object->is<DistanceConstraintBase>();
+            case TransformComponentConstraintBase::copyFactorPropertyKey:
+                return object->is<TransformComponentConstraintBase>();
+            case TransformComponentConstraintBase::minValuePropertyKey:
+                return object->is<TransformComponentConstraintBase>();
+            case TransformComponentConstraintBase::maxValuePropertyKey:
+                return object->is<TransformComponentConstraintBase>();
+            case TransformComponentConstraintYBase::copyFactorYPropertyKey:
+                return object->is<TransformComponentConstraintYBase>();
+            case TransformComponentConstraintYBase::minValueYPropertyKey:
+                return object->is<TransformComponentConstraintYBase>();
+            case TransformComponentConstraintYBase::maxValueYPropertyKey:
+                return object->is<TransformComponentConstraintYBase>();
+            case FollowPathConstraintBase::distancePropertyKey:
+                return object->is<FollowPathConstraintBase>();
+            case TransformConstraintBase::originXPropertyKey:
+                return object->is<TransformConstraintBase>();
+            case TransformConstraintBase::originYPropertyKey:
+                return object->is<TransformConstraintBase>();
+            case WorldTransformComponentBase::opacityPropertyKey:
+                return object->is<WorldTransformComponentBase>();
+            case TransformComponentBase::rotationPropertyKey:
+                return object->is<TransformComponentBase>();
+            case TransformComponentBase::scaleXPropertyKey:
+                return object->is<TransformComponentBase>();
+            case TransformComponentBase::scaleYPropertyKey:
+                return object->is<TransformComponentBase>();
+            case NodeBase::xPropertyKey:
+            case NodeBase::xArtboardPropertyKey:
+                return object->is<NodeBase>();
+            case NodeBase::yPropertyKey:
+            case NodeBase::yArtboardPropertyKey:
+                return object->is<NodeBase>();
+            case NestedArtboardLayoutBase::instanceWidthPropertyKey:
+                return object->is<NestedArtboardLayoutBase>();
+            case NestedArtboardLayoutBase::instanceHeightPropertyKey:
+                return object->is<NestedArtboardLayoutBase>();
+            case AxisBase::offsetPropertyKey:
+                return object->is<AxisBase>();
+            case LayoutComponentStyleBase::gapHorizontalPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::gapVerticalPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::maxWidthPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::maxHeightPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::minWidthPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::minHeightPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::borderLeftPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::borderRightPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::borderTopPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::borderBottomPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::marginLeftPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::marginRightPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::marginTopPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::marginBottomPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::paddingLeftPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::paddingRightPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::paddingTopPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::paddingBottomPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::positionLeftPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::positionRightPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::positionTopPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::positionBottomPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::flexPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::flexGrowPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::flexShrinkPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::flexBasisPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::aspectRatioPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::interpolationTimePropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::cornerRadiusTLPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::cornerRadiusTRPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::cornerRadiusBLPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case LayoutComponentStyleBase::cornerRadiusBRPropertyKey:
+                return object->is<LayoutComponentStyleBase>();
+            case NestedLinearAnimationBase::mixPropertyKey:
+                return object->is<NestedLinearAnimationBase>();
+            case NestedSimpleAnimationBase::speedPropertyKey:
+                return object->is<NestedSimpleAnimationBase>();
+            case AdvanceableStateBase::speedPropertyKey:
+                return object->is<AdvanceableStateBase>();
+            case BlendAnimationDirectBase::mixValuePropertyKey:
+                return object->is<BlendAnimationDirectBase>();
+            case StateMachineNumberBase::valuePropertyKey:
+                return object->is<StateMachineNumberBase>();
+            case CubicInterpolatorBase::x1PropertyKey:
+                return object->is<CubicInterpolatorBase>();
+            case CubicInterpolatorBase::y1PropertyKey:
+                return object->is<CubicInterpolatorBase>();
+            case CubicInterpolatorBase::x2PropertyKey:
+                return object->is<CubicInterpolatorBase>();
+            case CubicInterpolatorBase::y2PropertyKey:
+                return object->is<CubicInterpolatorBase>();
+            case TransitionNumberConditionBase::valuePropertyKey:
+                return object->is<TransitionNumberConditionBase>();
+            case CubicInterpolatorComponentBase::x1PropertyKey:
+                return object->is<CubicInterpolatorComponentBase>();
+            case CubicInterpolatorComponentBase::y1PropertyKey:
+                return object->is<CubicInterpolatorComponentBase>();
+            case CubicInterpolatorComponentBase::x2PropertyKey:
+                return object->is<CubicInterpolatorComponentBase>();
+            case CubicInterpolatorComponentBase::y2PropertyKey:
+                return object->is<CubicInterpolatorComponentBase>();
+            case ListenerNumberChangeBase::valuePropertyKey:
+                return object->is<ListenerNumberChangeBase>();
+            case KeyFrameDoubleBase::valuePropertyKey:
+                return object->is<KeyFrameDoubleBase>();
+            case LinearAnimationBase::speedPropertyKey:
+                return object->is<LinearAnimationBase>();
+            case TransitionValueNumberComparatorBase::valuePropertyKey:
+                return object->is<TransitionValueNumberComparatorBase>();
+            case ElasticInterpolatorBase::amplitudePropertyKey:
+                return object->is<ElasticInterpolatorBase>();
+            case ElasticInterpolatorBase::periodPropertyKey:
+                return object->is<ElasticInterpolatorBase>();
+            case NestedNumberBase::nestedValuePropertyKey:
+                return object->is<NestedNumberBase>();
+            case NestedRemapAnimationBase::timePropertyKey:
+                return object->is<NestedRemapAnimationBase>();
+            case BlendAnimation1DBase::valuePropertyKey:
+                return object->is<BlendAnimation1DBase>();
+            case LinearGradientBase::startXPropertyKey:
+                return object->is<LinearGradientBase>();
+            case LinearGradientBase::startYPropertyKey:
+                return object->is<LinearGradientBase>();
+            case LinearGradientBase::endXPropertyKey:
+                return object->is<LinearGradientBase>();
+            case LinearGradientBase::endYPropertyKey:
+                return object->is<LinearGradientBase>();
+            case LinearGradientBase::opacityPropertyKey:
+                return object->is<LinearGradientBase>();
+            case StrokeBase::thicknessPropertyKey:
+                return object->is<StrokeBase>();
+            case GradientStopBase::positionPropertyKey:
+                return object->is<GradientStopBase>();
+            case TrimPathBase::startPropertyKey:
+                return object->is<TrimPathBase>();
+            case TrimPathBase::endPropertyKey:
+                return object->is<TrimPathBase>();
+            case TrimPathBase::offsetPropertyKey:
+                return object->is<TrimPathBase>();
+            case VertexBase::xPropertyKey:
+                return object->is<VertexBase>();
+            case VertexBase::yPropertyKey:
+                return object->is<VertexBase>();
+            case MeshVertexBase::uPropertyKey:
+                return object->is<MeshVertexBase>();
+            case MeshVertexBase::vPropertyKey:
+                return object->is<MeshVertexBase>();
+            case StraightVertexBase::radiusPropertyKey:
+                return object->is<StraightVertexBase>();
+            case CubicAsymmetricVertexBase::rotationPropertyKey:
+                return object->is<CubicAsymmetricVertexBase>();
+            case CubicAsymmetricVertexBase::inDistancePropertyKey:
+                return object->is<CubicAsymmetricVertexBase>();
+            case CubicAsymmetricVertexBase::outDistancePropertyKey:
+                return object->is<CubicAsymmetricVertexBase>();
+            case ParametricPathBase::widthPropertyKey:
+                return object->is<ParametricPathBase>();
+            case ParametricPathBase::heightPropertyKey:
+                return object->is<ParametricPathBase>();
+            case ParametricPathBase::originXPropertyKey:
+                return object->is<ParametricPathBase>();
+            case ParametricPathBase::originYPropertyKey:
+                return object->is<ParametricPathBase>();
+            case RectangleBase::cornerRadiusTLPropertyKey:
+                return object->is<RectangleBase>();
+            case RectangleBase::cornerRadiusTRPropertyKey:
+                return object->is<RectangleBase>();
+            case RectangleBase::cornerRadiusBLPropertyKey:
+                return object->is<RectangleBase>();
+            case RectangleBase::cornerRadiusBRPropertyKey:
+                return object->is<RectangleBase>();
+            case CubicMirroredVertexBase::rotationPropertyKey:
+                return object->is<CubicMirroredVertexBase>();
+            case CubicMirroredVertexBase::distancePropertyKey:
+                return object->is<CubicMirroredVertexBase>();
+            case PolygonBase::cornerRadiusPropertyKey:
+                return object->is<PolygonBase>();
+            case StarBase::innerRadiusPropertyKey:
+                return object->is<StarBase>();
+            case ImageBase::originXPropertyKey:
+                return object->is<ImageBase>();
+            case ImageBase::originYPropertyKey:
+                return object->is<ImageBase>();
+            case CubicDetachedVertexBase::inRotationPropertyKey:
+                return object->is<CubicDetachedVertexBase>();
+            case CubicDetachedVertexBase::inDistancePropertyKey:
+                return object->is<CubicDetachedVertexBase>();
+            case CubicDetachedVertexBase::outRotationPropertyKey:
+                return object->is<CubicDetachedVertexBase>();
+            case CubicDetachedVertexBase::outDistancePropertyKey:
+                return object->is<CubicDetachedVertexBase>();
+            case LayoutComponentBase::widthPropertyKey:
+                return object->is<LayoutComponentBase>();
+            case LayoutComponentBase::heightPropertyKey:
+                return object->is<LayoutComponentBase>();
+            case ArtboardBase::originXPropertyKey:
+                return object->is<ArtboardBase>();
+            case ArtboardBase::originYPropertyKey:
+                return object->is<ArtboardBase>();
+            case JoystickBase::xPropertyKey:
+                return object->is<JoystickBase>();
+            case JoystickBase::yPropertyKey:
+                return object->is<JoystickBase>();
+            case JoystickBase::posXPropertyKey:
+                return object->is<JoystickBase>();
+            case JoystickBase::posYPropertyKey:
+                return object->is<JoystickBase>();
+            case JoystickBase::originXPropertyKey:
+                return object->is<JoystickBase>();
+            case JoystickBase::originYPropertyKey:
+                return object->is<JoystickBase>();
+            case JoystickBase::widthPropertyKey:
+                return object->is<JoystickBase>();
+            case JoystickBase::heightPropertyKey:
+                return object->is<JoystickBase>();
+            case DataConverterOperationBase::valuePropertyKey:
+                return object->is<DataConverterOperationBase>();
+            case BindablePropertyNumberBase::propertyValuePropertyKey:
+                return object->is<BindablePropertyNumberBase>();
+            case NestedArtboardLeafBase::alignmentXPropertyKey:
+                return object->is<NestedArtboardLeafBase>();
+            case NestedArtboardLeafBase::alignmentYPropertyKey:
+                return object->is<NestedArtboardLeafBase>();
+            case BoneBase::lengthPropertyKey:
+                return object->is<BoneBase>();
+            case RootBoneBase::xPropertyKey:
+                return object->is<RootBoneBase>();
+            case RootBoneBase::yPropertyKey:
+                return object->is<RootBoneBase>();
+            case SkinBase::xxPropertyKey:
+                return object->is<SkinBase>();
+            case SkinBase::yxPropertyKey:
+                return object->is<SkinBase>();
+            case SkinBase::xyPropertyKey:
+                return object->is<SkinBase>();
+            case SkinBase::yyPropertyKey:
+                return object->is<SkinBase>();
+            case SkinBase::txPropertyKey:
+                return object->is<SkinBase>();
+            case SkinBase::tyPropertyKey:
+                return object->is<SkinBase>();
+            case TendonBase::xxPropertyKey:
+                return object->is<TendonBase>();
+            case TendonBase::yxPropertyKey:
+                return object->is<TendonBase>();
+            case TendonBase::xyPropertyKey:
+                return object->is<TendonBase>();
+            case TendonBase::yyPropertyKey:
+                return object->is<TendonBase>();
+            case TendonBase::txPropertyKey:
+                return object->is<TendonBase>();
+            case TendonBase::tyPropertyKey:
+                return object->is<TendonBase>();
+            case TextModifierRangeBase::modifyFromPropertyKey:
+                return object->is<TextModifierRangeBase>();
+            case TextModifierRangeBase::modifyToPropertyKey:
+                return object->is<TextModifierRangeBase>();
+            case TextModifierRangeBase::strengthPropertyKey:
+                return object->is<TextModifierRangeBase>();
+            case TextModifierRangeBase::falloffFromPropertyKey:
+                return object->is<TextModifierRangeBase>();
+            case TextModifierRangeBase::falloffToPropertyKey:
+                return object->is<TextModifierRangeBase>();
+            case TextModifierRangeBase::offsetPropertyKey:
+                return object->is<TextModifierRangeBase>();
+            case TextVariationModifierBase::axisValuePropertyKey:
+                return object->is<TextVariationModifierBase>();
+            case TextModifierGroupBase::originXPropertyKey:
+                return object->is<TextModifierGroupBase>();
+            case TextModifierGroupBase::originYPropertyKey:
+                return object->is<TextModifierGroupBase>();
+            case TextModifierGroupBase::opacityPropertyKey:
+                return object->is<TextModifierGroupBase>();
+            case TextModifierGroupBase::xPropertyKey:
+                return object->is<TextModifierGroupBase>();
+            case TextModifierGroupBase::yPropertyKey:
+                return object->is<TextModifierGroupBase>();
+            case TextModifierGroupBase::rotationPropertyKey:
+                return object->is<TextModifierGroupBase>();
+            case TextModifierGroupBase::scaleXPropertyKey:
+                return object->is<TextModifierGroupBase>();
+            case TextModifierGroupBase::scaleYPropertyKey:
+                return object->is<TextModifierGroupBase>();
+            case TextStyleBase::fontSizePropertyKey:
+                return object->is<TextStyleBase>();
+            case TextStyleBase::lineHeightPropertyKey:
+                return object->is<TextStyleBase>();
+            case TextStyleBase::letterSpacingPropertyKey:
+                return object->is<TextStyleBase>();
+            case TextStyleAxisBase::axisValuePropertyKey:
+                return object->is<TextStyleAxisBase>();
+            case TextBase::widthPropertyKey:
+                return object->is<TextBase>();
+            case TextBase::heightPropertyKey:
+                return object->is<TextBase>();
+            case TextBase::originXPropertyKey:
+                return object->is<TextBase>();
+            case TextBase::originYPropertyKey:
+                return object->is<TextBase>();
+            case TextBase::paragraphSpacingPropertyKey:
+                return object->is<TextBase>();
+            case DrawableAssetBase::heightPropertyKey:
+                return object->is<DrawableAssetBase>();
+            case DrawableAssetBase::widthPropertyKey:
+                return object->is<DrawableAssetBase>();
+            case ExportAudioBase::volumePropertyKey:
+                return object->is<ExportAudioBase>();
+            case NestedTriggerBase::firePropertyKey:
+                return object->is<NestedTriggerBase>();
+            case EventBase::triggerPropertyKey:
+                return object->is<EventBase>();
+        }
+        return false;
+    }
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/custom_property_base.hpp b/include/rive/generated/custom_property_base.hpp
new file mode 100644
index 0000000..1e21815
--- /dev/null
+++ b/include/rive/generated/custom_property_base.hpp
@@ -0,0 +1,34 @@
+#ifndef _RIVE_CUSTOM_PROPERTY_BASE_HPP_
+#define _RIVE_CUSTOM_PROPERTY_BASE_HPP_
+#include "rive/component.hpp"
+namespace rive
+{
+class CustomPropertyBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 167;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case CustomPropertyBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/custom_property_boolean_base.hpp b/include/rive/generated/custom_property_boolean_base.hpp
new file mode 100644
index 0000000..ebfe524
--- /dev/null
+++ b/include/rive/generated/custom_property_boolean_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_CUSTOM_PROPERTY_BOOLEAN_BASE_HPP_
+#define _RIVE_CUSTOM_PROPERTY_BOOLEAN_BASE_HPP_
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/custom_property.hpp"
+namespace rive
+{
+class CustomPropertyBooleanBase : public CustomProperty
+{
+protected:
+    typedef CustomProperty Super;
+
+public:
+    static const uint16_t typeKey = 129;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case CustomPropertyBooleanBase::typeKey:
+            case CustomPropertyBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t propertyValuePropertyKey = 245;
+
+private:
+    bool m_PropertyValue = false;
+
+public:
+    inline bool propertyValue() const { return m_PropertyValue; }
+    void propertyValue(bool value)
+    {
+        if (m_PropertyValue == value)
+        {
+            return;
+        }
+        m_PropertyValue = value;
+        propertyValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const CustomPropertyBooleanBase& object)
+    {
+        m_PropertyValue = object.m_PropertyValue;
+        CustomProperty::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case propertyValuePropertyKey:
+                m_PropertyValue = CoreBoolType::deserialize(reader);
+                return true;
+        }
+        return CustomProperty::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void propertyValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/custom_property_number_base.hpp b/include/rive/generated/custom_property_number_base.hpp
new file mode 100644
index 0000000..57d6d2a
--- /dev/null
+++ b/include/rive/generated/custom_property_number_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_CUSTOM_PROPERTY_NUMBER_BASE_HPP_
+#define _RIVE_CUSTOM_PROPERTY_NUMBER_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/custom_property.hpp"
+namespace rive
+{
+class CustomPropertyNumberBase : public CustomProperty
+{
+protected:
+    typedef CustomProperty Super;
+
+public:
+    static const uint16_t typeKey = 127;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case CustomPropertyNumberBase::typeKey:
+            case CustomPropertyBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t propertyValuePropertyKey = 243;
+
+private:
+    float m_PropertyValue = 0.0f;
+
+public:
+    inline float propertyValue() const { return m_PropertyValue; }
+    void propertyValue(float value)
+    {
+        if (m_PropertyValue == value)
+        {
+            return;
+        }
+        m_PropertyValue = value;
+        propertyValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const CustomPropertyNumberBase& object)
+    {
+        m_PropertyValue = object.m_PropertyValue;
+        CustomProperty::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case propertyValuePropertyKey:
+                m_PropertyValue = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return CustomProperty::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void propertyValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/custom_property_string_base.hpp b/include/rive/generated/custom_property_string_base.hpp
new file mode 100644
index 0000000..40c57ca
--- /dev/null
+++ b/include/rive/generated/custom_property_string_base.hpp
@@ -0,0 +1,73 @@
+#ifndef _RIVE_CUSTOM_PROPERTY_STRING_BASE_HPP_
+#define _RIVE_CUSTOM_PROPERTY_STRING_BASE_HPP_
+#include <string>
+#include "rive/core/field_types/core_string_type.hpp"
+#include "rive/custom_property.hpp"
+namespace rive
+{
+class CustomPropertyStringBase : public CustomProperty
+{
+protected:
+    typedef CustomProperty Super;
+
+public:
+    static const uint16_t typeKey = 130;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case CustomPropertyStringBase::typeKey:
+            case CustomPropertyBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t propertyValuePropertyKey = 246;
+
+private:
+    std::string m_PropertyValue = "";
+
+public:
+    inline const std::string& propertyValue() const { return m_PropertyValue; }
+    void propertyValue(std::string value)
+    {
+        if (m_PropertyValue == value)
+        {
+            return;
+        }
+        m_PropertyValue = value;
+        propertyValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const CustomPropertyStringBase& object)
+    {
+        m_PropertyValue = object.m_PropertyValue;
+        CustomProperty::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case propertyValuePropertyKey:
+                m_PropertyValue = CoreStringType::deserialize(reader);
+                return true;
+        }
+        return CustomProperty::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void propertyValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/data_bind/bindable_property_base.hpp b/include/rive/generated/data_bind/bindable_property_base.hpp
new file mode 100644
index 0000000..b5d5f14
--- /dev/null
+++ b/include/rive/generated/data_bind/bindable_property_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_BINDABLE_PROPERTY_BASE_HPP_
+#define _RIVE_BINDABLE_PROPERTY_BASE_HPP_
+#include "rive/core.hpp"
+namespace rive
+{
+class BindablePropertyBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 9;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case BindablePropertyBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    void copy(const BindablePropertyBase& object) {}
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override { return false; }
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/data_bind/bindable_property_boolean_base.hpp b/include/rive/generated/data_bind/bindable_property_boolean_base.hpp
new file mode 100644
index 0000000..366b551
--- /dev/null
+++ b/include/rive/generated/data_bind/bindable_property_boolean_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_BINDABLE_PROPERTY_BOOLEAN_BASE_HPP_
+#define _RIVE_BINDABLE_PROPERTY_BOOLEAN_BASE_HPP_
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/data_bind/bindable_property.hpp"
+namespace rive
+{
+class BindablePropertyBooleanBase : public BindableProperty
+{
+protected:
+    typedef BindableProperty Super;
+
+public:
+    static const uint16_t typeKey = 472;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case BindablePropertyBooleanBase::typeKey:
+            case BindablePropertyBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t propertyValuePropertyKey = 634;
+
+private:
+    bool m_PropertyValue = false;
+
+public:
+    inline bool propertyValue() const { return m_PropertyValue; }
+    void propertyValue(bool value)
+    {
+        if (m_PropertyValue == value)
+        {
+            return;
+        }
+        m_PropertyValue = value;
+        propertyValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const BindablePropertyBooleanBase& object)
+    {
+        m_PropertyValue = object.m_PropertyValue;
+        BindableProperty::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case propertyValuePropertyKey:
+                m_PropertyValue = CoreBoolType::deserialize(reader);
+                return true;
+        }
+        return BindableProperty::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void propertyValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/data_bind/bindable_property_color_base.hpp b/include/rive/generated/data_bind/bindable_property_color_base.hpp
new file mode 100644
index 0000000..a14617d
--- /dev/null
+++ b/include/rive/generated/data_bind/bindable_property_color_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_BINDABLE_PROPERTY_COLOR_BASE_HPP_
+#define _RIVE_BINDABLE_PROPERTY_COLOR_BASE_HPP_
+#include "rive/core/field_types/core_color_type.hpp"
+#include "rive/data_bind/bindable_property.hpp"
+namespace rive
+{
+class BindablePropertyColorBase : public BindableProperty
+{
+protected:
+    typedef BindableProperty Super;
+
+public:
+    static const uint16_t typeKey = 475;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case BindablePropertyColorBase::typeKey:
+            case BindablePropertyBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t propertyValuePropertyKey = 638;
+
+private:
+    int m_PropertyValue = 0xFF1D1D1D;
+
+public:
+    inline int propertyValue() const { return m_PropertyValue; }
+    void propertyValue(int value)
+    {
+        if (m_PropertyValue == value)
+        {
+            return;
+        }
+        m_PropertyValue = value;
+        propertyValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const BindablePropertyColorBase& object)
+    {
+        m_PropertyValue = object.m_PropertyValue;
+        BindableProperty::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case propertyValuePropertyKey:
+                m_PropertyValue = CoreColorType::deserialize(reader);
+                return true;
+        }
+        return BindableProperty::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void propertyValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/data_bind/bindable_property_enum_base.hpp b/include/rive/generated/data_bind/bindable_property_enum_base.hpp
new file mode 100644
index 0000000..30c6f44
--- /dev/null
+++ b/include/rive/generated/data_bind/bindable_property_enum_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_BINDABLE_PROPERTY_ENUM_BASE_HPP_
+#define _RIVE_BINDABLE_PROPERTY_ENUM_BASE_HPP_
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/data_bind/bindable_property.hpp"
+namespace rive
+{
+class BindablePropertyEnumBase : public BindableProperty
+{
+protected:
+    typedef BindableProperty Super;
+
+public:
+    static const uint16_t typeKey = 474;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case BindablePropertyEnumBase::typeKey:
+            case BindablePropertyBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t propertyValuePropertyKey = 637;
+
+private:
+    uint32_t m_PropertyValue = -1;
+
+public:
+    inline uint32_t propertyValue() const { return m_PropertyValue; }
+    void propertyValue(uint32_t value)
+    {
+        if (m_PropertyValue == value)
+        {
+            return;
+        }
+        m_PropertyValue = value;
+        propertyValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const BindablePropertyEnumBase& object)
+    {
+        m_PropertyValue = object.m_PropertyValue;
+        BindableProperty::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case propertyValuePropertyKey:
+                m_PropertyValue = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return BindableProperty::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void propertyValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/data_bind/bindable_property_number_base.hpp b/include/rive/generated/data_bind/bindable_property_number_base.hpp
new file mode 100644
index 0000000..72596c1
--- /dev/null
+++ b/include/rive/generated/data_bind/bindable_property_number_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_BINDABLE_PROPERTY_NUMBER_BASE_HPP_
+#define _RIVE_BINDABLE_PROPERTY_NUMBER_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/data_bind/bindable_property.hpp"
+namespace rive
+{
+class BindablePropertyNumberBase : public BindableProperty
+{
+protected:
+    typedef BindableProperty Super;
+
+public:
+    static const uint16_t typeKey = 473;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case BindablePropertyNumberBase::typeKey:
+            case BindablePropertyBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t propertyValuePropertyKey = 636;
+
+private:
+    float m_PropertyValue = 0.0f;
+
+public:
+    inline float propertyValue() const { return m_PropertyValue; }
+    void propertyValue(float value)
+    {
+        if (m_PropertyValue == value)
+        {
+            return;
+        }
+        m_PropertyValue = value;
+        propertyValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const BindablePropertyNumberBase& object)
+    {
+        m_PropertyValue = object.m_PropertyValue;
+        BindableProperty::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case propertyValuePropertyKey:
+                m_PropertyValue = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return BindableProperty::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void propertyValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/data_bind/bindable_property_string_base.hpp b/include/rive/generated/data_bind/bindable_property_string_base.hpp
new file mode 100644
index 0000000..88d5121
--- /dev/null
+++ b/include/rive/generated/data_bind/bindable_property_string_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_BINDABLE_PROPERTY_STRING_BASE_HPP_
+#define _RIVE_BINDABLE_PROPERTY_STRING_BASE_HPP_
+#include <string>
+#include "rive/core/field_types/core_string_type.hpp"
+#include "rive/data_bind/bindable_property.hpp"
+namespace rive
+{
+class BindablePropertyStringBase : public BindableProperty
+{
+protected:
+    typedef BindableProperty Super;
+
+public:
+    static const uint16_t typeKey = 471;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case BindablePropertyStringBase::typeKey:
+            case BindablePropertyBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t propertyValuePropertyKey = 635;
+
+private:
+    std::string m_PropertyValue = "";
+
+public:
+    inline const std::string& propertyValue() const { return m_PropertyValue; }
+    void propertyValue(std::string value)
+    {
+        if (m_PropertyValue == value)
+        {
+            return;
+        }
+        m_PropertyValue = value;
+        propertyValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const BindablePropertyStringBase& object)
+    {
+        m_PropertyValue = object.m_PropertyValue;
+        BindableProperty::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case propertyValuePropertyKey:
+                m_PropertyValue = CoreStringType::deserialize(reader);
+                return true;
+        }
+        return BindableProperty::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void propertyValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/data_bind/converters/data_converter_base.hpp b/include/rive/generated/data_bind/converters/data_converter_base.hpp
new file mode 100644
index 0000000..737b567
--- /dev/null
+++ b/include/rive/generated/data_bind/converters/data_converter_base.hpp
@@ -0,0 +1,66 @@
+#ifndef _RIVE_DATA_CONVERTER_BASE_HPP_
+#define _RIVE_DATA_CONVERTER_BASE_HPP_
+#include <string>
+#include "rive/core.hpp"
+#include "rive/core/field_types/core_string_type.hpp"
+namespace rive
+{
+class DataConverterBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 488;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case DataConverterBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t namePropertyKey = 662;
+
+private:
+    std::string m_Name = "";
+
+public:
+    inline const std::string& name() const { return m_Name; }
+    void name(std::string value)
+    {
+        if (m_Name == value)
+        {
+            return;
+        }
+        m_Name = value;
+        nameChanged();
+    }
+
+    void copy(const DataConverterBase& object) { m_Name = object.m_Name; }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case namePropertyKey:
+                m_Name = CoreStringType::deserialize(reader);
+                return true;
+        }
+        return false;
+    }
+
+protected:
+    virtual void nameChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/data_bind/converters/data_converter_group_base.hpp b/include/rive/generated/data_bind/converters/data_converter_group_base.hpp
new file mode 100644
index 0000000..32377ba
--- /dev/null
+++ b/include/rive/generated/data_bind/converters/data_converter_group_base.hpp
@@ -0,0 +1,36 @@
+#ifndef _RIVE_DATA_CONVERTER_GROUP_BASE_HPP_
+#define _RIVE_DATA_CONVERTER_GROUP_BASE_HPP_
+#include "rive/data_bind/converters/data_converter.hpp"
+namespace rive
+{
+class DataConverterGroupBase : public DataConverter
+{
+protected:
+    typedef DataConverter Super;
+
+public:
+    static const uint16_t typeKey = 499;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case DataConverterGroupBase::typeKey:
+            case DataConverterBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/data_bind/converters/data_converter_group_item_base.hpp b/include/rive/generated/data_bind/converters/data_converter_group_item_base.hpp
new file mode 100644
index 0000000..2f51281
--- /dev/null
+++ b/include/rive/generated/data_bind/converters/data_converter_group_item_base.hpp
@@ -0,0 +1,66 @@
+#ifndef _RIVE_DATA_CONVERTER_GROUP_ITEM_BASE_HPP_
+#define _RIVE_DATA_CONVERTER_GROUP_ITEM_BASE_HPP_
+#include "rive/core.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class DataConverterGroupItemBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 498;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case DataConverterGroupItemBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t converterIdPropertyKey = 679;
+
+private:
+    uint32_t m_ConverterId = -1;
+
+public:
+    inline uint32_t converterId() const { return m_ConverterId; }
+    void converterId(uint32_t value)
+    {
+        if (m_ConverterId == value)
+        {
+            return;
+        }
+        m_ConverterId = value;
+        converterIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const DataConverterGroupItemBase& object) { m_ConverterId = object.m_ConverterId; }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case converterIdPropertyKey:
+                m_ConverterId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return false;
+    }
+
+protected:
+    virtual void converterIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/data_bind/converters/data_converter_operation_base.hpp b/include/rive/generated/data_bind/converters/data_converter_operation_base.hpp
new file mode 100644
index 0000000..1291318
--- /dev/null
+++ b/include/rive/generated/data_bind/converters/data_converter_operation_base.hpp
@@ -0,0 +1,90 @@
+#ifndef _RIVE_DATA_CONVERTER_OPERATION_BASE_HPP_
+#define _RIVE_DATA_CONVERTER_OPERATION_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/data_bind/converters/data_converter.hpp"
+namespace rive
+{
+class DataConverterOperationBase : public DataConverter
+{
+protected:
+    typedef DataConverter Super;
+
+public:
+    static const uint16_t typeKey = 500;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case DataConverterOperationBase::typeKey:
+            case DataConverterBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t valuePropertyKey = 681;
+    static const uint16_t operationTypePropertyKey = 682;
+
+private:
+    float m_Value = 1.0f;
+    uint32_t m_OperationType = 0;
+
+public:
+    inline float value() const { return m_Value; }
+    void value(float value)
+    {
+        if (m_Value == value)
+        {
+            return;
+        }
+        m_Value = value;
+        valueChanged();
+    }
+
+    inline uint32_t operationType() const { return m_OperationType; }
+    void operationType(uint32_t value)
+    {
+        if (m_OperationType == value)
+        {
+            return;
+        }
+        m_OperationType = value;
+        operationTypeChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const DataConverterOperationBase& object)
+    {
+        m_Value = object.m_Value;
+        m_OperationType = object.m_OperationType;
+        DataConverter::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case valuePropertyKey:
+                m_Value = CoreDoubleType::deserialize(reader);
+                return true;
+            case operationTypePropertyKey:
+                m_OperationType = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return DataConverter::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void valueChanged() {}
+    virtual void operationTypeChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/data_bind/converters/data_converter_rounder_base.hpp b/include/rive/generated/data_bind/converters/data_converter_rounder_base.hpp
new file mode 100644
index 0000000..b1098fb
--- /dev/null
+++ b/include/rive/generated/data_bind/converters/data_converter_rounder_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_DATA_CONVERTER_ROUNDER_BASE_HPP_
+#define _RIVE_DATA_CONVERTER_ROUNDER_BASE_HPP_
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/data_bind/converters/data_converter.hpp"
+namespace rive
+{
+class DataConverterRounderBase : public DataConverter
+{
+protected:
+    typedef DataConverter Super;
+
+public:
+    static const uint16_t typeKey = 489;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case DataConverterRounderBase::typeKey:
+            case DataConverterBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t decimalsPropertyKey = 669;
+
+private:
+    uint32_t m_Decimals = 0;
+
+public:
+    inline uint32_t decimals() const { return m_Decimals; }
+    void decimals(uint32_t value)
+    {
+        if (m_Decimals == value)
+        {
+            return;
+        }
+        m_Decimals = value;
+        decimalsChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const DataConverterRounderBase& object)
+    {
+        m_Decimals = object.m_Decimals;
+        DataConverter::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case decimalsPropertyKey:
+                m_Decimals = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return DataConverter::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void decimalsChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/data_bind/converters/data_converter_to_string_base.hpp b/include/rive/generated/data_bind/converters/data_converter_to_string_base.hpp
new file mode 100644
index 0000000..c919cba
--- /dev/null
+++ b/include/rive/generated/data_bind/converters/data_converter_to_string_base.hpp
@@ -0,0 +1,36 @@
+#ifndef _RIVE_DATA_CONVERTER_TO_STRING_BASE_HPP_
+#define _RIVE_DATA_CONVERTER_TO_STRING_BASE_HPP_
+#include "rive/data_bind/converters/data_converter.hpp"
+namespace rive
+{
+class DataConverterToStringBase : public DataConverter
+{
+protected:
+    typedef DataConverter Super;
+
+public:
+    static const uint16_t typeKey = 490;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case DataConverterToStringBase::typeKey:
+            case DataConverterBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/data_bind/data_bind_base.hpp b/include/rive/generated/data_bind/data_bind_base.hpp
new file mode 100644
index 0000000..77db021
--- /dev/null
+++ b/include/rive/generated/data_bind/data_bind_base.hpp
@@ -0,0 +1,105 @@
+#ifndef _RIVE_DATA_BIND_BASE_HPP_
+#define _RIVE_DATA_BIND_BASE_HPP_
+#include "rive/core.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class DataBindBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 446;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case DataBindBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t propertyKeyPropertyKey = 586;
+    static const uint16_t flagsPropertyKey = 587;
+    static const uint16_t converterIdPropertyKey = 660;
+
+private:
+    uint32_t m_PropertyKey = Core::invalidPropertyKey;
+    uint32_t m_Flags = 0;
+    uint32_t m_ConverterId = -1;
+
+public:
+    inline uint32_t propertyKey() const { return m_PropertyKey; }
+    void propertyKey(uint32_t value)
+    {
+        if (m_PropertyKey == value)
+        {
+            return;
+        }
+        m_PropertyKey = value;
+        propertyKeyChanged();
+    }
+
+    inline uint32_t flags() const { return m_Flags; }
+    void flags(uint32_t value)
+    {
+        if (m_Flags == value)
+        {
+            return;
+        }
+        m_Flags = value;
+        flagsChanged();
+    }
+
+    inline uint32_t converterId() const { return m_ConverterId; }
+    void converterId(uint32_t value)
+    {
+        if (m_ConverterId == value)
+        {
+            return;
+        }
+        m_ConverterId = value;
+        converterIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const DataBindBase& object)
+    {
+        m_PropertyKey = object.m_PropertyKey;
+        m_Flags = object.m_Flags;
+        m_ConverterId = object.m_ConverterId;
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case propertyKeyPropertyKey:
+                m_PropertyKey = CoreUintType::deserialize(reader);
+                return true;
+            case flagsPropertyKey:
+                m_Flags = CoreUintType::deserialize(reader);
+                return true;
+            case converterIdPropertyKey:
+                m_ConverterId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return false;
+    }
+
+protected:
+    virtual void propertyKeyChanged() {}
+    virtual void flagsChanged() {}
+    virtual void converterIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/data_bind/data_bind_context_base.hpp b/include/rive/generated/data_bind/data_bind_context_base.hpp
new file mode 100644
index 0000000..e8b772a
--- /dev/null
+++ b/include/rive/generated/data_bind/data_bind_context_base.hpp
@@ -0,0 +1,61 @@
+#ifndef _RIVE_DATA_BIND_CONTEXT_BASE_HPP_
+#define _RIVE_DATA_BIND_CONTEXT_BASE_HPP_
+#include "rive/core/field_types/core_bytes_type.hpp"
+#include "rive/data_bind/data_bind.hpp"
+#include "rive/span.hpp"
+namespace rive
+{
+class DataBindContextBase : public DataBind
+{
+protected:
+    typedef DataBind Super;
+
+public:
+    static const uint16_t typeKey = 447;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case DataBindContextBase::typeKey:
+            case DataBindBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t sourcePathIdsPropertyKey = 588;
+
+public:
+    virtual void decodeSourcePathIds(Span<const uint8_t> value) = 0;
+    virtual void copySourcePathIds(const DataBindContextBase& object) = 0;
+
+    Core* clone() const override;
+    void copy(const DataBindContextBase& object)
+    {
+        copySourcePathIds(object);
+        DataBind::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case sourcePathIdsPropertyKey:
+                decodeSourcePathIds(CoreBytesType::deserialize(reader));
+                return true;
+        }
+        return DataBind::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void sourcePathIdsChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/draw_rules_base.hpp b/include/rive/generated/draw_rules_base.hpp
new file mode 100644
index 0000000..75b9cfd
--- /dev/null
+++ b/include/rive/generated/draw_rules_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_DRAW_RULES_BASE_HPP_
+#define _RIVE_DRAW_RULES_BASE_HPP_
+#include "rive/container_component.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class DrawRulesBase : public ContainerComponent
+{
+protected:
+    typedef ContainerComponent Super;
+
+public:
+    static const uint16_t typeKey = 49;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case DrawRulesBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t drawTargetIdPropertyKey = 121;
+
+private:
+    uint32_t m_DrawTargetId = -1;
+
+public:
+    inline uint32_t drawTargetId() const { return m_DrawTargetId; }
+    void drawTargetId(uint32_t value)
+    {
+        if (m_DrawTargetId == value)
+        {
+            return;
+        }
+        m_DrawTargetId = value;
+        drawTargetIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const DrawRulesBase& object)
+    {
+        m_DrawTargetId = object.m_DrawTargetId;
+        ContainerComponent::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case drawTargetIdPropertyKey:
+                m_DrawTargetId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return ContainerComponent::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void drawTargetIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/draw_target_base.hpp b/include/rive/generated/draw_target_base.hpp
new file mode 100644
index 0000000..0bc6ea4
--- /dev/null
+++ b/include/rive/generated/draw_target_base.hpp
@@ -0,0 +1,89 @@
+#ifndef _RIVE_DRAW_TARGET_BASE_HPP_
+#define _RIVE_DRAW_TARGET_BASE_HPP_
+#include "rive/component.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class DrawTargetBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 48;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case DrawTargetBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t drawableIdPropertyKey = 119;
+    static const uint16_t placementValuePropertyKey = 120;
+
+private:
+    uint32_t m_DrawableId = -1;
+    uint32_t m_PlacementValue = 0;
+
+public:
+    inline uint32_t drawableId() const { return m_DrawableId; }
+    void drawableId(uint32_t value)
+    {
+        if (m_DrawableId == value)
+        {
+            return;
+        }
+        m_DrawableId = value;
+        drawableIdChanged();
+    }
+
+    inline uint32_t placementValue() const { return m_PlacementValue; }
+    void placementValue(uint32_t value)
+    {
+        if (m_PlacementValue == value)
+        {
+            return;
+        }
+        m_PlacementValue = value;
+        placementValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const DrawTargetBase& object)
+    {
+        m_DrawableId = object.m_DrawableId;
+        m_PlacementValue = object.m_PlacementValue;
+        Component::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case drawableIdPropertyKey:
+                m_DrawableId = CoreUintType::deserialize(reader);
+                return true;
+            case placementValuePropertyKey:
+                m_PlacementValue = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return Component::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void drawableIdChanged() {}
+    virtual void placementValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/drawable_base.hpp b/include/rive/generated/drawable_base.hpp
new file mode 100644
index 0000000..0d617f5
--- /dev/null
+++ b/include/rive/generated/drawable_base.hpp
@@ -0,0 +1,92 @@
+#ifndef _RIVE_DRAWABLE_BASE_HPP_
+#define _RIVE_DRAWABLE_BASE_HPP_
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/node.hpp"
+namespace rive
+{
+class DrawableBase : public Node
+{
+protected:
+    typedef Node Super;
+
+public:
+    static const uint16_t typeKey = 13;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case DrawableBase::typeKey:
+            case NodeBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t blendModeValuePropertyKey = 23;
+    static const uint16_t drawableFlagsPropertyKey = 129;
+
+private:
+    uint32_t m_BlendModeValue = 3;
+    uint32_t m_DrawableFlags = 0;
+
+public:
+    inline uint32_t blendModeValue() const { return m_BlendModeValue; }
+    void blendModeValue(uint32_t value)
+    {
+        if (m_BlendModeValue == value)
+        {
+            return;
+        }
+        m_BlendModeValue = value;
+        blendModeValueChanged();
+    }
+
+    inline uint32_t drawableFlags() const { return m_DrawableFlags; }
+    void drawableFlags(uint32_t value)
+    {
+        if (m_DrawableFlags == value)
+        {
+            return;
+        }
+        m_DrawableFlags = value;
+        drawableFlagsChanged();
+    }
+
+    void copy(const DrawableBase& object)
+    {
+        m_BlendModeValue = object.m_BlendModeValue;
+        m_DrawableFlags = object.m_DrawableFlags;
+        Node::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case blendModeValuePropertyKey:
+                m_BlendModeValue = CoreUintType::deserialize(reader);
+                return true;
+            case drawableFlagsPropertyKey:
+                m_DrawableFlags = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return Node::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void blendModeValueChanged() {}
+    virtual void drawableFlagsChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/event_base.hpp b/include/rive/generated/event_base.hpp
new file mode 100644
index 0000000..dd04e08
--- /dev/null
+++ b/include/rive/generated/event_base.hpp
@@ -0,0 +1,43 @@
+#ifndef _RIVE_EVENT_BASE_HPP_
+#define _RIVE_EVENT_BASE_HPP_
+#include "rive/container_component.hpp"
+#include "rive/core/field_types/core_callback_type.hpp"
+namespace rive
+{
+class EventBase : public ContainerComponent
+{
+protected:
+    typedef ContainerComponent Super;
+
+public:
+    static const uint16_t typeKey = 128;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case EventBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t triggerPropertyKey = 395;
+
+public:
+    virtual void trigger(const CallbackData& value) = 0;
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/joystick_base.hpp b/include/rive/generated/joystick_base.hpp
new file mode 100644
index 0000000..d32b1a4
--- /dev/null
+++ b/include/rive/generated/joystick_base.hpp
@@ -0,0 +1,270 @@
+#ifndef _RIVE_JOYSTICK_BASE_HPP_
+#define _RIVE_JOYSTICK_BASE_HPP_
+#include "rive/component.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class JoystickBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 148;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case JoystickBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t xPropertyKey = 299;
+    static const uint16_t yPropertyKey = 300;
+    static const uint16_t posXPropertyKey = 303;
+    static const uint16_t posYPropertyKey = 304;
+    static const uint16_t originXPropertyKey = 307;
+    static const uint16_t originYPropertyKey = 308;
+    static const uint16_t widthPropertyKey = 305;
+    static const uint16_t heightPropertyKey = 306;
+    static const uint16_t xIdPropertyKey = 301;
+    static const uint16_t yIdPropertyKey = 302;
+    static const uint16_t joystickFlagsPropertyKey = 312;
+    static const uint16_t handleSourceIdPropertyKey = 313;
+
+private:
+    float m_X = 0.0f;
+    float m_Y = 0.0f;
+    float m_PosX = 0.0f;
+    float m_PosY = 0.0f;
+    float m_OriginX = 0.5f;
+    float m_OriginY = 0.5f;
+    float m_Width = 100.0f;
+    float m_Height = 100.0f;
+    uint32_t m_XId = -1;
+    uint32_t m_YId = -1;
+    uint32_t m_JoystickFlags = 0;
+    uint32_t m_HandleSourceId = -1;
+
+public:
+    inline float x() const { return m_X; }
+    void x(float value)
+    {
+        if (m_X == value)
+        {
+            return;
+        }
+        m_X = value;
+        xChanged();
+    }
+
+    inline float y() const { return m_Y; }
+    void y(float value)
+    {
+        if (m_Y == value)
+        {
+            return;
+        }
+        m_Y = value;
+        yChanged();
+    }
+
+    inline float posX() const { return m_PosX; }
+    void posX(float value)
+    {
+        if (m_PosX == value)
+        {
+            return;
+        }
+        m_PosX = value;
+        posXChanged();
+    }
+
+    inline float posY() const { return m_PosY; }
+    void posY(float value)
+    {
+        if (m_PosY == value)
+        {
+            return;
+        }
+        m_PosY = value;
+        posYChanged();
+    }
+
+    inline float originX() const { return m_OriginX; }
+    void originX(float value)
+    {
+        if (m_OriginX == value)
+        {
+            return;
+        }
+        m_OriginX = value;
+        originXChanged();
+    }
+
+    inline float originY() const { return m_OriginY; }
+    void originY(float value)
+    {
+        if (m_OriginY == value)
+        {
+            return;
+        }
+        m_OriginY = value;
+        originYChanged();
+    }
+
+    inline float width() const { return m_Width; }
+    void width(float value)
+    {
+        if (m_Width == value)
+        {
+            return;
+        }
+        m_Width = value;
+        widthChanged();
+    }
+
+    inline float height() const { return m_Height; }
+    void height(float value)
+    {
+        if (m_Height == value)
+        {
+            return;
+        }
+        m_Height = value;
+        heightChanged();
+    }
+
+    inline uint32_t xId() const { return m_XId; }
+    void xId(uint32_t value)
+    {
+        if (m_XId == value)
+        {
+            return;
+        }
+        m_XId = value;
+        xIdChanged();
+    }
+
+    inline uint32_t yId() const { return m_YId; }
+    void yId(uint32_t value)
+    {
+        if (m_YId == value)
+        {
+            return;
+        }
+        m_YId = value;
+        yIdChanged();
+    }
+
+    inline uint32_t joystickFlags() const { return m_JoystickFlags; }
+    void joystickFlags(uint32_t value)
+    {
+        if (m_JoystickFlags == value)
+        {
+            return;
+        }
+        m_JoystickFlags = value;
+        joystickFlagsChanged();
+    }
+
+    inline uint32_t handleSourceId() const { return m_HandleSourceId; }
+    void handleSourceId(uint32_t value)
+    {
+        if (m_HandleSourceId == value)
+        {
+            return;
+        }
+        m_HandleSourceId = value;
+        handleSourceIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const JoystickBase& object)
+    {
+        m_X = object.m_X;
+        m_Y = object.m_Y;
+        m_PosX = object.m_PosX;
+        m_PosY = object.m_PosY;
+        m_OriginX = object.m_OriginX;
+        m_OriginY = object.m_OriginY;
+        m_Width = object.m_Width;
+        m_Height = object.m_Height;
+        m_XId = object.m_XId;
+        m_YId = object.m_YId;
+        m_JoystickFlags = object.m_JoystickFlags;
+        m_HandleSourceId = object.m_HandleSourceId;
+        Component::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case xPropertyKey:
+                m_X = CoreDoubleType::deserialize(reader);
+                return true;
+            case yPropertyKey:
+                m_Y = CoreDoubleType::deserialize(reader);
+                return true;
+            case posXPropertyKey:
+                m_PosX = CoreDoubleType::deserialize(reader);
+                return true;
+            case posYPropertyKey:
+                m_PosY = CoreDoubleType::deserialize(reader);
+                return true;
+            case originXPropertyKey:
+                m_OriginX = CoreDoubleType::deserialize(reader);
+                return true;
+            case originYPropertyKey:
+                m_OriginY = CoreDoubleType::deserialize(reader);
+                return true;
+            case widthPropertyKey:
+                m_Width = CoreDoubleType::deserialize(reader);
+                return true;
+            case heightPropertyKey:
+                m_Height = CoreDoubleType::deserialize(reader);
+                return true;
+            case xIdPropertyKey:
+                m_XId = CoreUintType::deserialize(reader);
+                return true;
+            case yIdPropertyKey:
+                m_YId = CoreUintType::deserialize(reader);
+                return true;
+            case joystickFlagsPropertyKey:
+                m_JoystickFlags = CoreUintType::deserialize(reader);
+                return true;
+            case handleSourceIdPropertyKey:
+                m_HandleSourceId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return Component::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void xChanged() {}
+    virtual void yChanged() {}
+    virtual void posXChanged() {}
+    virtual void posYChanged() {}
+    virtual void originXChanged() {}
+    virtual void originYChanged() {}
+    virtual void widthChanged() {}
+    virtual void heightChanged() {}
+    virtual void xIdChanged() {}
+    virtual void yIdChanged() {}
+    virtual void joystickFlagsChanged() {}
+    virtual void handleSourceIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/layout/axis_base.hpp b/include/rive/generated/layout/axis_base.hpp
new file mode 100644
index 0000000..b7bf729
--- /dev/null
+++ b/include/rive/generated/layout/axis_base.hpp
@@ -0,0 +1,89 @@
+#ifndef _RIVE_AXIS_BASE_HPP_
+#define _RIVE_AXIS_BASE_HPP_
+#include "rive/component.hpp"
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class AxisBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 492;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case AxisBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t offsetPropertyKey = 675;
+    static const uint16_t normalizedPropertyKey = 676;
+
+private:
+    float m_Offset = 0.0f;
+    bool m_Normalized = false;
+
+public:
+    inline float offset() const { return m_Offset; }
+    void offset(float value)
+    {
+        if (m_Offset == value)
+        {
+            return;
+        }
+        m_Offset = value;
+        offsetChanged();
+    }
+
+    inline bool normalized() const { return m_Normalized; }
+    void normalized(bool value)
+    {
+        if (m_Normalized == value)
+        {
+            return;
+        }
+        m_Normalized = value;
+        normalizedChanged();
+    }
+
+    void copy(const AxisBase& object)
+    {
+        m_Offset = object.m_Offset;
+        m_Normalized = object.m_Normalized;
+        Component::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case offsetPropertyKey:
+                m_Offset = CoreDoubleType::deserialize(reader);
+                return true;
+            case normalizedPropertyKey:
+                m_Normalized = CoreBoolType::deserialize(reader);
+                return true;
+        }
+        return Component::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void offsetChanged() {}
+    virtual void normalizedChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/layout/axis_x_base.hpp b/include/rive/generated/layout/axis_x_base.hpp
new file mode 100644
index 0000000..0785962
--- /dev/null
+++ b/include/rive/generated/layout/axis_x_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_AXIS_XBASE_HPP_
+#define _RIVE_AXIS_XBASE_HPP_
+#include "rive/layout/axis.hpp"
+namespace rive
+{
+class AxisXBase : public Axis
+{
+protected:
+    typedef Axis Super;
+
+public:
+    static const uint16_t typeKey = 495;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case AxisXBase::typeKey:
+            case AxisBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/layout/axis_y_base.hpp b/include/rive/generated/layout/axis_y_base.hpp
new file mode 100644
index 0000000..e54b22a
--- /dev/null
+++ b/include/rive/generated/layout/axis_y_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_AXIS_YBASE_HPP_
+#define _RIVE_AXIS_YBASE_HPP_
+#include "rive/layout/axis.hpp"
+namespace rive
+{
+class AxisYBase : public Axis
+{
+protected:
+    typedef Axis Super;
+
+public:
+    static const uint16_t typeKey = 494;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case AxisYBase::typeKey:
+            case AxisBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/layout/layout_component_style_base.hpp b/include/rive/generated/layout/layout_component_style_base.hpp
new file mode 100644
index 0000000..13003f5
--- /dev/null
+++ b/include/rive/generated/layout/layout_component_style_base.hpp
@@ -0,0 +1,1387 @@
+#ifndef _RIVE_LAYOUT_COMPONENT_STYLE_BASE_HPP_
+#define _RIVE_LAYOUT_COMPONENT_STYLE_BASE_HPP_
+#include "rive/component.hpp"
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class LayoutComponentStyleBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 420;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case LayoutComponentStyleBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t gapHorizontalPropertyKey = 498;
+    static const uint16_t gapVerticalPropertyKey = 499;
+    static const uint16_t maxWidthPropertyKey = 500;
+    static const uint16_t maxHeightPropertyKey = 501;
+    static const uint16_t minWidthPropertyKey = 502;
+    static const uint16_t minHeightPropertyKey = 503;
+    static const uint16_t borderLeftPropertyKey = 504;
+    static const uint16_t borderRightPropertyKey = 505;
+    static const uint16_t borderTopPropertyKey = 506;
+    static const uint16_t borderBottomPropertyKey = 507;
+    static const uint16_t marginLeftPropertyKey = 508;
+    static const uint16_t marginRightPropertyKey = 509;
+    static const uint16_t marginTopPropertyKey = 510;
+    static const uint16_t marginBottomPropertyKey = 511;
+    static const uint16_t paddingLeftPropertyKey = 512;
+    static const uint16_t paddingRightPropertyKey = 513;
+    static const uint16_t paddingTopPropertyKey = 514;
+    static const uint16_t paddingBottomPropertyKey = 515;
+    static const uint16_t positionLeftPropertyKey = 516;
+    static const uint16_t positionRightPropertyKey = 517;
+    static const uint16_t positionTopPropertyKey = 518;
+    static const uint16_t positionBottomPropertyKey = 519;
+    static const uint16_t flexPropertyKey = 520;
+    static const uint16_t flexGrowPropertyKey = 521;
+    static const uint16_t flexShrinkPropertyKey = 522;
+    static const uint16_t flexBasisPropertyKey = 523;
+    static const uint16_t aspectRatioPropertyKey = 524;
+    static const uint16_t layoutWidthScaleTypePropertyKey = 655;
+    static const uint16_t layoutHeightScaleTypePropertyKey = 656;
+    static const uint16_t layoutAlignmentTypePropertyKey = 632;
+    static const uint16_t animationStyleTypePropertyKey = 589;
+    static const uint16_t interpolationTypePropertyKey = 590;
+    static const uint16_t interpolatorIdPropertyKey = 591;
+    static const uint16_t interpolationTimePropertyKey = 592;
+    static const uint16_t displayValuePropertyKey = 596;
+    static const uint16_t positionTypeValuePropertyKey = 597;
+    static const uint16_t flexDirectionValuePropertyKey = 598;
+    static const uint16_t directionValuePropertyKey = 599;
+    static const uint16_t alignContentValuePropertyKey = 600;
+    static const uint16_t alignItemsValuePropertyKey = 601;
+    static const uint16_t alignSelfValuePropertyKey = 602;
+    static const uint16_t justifyContentValuePropertyKey = 603;
+    static const uint16_t flexWrapValuePropertyKey = 604;
+    static const uint16_t overflowValuePropertyKey = 605;
+    static const uint16_t intrinsicallySizedValuePropertyKey = 606;
+    static const uint16_t widthUnitsValuePropertyKey = 607;
+    static const uint16_t heightUnitsValuePropertyKey = 608;
+    static const uint16_t borderLeftUnitsValuePropertyKey = 609;
+    static const uint16_t borderRightUnitsValuePropertyKey = 610;
+    static const uint16_t borderTopUnitsValuePropertyKey = 611;
+    static const uint16_t borderBottomUnitsValuePropertyKey = 612;
+    static const uint16_t marginLeftUnitsValuePropertyKey = 613;
+    static const uint16_t marginRightUnitsValuePropertyKey = 614;
+    static const uint16_t marginTopUnitsValuePropertyKey = 615;
+    static const uint16_t marginBottomUnitsValuePropertyKey = 616;
+    static const uint16_t paddingLeftUnitsValuePropertyKey = 617;
+    static const uint16_t paddingRightUnitsValuePropertyKey = 618;
+    static const uint16_t paddingTopUnitsValuePropertyKey = 619;
+    static const uint16_t paddingBottomUnitsValuePropertyKey = 620;
+    static const uint16_t positionLeftUnitsValuePropertyKey = 621;
+    static const uint16_t positionRightUnitsValuePropertyKey = 622;
+    static const uint16_t positionTopUnitsValuePropertyKey = 623;
+    static const uint16_t positionBottomUnitsValuePropertyKey = 624;
+    static const uint16_t gapHorizontalUnitsValuePropertyKey = 625;
+    static const uint16_t gapVerticalUnitsValuePropertyKey = 626;
+    static const uint16_t minWidthUnitsValuePropertyKey = 627;
+    static const uint16_t minHeightUnitsValuePropertyKey = 628;
+    static const uint16_t maxWidthUnitsValuePropertyKey = 629;
+    static const uint16_t maxHeightUnitsValuePropertyKey = 630;
+    static const uint16_t linkCornerRadiusPropertyKey = 639;
+    static const uint16_t cornerRadiusTLPropertyKey = 640;
+    static const uint16_t cornerRadiusTRPropertyKey = 641;
+    static const uint16_t cornerRadiusBLPropertyKey = 642;
+    static const uint16_t cornerRadiusBRPropertyKey = 643;
+
+private:
+    float m_GapHorizontal = 0.0f;
+    float m_GapVertical = 0.0f;
+    float m_MaxWidth = 0.0f;
+    float m_MaxHeight = 0.0f;
+    float m_MinWidth = 0.0f;
+    float m_MinHeight = 0.0f;
+    float m_BorderLeft = 0.0f;
+    float m_BorderRight = 0.0f;
+    float m_BorderTop = 0.0f;
+    float m_BorderBottom = 0.0f;
+    float m_MarginLeft = 0.0f;
+    float m_MarginRight = 0.0f;
+    float m_MarginTop = 0.0f;
+    float m_MarginBottom = 0.0f;
+    float m_PaddingLeft = 0.0f;
+    float m_PaddingRight = 0.0f;
+    float m_PaddingTop = 0.0f;
+    float m_PaddingBottom = 0.0f;
+    float m_PositionLeft = 0.0f;
+    float m_PositionRight = 0.0f;
+    float m_PositionTop = 0.0f;
+    float m_PositionBottom = 0.0f;
+    float m_Flex = 0.0f;
+    float m_FlexGrow = 0.0f;
+    float m_FlexShrink = 1.0f;
+    float m_FlexBasis = 1.0f;
+    float m_AspectRatio = 0.0f;
+    uint32_t m_LayoutWidthScaleType = 0;
+    uint32_t m_LayoutHeightScaleType = 0;
+    uint32_t m_LayoutAlignmentType = 0;
+    uint32_t m_AnimationStyleType = 0;
+    uint32_t m_InterpolationType = 0;
+    uint32_t m_InterpolatorId = -1;
+    float m_InterpolationTime = 0.0f;
+    uint32_t m_DisplayValue = 0;
+    uint32_t m_PositionTypeValue = 1;
+    uint32_t m_FlexDirectionValue = 2;
+    uint32_t m_DirectionValue = 0;
+    uint32_t m_AlignContentValue = 0;
+    uint32_t m_AlignItemsValue = 1;
+    uint32_t m_AlignSelfValue = 0;
+    uint32_t m_JustifyContentValue = 0;
+    uint32_t m_FlexWrapValue = 0;
+    uint32_t m_OverflowValue = 0;
+    bool m_IntrinsicallySizedValue = false;
+    uint32_t m_WidthUnitsValue = 1;
+    uint32_t m_HeightUnitsValue = 1;
+    uint32_t m_BorderLeftUnitsValue = 0;
+    uint32_t m_BorderRightUnitsValue = 0;
+    uint32_t m_BorderTopUnitsValue = 0;
+    uint32_t m_BorderBottomUnitsValue = 0;
+    uint32_t m_MarginLeftUnitsValue = 0;
+    uint32_t m_MarginRightUnitsValue = 0;
+    uint32_t m_MarginTopUnitsValue = 0;
+    uint32_t m_MarginBottomUnitsValue = 0;
+    uint32_t m_PaddingLeftUnitsValue = 0;
+    uint32_t m_PaddingRightUnitsValue = 0;
+    uint32_t m_PaddingTopUnitsValue = 0;
+    uint32_t m_PaddingBottomUnitsValue = 0;
+    uint32_t m_PositionLeftUnitsValue = 0;
+    uint32_t m_PositionRightUnitsValue = 0;
+    uint32_t m_PositionTopUnitsValue = 0;
+    uint32_t m_PositionBottomUnitsValue = 0;
+    uint32_t m_GapHorizontalUnitsValue = 0;
+    uint32_t m_GapVerticalUnitsValue = 0;
+    uint32_t m_MinWidthUnitsValue = 0;
+    uint32_t m_MinHeightUnitsValue = 0;
+    uint32_t m_MaxWidthUnitsValue = 0;
+    uint32_t m_MaxHeightUnitsValue = 0;
+    bool m_LinkCornerRadius = true;
+    float m_CornerRadiusTL = 0.0f;
+    float m_CornerRadiusTR = 0.0f;
+    float m_CornerRadiusBL = 0.0f;
+    float m_CornerRadiusBR = 0.0f;
+
+public:
+    inline float gapHorizontal() const { return m_GapHorizontal; }
+    void gapHorizontal(float value)
+    {
+        if (m_GapHorizontal == value)
+        {
+            return;
+        }
+        m_GapHorizontal = value;
+        gapHorizontalChanged();
+    }
+
+    inline float gapVertical() const { return m_GapVertical; }
+    void gapVertical(float value)
+    {
+        if (m_GapVertical == value)
+        {
+            return;
+        }
+        m_GapVertical = value;
+        gapVerticalChanged();
+    }
+
+    inline float maxWidth() const { return m_MaxWidth; }
+    void maxWidth(float value)
+    {
+        if (m_MaxWidth == value)
+        {
+            return;
+        }
+        m_MaxWidth = value;
+        maxWidthChanged();
+    }
+
+    inline float maxHeight() const { return m_MaxHeight; }
+    void maxHeight(float value)
+    {
+        if (m_MaxHeight == value)
+        {
+            return;
+        }
+        m_MaxHeight = value;
+        maxHeightChanged();
+    }
+
+    inline float minWidth() const { return m_MinWidth; }
+    void minWidth(float value)
+    {
+        if (m_MinWidth == value)
+        {
+            return;
+        }
+        m_MinWidth = value;
+        minWidthChanged();
+    }
+
+    inline float minHeight() const { return m_MinHeight; }
+    void minHeight(float value)
+    {
+        if (m_MinHeight == value)
+        {
+            return;
+        }
+        m_MinHeight = value;
+        minHeightChanged();
+    }
+
+    inline float borderLeft() const { return m_BorderLeft; }
+    void borderLeft(float value)
+    {
+        if (m_BorderLeft == value)
+        {
+            return;
+        }
+        m_BorderLeft = value;
+        borderLeftChanged();
+    }
+
+    inline float borderRight() const { return m_BorderRight; }
+    void borderRight(float value)
+    {
+        if (m_BorderRight == value)
+        {
+            return;
+        }
+        m_BorderRight = value;
+        borderRightChanged();
+    }
+
+    inline float borderTop() const { return m_BorderTop; }
+    void borderTop(float value)
+    {
+        if (m_BorderTop == value)
+        {
+            return;
+        }
+        m_BorderTop = value;
+        borderTopChanged();
+    }
+
+    inline float borderBottom() const { return m_BorderBottom; }
+    void borderBottom(float value)
+    {
+        if (m_BorderBottom == value)
+        {
+            return;
+        }
+        m_BorderBottom = value;
+        borderBottomChanged();
+    }
+
+    inline float marginLeft() const { return m_MarginLeft; }
+    void marginLeft(float value)
+    {
+        if (m_MarginLeft == value)
+        {
+            return;
+        }
+        m_MarginLeft = value;
+        marginLeftChanged();
+    }
+
+    inline float marginRight() const { return m_MarginRight; }
+    void marginRight(float value)
+    {
+        if (m_MarginRight == value)
+        {
+            return;
+        }
+        m_MarginRight = value;
+        marginRightChanged();
+    }
+
+    inline float marginTop() const { return m_MarginTop; }
+    void marginTop(float value)
+    {
+        if (m_MarginTop == value)
+        {
+            return;
+        }
+        m_MarginTop = value;
+        marginTopChanged();
+    }
+
+    inline float marginBottom() const { return m_MarginBottom; }
+    void marginBottom(float value)
+    {
+        if (m_MarginBottom == value)
+        {
+            return;
+        }
+        m_MarginBottom = value;
+        marginBottomChanged();
+    }
+
+    inline float paddingLeft() const { return m_PaddingLeft; }
+    void paddingLeft(float value)
+    {
+        if (m_PaddingLeft == value)
+        {
+            return;
+        }
+        m_PaddingLeft = value;
+        paddingLeftChanged();
+    }
+
+    inline float paddingRight() const { return m_PaddingRight; }
+    void paddingRight(float value)
+    {
+        if (m_PaddingRight == value)
+        {
+            return;
+        }
+        m_PaddingRight = value;
+        paddingRightChanged();
+    }
+
+    inline float paddingTop() const { return m_PaddingTop; }
+    void paddingTop(float value)
+    {
+        if (m_PaddingTop == value)
+        {
+            return;
+        }
+        m_PaddingTop = value;
+        paddingTopChanged();
+    }
+
+    inline float paddingBottom() const { return m_PaddingBottom; }
+    void paddingBottom(float value)
+    {
+        if (m_PaddingBottom == value)
+        {
+            return;
+        }
+        m_PaddingBottom = value;
+        paddingBottomChanged();
+    }
+
+    inline float positionLeft() const { return m_PositionLeft; }
+    void positionLeft(float value)
+    {
+        if (m_PositionLeft == value)
+        {
+            return;
+        }
+        m_PositionLeft = value;
+        positionLeftChanged();
+    }
+
+    inline float positionRight() const { return m_PositionRight; }
+    void positionRight(float value)
+    {
+        if (m_PositionRight == value)
+        {
+            return;
+        }
+        m_PositionRight = value;
+        positionRightChanged();
+    }
+
+    inline float positionTop() const { return m_PositionTop; }
+    void positionTop(float value)
+    {
+        if (m_PositionTop == value)
+        {
+            return;
+        }
+        m_PositionTop = value;
+        positionTopChanged();
+    }
+
+    inline float positionBottom() const { return m_PositionBottom; }
+    void positionBottom(float value)
+    {
+        if (m_PositionBottom == value)
+        {
+            return;
+        }
+        m_PositionBottom = value;
+        positionBottomChanged();
+    }
+
+    inline float flex() const { return m_Flex; }
+    void flex(float value)
+    {
+        if (m_Flex == value)
+        {
+            return;
+        }
+        m_Flex = value;
+        flexChanged();
+    }
+
+    inline float flexGrow() const { return m_FlexGrow; }
+    void flexGrow(float value)
+    {
+        if (m_FlexGrow == value)
+        {
+            return;
+        }
+        m_FlexGrow = value;
+        flexGrowChanged();
+    }
+
+    inline float flexShrink() const { return m_FlexShrink; }
+    void flexShrink(float value)
+    {
+        if (m_FlexShrink == value)
+        {
+            return;
+        }
+        m_FlexShrink = value;
+        flexShrinkChanged();
+    }
+
+    inline float flexBasis() const { return m_FlexBasis; }
+    void flexBasis(float value)
+    {
+        if (m_FlexBasis == value)
+        {
+            return;
+        }
+        m_FlexBasis = value;
+        flexBasisChanged();
+    }
+
+    inline float aspectRatio() const { return m_AspectRatio; }
+    void aspectRatio(float value)
+    {
+        if (m_AspectRatio == value)
+        {
+            return;
+        }
+        m_AspectRatio = value;
+        aspectRatioChanged();
+    }
+
+    inline uint32_t layoutWidthScaleType() const { return m_LayoutWidthScaleType; }
+    void layoutWidthScaleType(uint32_t value)
+    {
+        if (m_LayoutWidthScaleType == value)
+        {
+            return;
+        }
+        m_LayoutWidthScaleType = value;
+        layoutWidthScaleTypeChanged();
+    }
+
+    inline uint32_t layoutHeightScaleType() const { return m_LayoutHeightScaleType; }
+    void layoutHeightScaleType(uint32_t value)
+    {
+        if (m_LayoutHeightScaleType == value)
+        {
+            return;
+        }
+        m_LayoutHeightScaleType = value;
+        layoutHeightScaleTypeChanged();
+    }
+
+    inline uint32_t layoutAlignmentType() const { return m_LayoutAlignmentType; }
+    void layoutAlignmentType(uint32_t value)
+    {
+        if (m_LayoutAlignmentType == value)
+        {
+            return;
+        }
+        m_LayoutAlignmentType = value;
+        layoutAlignmentTypeChanged();
+    }
+
+    inline uint32_t animationStyleType() const { return m_AnimationStyleType; }
+    void animationStyleType(uint32_t value)
+    {
+        if (m_AnimationStyleType == value)
+        {
+            return;
+        }
+        m_AnimationStyleType = value;
+        animationStyleTypeChanged();
+    }
+
+    inline uint32_t interpolationType() const { return m_InterpolationType; }
+    void interpolationType(uint32_t value)
+    {
+        if (m_InterpolationType == value)
+        {
+            return;
+        }
+        m_InterpolationType = value;
+        interpolationTypeChanged();
+    }
+
+    inline uint32_t interpolatorId() const { return m_InterpolatorId; }
+    void interpolatorId(uint32_t value)
+    {
+        if (m_InterpolatorId == value)
+        {
+            return;
+        }
+        m_InterpolatorId = value;
+        interpolatorIdChanged();
+    }
+
+    inline float interpolationTime() const { return m_InterpolationTime; }
+    void interpolationTime(float value)
+    {
+        if (m_InterpolationTime == value)
+        {
+            return;
+        }
+        m_InterpolationTime = value;
+        interpolationTimeChanged();
+    }
+
+    inline uint32_t displayValue() const { return m_DisplayValue; }
+    void displayValue(uint32_t value)
+    {
+        if (m_DisplayValue == value)
+        {
+            return;
+        }
+        m_DisplayValue = value;
+        displayValueChanged();
+    }
+
+    inline uint32_t positionTypeValue() const { return m_PositionTypeValue; }
+    void positionTypeValue(uint32_t value)
+    {
+        if (m_PositionTypeValue == value)
+        {
+            return;
+        }
+        m_PositionTypeValue = value;
+        positionTypeValueChanged();
+    }
+
+    inline uint32_t flexDirectionValue() const { return m_FlexDirectionValue; }
+    void flexDirectionValue(uint32_t value)
+    {
+        if (m_FlexDirectionValue == value)
+        {
+            return;
+        }
+        m_FlexDirectionValue = value;
+        flexDirectionValueChanged();
+    }
+
+    inline uint32_t directionValue() const { return m_DirectionValue; }
+    void directionValue(uint32_t value)
+    {
+        if (m_DirectionValue == value)
+        {
+            return;
+        }
+        m_DirectionValue = value;
+        directionValueChanged();
+    }
+
+    inline uint32_t alignContentValue() const { return m_AlignContentValue; }
+    void alignContentValue(uint32_t value)
+    {
+        if (m_AlignContentValue == value)
+        {
+            return;
+        }
+        m_AlignContentValue = value;
+        alignContentValueChanged();
+    }
+
+    inline uint32_t alignItemsValue() const { return m_AlignItemsValue; }
+    void alignItemsValue(uint32_t value)
+    {
+        if (m_AlignItemsValue == value)
+        {
+            return;
+        }
+        m_AlignItemsValue = value;
+        alignItemsValueChanged();
+    }
+
+    inline uint32_t alignSelfValue() const { return m_AlignSelfValue; }
+    void alignSelfValue(uint32_t value)
+    {
+        if (m_AlignSelfValue == value)
+        {
+            return;
+        }
+        m_AlignSelfValue = value;
+        alignSelfValueChanged();
+    }
+
+    inline uint32_t justifyContentValue() const { return m_JustifyContentValue; }
+    void justifyContentValue(uint32_t value)
+    {
+        if (m_JustifyContentValue == value)
+        {
+            return;
+        }
+        m_JustifyContentValue = value;
+        justifyContentValueChanged();
+    }
+
+    inline uint32_t flexWrapValue() const { return m_FlexWrapValue; }
+    void flexWrapValue(uint32_t value)
+    {
+        if (m_FlexWrapValue == value)
+        {
+            return;
+        }
+        m_FlexWrapValue = value;
+        flexWrapValueChanged();
+    }
+
+    inline uint32_t overflowValue() const { return m_OverflowValue; }
+    void overflowValue(uint32_t value)
+    {
+        if (m_OverflowValue == value)
+        {
+            return;
+        }
+        m_OverflowValue = value;
+        overflowValueChanged();
+    }
+
+    inline bool intrinsicallySizedValue() const { return m_IntrinsicallySizedValue; }
+    void intrinsicallySizedValue(bool value)
+    {
+        if (m_IntrinsicallySizedValue == value)
+        {
+            return;
+        }
+        m_IntrinsicallySizedValue = value;
+        intrinsicallySizedValueChanged();
+    }
+
+    inline uint32_t widthUnitsValue() const { return m_WidthUnitsValue; }
+    void widthUnitsValue(uint32_t value)
+    {
+        if (m_WidthUnitsValue == value)
+        {
+            return;
+        }
+        m_WidthUnitsValue = value;
+        widthUnitsValueChanged();
+    }
+
+    inline uint32_t heightUnitsValue() const { return m_HeightUnitsValue; }
+    void heightUnitsValue(uint32_t value)
+    {
+        if (m_HeightUnitsValue == value)
+        {
+            return;
+        }
+        m_HeightUnitsValue = value;
+        heightUnitsValueChanged();
+    }
+
+    inline uint32_t borderLeftUnitsValue() const { return m_BorderLeftUnitsValue; }
+    void borderLeftUnitsValue(uint32_t value)
+    {
+        if (m_BorderLeftUnitsValue == value)
+        {
+            return;
+        }
+        m_BorderLeftUnitsValue = value;
+        borderLeftUnitsValueChanged();
+    }
+
+    inline uint32_t borderRightUnitsValue() const { return m_BorderRightUnitsValue; }
+    void borderRightUnitsValue(uint32_t value)
+    {
+        if (m_BorderRightUnitsValue == value)
+        {
+            return;
+        }
+        m_BorderRightUnitsValue = value;
+        borderRightUnitsValueChanged();
+    }
+
+    inline uint32_t borderTopUnitsValue() const { return m_BorderTopUnitsValue; }
+    void borderTopUnitsValue(uint32_t value)
+    {
+        if (m_BorderTopUnitsValue == value)
+        {
+            return;
+        }
+        m_BorderTopUnitsValue = value;
+        borderTopUnitsValueChanged();
+    }
+
+    inline uint32_t borderBottomUnitsValue() const { return m_BorderBottomUnitsValue; }
+    void borderBottomUnitsValue(uint32_t value)
+    {
+        if (m_BorderBottomUnitsValue == value)
+        {
+            return;
+        }
+        m_BorderBottomUnitsValue = value;
+        borderBottomUnitsValueChanged();
+    }
+
+    inline uint32_t marginLeftUnitsValue() const { return m_MarginLeftUnitsValue; }
+    void marginLeftUnitsValue(uint32_t value)
+    {
+        if (m_MarginLeftUnitsValue == value)
+        {
+            return;
+        }
+        m_MarginLeftUnitsValue = value;
+        marginLeftUnitsValueChanged();
+    }
+
+    inline uint32_t marginRightUnitsValue() const { return m_MarginRightUnitsValue; }
+    void marginRightUnitsValue(uint32_t value)
+    {
+        if (m_MarginRightUnitsValue == value)
+        {
+            return;
+        }
+        m_MarginRightUnitsValue = value;
+        marginRightUnitsValueChanged();
+    }
+
+    inline uint32_t marginTopUnitsValue() const { return m_MarginTopUnitsValue; }
+    void marginTopUnitsValue(uint32_t value)
+    {
+        if (m_MarginTopUnitsValue == value)
+        {
+            return;
+        }
+        m_MarginTopUnitsValue = value;
+        marginTopUnitsValueChanged();
+    }
+
+    inline uint32_t marginBottomUnitsValue() const { return m_MarginBottomUnitsValue; }
+    void marginBottomUnitsValue(uint32_t value)
+    {
+        if (m_MarginBottomUnitsValue == value)
+        {
+            return;
+        }
+        m_MarginBottomUnitsValue = value;
+        marginBottomUnitsValueChanged();
+    }
+
+    inline uint32_t paddingLeftUnitsValue() const { return m_PaddingLeftUnitsValue; }
+    void paddingLeftUnitsValue(uint32_t value)
+    {
+        if (m_PaddingLeftUnitsValue == value)
+        {
+            return;
+        }
+        m_PaddingLeftUnitsValue = value;
+        paddingLeftUnitsValueChanged();
+    }
+
+    inline uint32_t paddingRightUnitsValue() const { return m_PaddingRightUnitsValue; }
+    void paddingRightUnitsValue(uint32_t value)
+    {
+        if (m_PaddingRightUnitsValue == value)
+        {
+            return;
+        }
+        m_PaddingRightUnitsValue = value;
+        paddingRightUnitsValueChanged();
+    }
+
+    inline uint32_t paddingTopUnitsValue() const { return m_PaddingTopUnitsValue; }
+    void paddingTopUnitsValue(uint32_t value)
+    {
+        if (m_PaddingTopUnitsValue == value)
+        {
+            return;
+        }
+        m_PaddingTopUnitsValue = value;
+        paddingTopUnitsValueChanged();
+    }
+
+    inline uint32_t paddingBottomUnitsValue() const { return m_PaddingBottomUnitsValue; }
+    void paddingBottomUnitsValue(uint32_t value)
+    {
+        if (m_PaddingBottomUnitsValue == value)
+        {
+            return;
+        }
+        m_PaddingBottomUnitsValue = value;
+        paddingBottomUnitsValueChanged();
+    }
+
+    inline uint32_t positionLeftUnitsValue() const { return m_PositionLeftUnitsValue; }
+    void positionLeftUnitsValue(uint32_t value)
+    {
+        if (m_PositionLeftUnitsValue == value)
+        {
+            return;
+        }
+        m_PositionLeftUnitsValue = value;
+        positionLeftUnitsValueChanged();
+    }
+
+    inline uint32_t positionRightUnitsValue() const { return m_PositionRightUnitsValue; }
+    void positionRightUnitsValue(uint32_t value)
+    {
+        if (m_PositionRightUnitsValue == value)
+        {
+            return;
+        }
+        m_PositionRightUnitsValue = value;
+        positionRightUnitsValueChanged();
+    }
+
+    inline uint32_t positionTopUnitsValue() const { return m_PositionTopUnitsValue; }
+    void positionTopUnitsValue(uint32_t value)
+    {
+        if (m_PositionTopUnitsValue == value)
+        {
+            return;
+        }
+        m_PositionTopUnitsValue = value;
+        positionTopUnitsValueChanged();
+    }
+
+    inline uint32_t positionBottomUnitsValue() const { return m_PositionBottomUnitsValue; }
+    void positionBottomUnitsValue(uint32_t value)
+    {
+        if (m_PositionBottomUnitsValue == value)
+        {
+            return;
+        }
+        m_PositionBottomUnitsValue = value;
+        positionBottomUnitsValueChanged();
+    }
+
+    inline uint32_t gapHorizontalUnitsValue() const { return m_GapHorizontalUnitsValue; }
+    void gapHorizontalUnitsValue(uint32_t value)
+    {
+        if (m_GapHorizontalUnitsValue == value)
+        {
+            return;
+        }
+        m_GapHorizontalUnitsValue = value;
+        gapHorizontalUnitsValueChanged();
+    }
+
+    inline uint32_t gapVerticalUnitsValue() const { return m_GapVerticalUnitsValue; }
+    void gapVerticalUnitsValue(uint32_t value)
+    {
+        if (m_GapVerticalUnitsValue == value)
+        {
+            return;
+        }
+        m_GapVerticalUnitsValue = value;
+        gapVerticalUnitsValueChanged();
+    }
+
+    inline uint32_t minWidthUnitsValue() const { return m_MinWidthUnitsValue; }
+    void minWidthUnitsValue(uint32_t value)
+    {
+        if (m_MinWidthUnitsValue == value)
+        {
+            return;
+        }
+        m_MinWidthUnitsValue = value;
+        minWidthUnitsValueChanged();
+    }
+
+    inline uint32_t minHeightUnitsValue() const { return m_MinHeightUnitsValue; }
+    void minHeightUnitsValue(uint32_t value)
+    {
+        if (m_MinHeightUnitsValue == value)
+        {
+            return;
+        }
+        m_MinHeightUnitsValue = value;
+        minHeightUnitsValueChanged();
+    }
+
+    inline uint32_t maxWidthUnitsValue() const { return m_MaxWidthUnitsValue; }
+    void maxWidthUnitsValue(uint32_t value)
+    {
+        if (m_MaxWidthUnitsValue == value)
+        {
+            return;
+        }
+        m_MaxWidthUnitsValue = value;
+        maxWidthUnitsValueChanged();
+    }
+
+    inline uint32_t maxHeightUnitsValue() const { return m_MaxHeightUnitsValue; }
+    void maxHeightUnitsValue(uint32_t value)
+    {
+        if (m_MaxHeightUnitsValue == value)
+        {
+            return;
+        }
+        m_MaxHeightUnitsValue = value;
+        maxHeightUnitsValueChanged();
+    }
+
+    inline bool linkCornerRadius() const { return m_LinkCornerRadius; }
+    void linkCornerRadius(bool value)
+    {
+        if (m_LinkCornerRadius == value)
+        {
+            return;
+        }
+        m_LinkCornerRadius = value;
+        linkCornerRadiusChanged();
+    }
+
+    inline float cornerRadiusTL() const { return m_CornerRadiusTL; }
+    void cornerRadiusTL(float value)
+    {
+        if (m_CornerRadiusTL == value)
+        {
+            return;
+        }
+        m_CornerRadiusTL = value;
+        cornerRadiusTLChanged();
+    }
+
+    inline float cornerRadiusTR() const { return m_CornerRadiusTR; }
+    void cornerRadiusTR(float value)
+    {
+        if (m_CornerRadiusTR == value)
+        {
+            return;
+        }
+        m_CornerRadiusTR = value;
+        cornerRadiusTRChanged();
+    }
+
+    inline float cornerRadiusBL() const { return m_CornerRadiusBL; }
+    void cornerRadiusBL(float value)
+    {
+        if (m_CornerRadiusBL == value)
+        {
+            return;
+        }
+        m_CornerRadiusBL = value;
+        cornerRadiusBLChanged();
+    }
+
+    inline float cornerRadiusBR() const { return m_CornerRadiusBR; }
+    void cornerRadiusBR(float value)
+    {
+        if (m_CornerRadiusBR == value)
+        {
+            return;
+        }
+        m_CornerRadiusBR = value;
+        cornerRadiusBRChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const LayoutComponentStyleBase& object)
+    {
+        m_GapHorizontal = object.m_GapHorizontal;
+        m_GapVertical = object.m_GapVertical;
+        m_MaxWidth = object.m_MaxWidth;
+        m_MaxHeight = object.m_MaxHeight;
+        m_MinWidth = object.m_MinWidth;
+        m_MinHeight = object.m_MinHeight;
+        m_BorderLeft = object.m_BorderLeft;
+        m_BorderRight = object.m_BorderRight;
+        m_BorderTop = object.m_BorderTop;
+        m_BorderBottom = object.m_BorderBottom;
+        m_MarginLeft = object.m_MarginLeft;
+        m_MarginRight = object.m_MarginRight;
+        m_MarginTop = object.m_MarginTop;
+        m_MarginBottom = object.m_MarginBottom;
+        m_PaddingLeft = object.m_PaddingLeft;
+        m_PaddingRight = object.m_PaddingRight;
+        m_PaddingTop = object.m_PaddingTop;
+        m_PaddingBottom = object.m_PaddingBottom;
+        m_PositionLeft = object.m_PositionLeft;
+        m_PositionRight = object.m_PositionRight;
+        m_PositionTop = object.m_PositionTop;
+        m_PositionBottom = object.m_PositionBottom;
+        m_Flex = object.m_Flex;
+        m_FlexGrow = object.m_FlexGrow;
+        m_FlexShrink = object.m_FlexShrink;
+        m_FlexBasis = object.m_FlexBasis;
+        m_AspectRatio = object.m_AspectRatio;
+        m_LayoutWidthScaleType = object.m_LayoutWidthScaleType;
+        m_LayoutHeightScaleType = object.m_LayoutHeightScaleType;
+        m_LayoutAlignmentType = object.m_LayoutAlignmentType;
+        m_AnimationStyleType = object.m_AnimationStyleType;
+        m_InterpolationType = object.m_InterpolationType;
+        m_InterpolatorId = object.m_InterpolatorId;
+        m_InterpolationTime = object.m_InterpolationTime;
+        m_DisplayValue = object.m_DisplayValue;
+        m_PositionTypeValue = object.m_PositionTypeValue;
+        m_FlexDirectionValue = object.m_FlexDirectionValue;
+        m_DirectionValue = object.m_DirectionValue;
+        m_AlignContentValue = object.m_AlignContentValue;
+        m_AlignItemsValue = object.m_AlignItemsValue;
+        m_AlignSelfValue = object.m_AlignSelfValue;
+        m_JustifyContentValue = object.m_JustifyContentValue;
+        m_FlexWrapValue = object.m_FlexWrapValue;
+        m_OverflowValue = object.m_OverflowValue;
+        m_IntrinsicallySizedValue = object.m_IntrinsicallySizedValue;
+        m_WidthUnitsValue = object.m_WidthUnitsValue;
+        m_HeightUnitsValue = object.m_HeightUnitsValue;
+        m_BorderLeftUnitsValue = object.m_BorderLeftUnitsValue;
+        m_BorderRightUnitsValue = object.m_BorderRightUnitsValue;
+        m_BorderTopUnitsValue = object.m_BorderTopUnitsValue;
+        m_BorderBottomUnitsValue = object.m_BorderBottomUnitsValue;
+        m_MarginLeftUnitsValue = object.m_MarginLeftUnitsValue;
+        m_MarginRightUnitsValue = object.m_MarginRightUnitsValue;
+        m_MarginTopUnitsValue = object.m_MarginTopUnitsValue;
+        m_MarginBottomUnitsValue = object.m_MarginBottomUnitsValue;
+        m_PaddingLeftUnitsValue = object.m_PaddingLeftUnitsValue;
+        m_PaddingRightUnitsValue = object.m_PaddingRightUnitsValue;
+        m_PaddingTopUnitsValue = object.m_PaddingTopUnitsValue;
+        m_PaddingBottomUnitsValue = object.m_PaddingBottomUnitsValue;
+        m_PositionLeftUnitsValue = object.m_PositionLeftUnitsValue;
+        m_PositionRightUnitsValue = object.m_PositionRightUnitsValue;
+        m_PositionTopUnitsValue = object.m_PositionTopUnitsValue;
+        m_PositionBottomUnitsValue = object.m_PositionBottomUnitsValue;
+        m_GapHorizontalUnitsValue = object.m_GapHorizontalUnitsValue;
+        m_GapVerticalUnitsValue = object.m_GapVerticalUnitsValue;
+        m_MinWidthUnitsValue = object.m_MinWidthUnitsValue;
+        m_MinHeightUnitsValue = object.m_MinHeightUnitsValue;
+        m_MaxWidthUnitsValue = object.m_MaxWidthUnitsValue;
+        m_MaxHeightUnitsValue = object.m_MaxHeightUnitsValue;
+        m_LinkCornerRadius = object.m_LinkCornerRadius;
+        m_CornerRadiusTL = object.m_CornerRadiusTL;
+        m_CornerRadiusTR = object.m_CornerRadiusTR;
+        m_CornerRadiusBL = object.m_CornerRadiusBL;
+        m_CornerRadiusBR = object.m_CornerRadiusBR;
+        Component::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case gapHorizontalPropertyKey:
+                m_GapHorizontal = CoreDoubleType::deserialize(reader);
+                return true;
+            case gapVerticalPropertyKey:
+                m_GapVertical = CoreDoubleType::deserialize(reader);
+                return true;
+            case maxWidthPropertyKey:
+                m_MaxWidth = CoreDoubleType::deserialize(reader);
+                return true;
+            case maxHeightPropertyKey:
+                m_MaxHeight = CoreDoubleType::deserialize(reader);
+                return true;
+            case minWidthPropertyKey:
+                m_MinWidth = CoreDoubleType::deserialize(reader);
+                return true;
+            case minHeightPropertyKey:
+                m_MinHeight = CoreDoubleType::deserialize(reader);
+                return true;
+            case borderLeftPropertyKey:
+                m_BorderLeft = CoreDoubleType::deserialize(reader);
+                return true;
+            case borderRightPropertyKey:
+                m_BorderRight = CoreDoubleType::deserialize(reader);
+                return true;
+            case borderTopPropertyKey:
+                m_BorderTop = CoreDoubleType::deserialize(reader);
+                return true;
+            case borderBottomPropertyKey:
+                m_BorderBottom = CoreDoubleType::deserialize(reader);
+                return true;
+            case marginLeftPropertyKey:
+                m_MarginLeft = CoreDoubleType::deserialize(reader);
+                return true;
+            case marginRightPropertyKey:
+                m_MarginRight = CoreDoubleType::deserialize(reader);
+                return true;
+            case marginTopPropertyKey:
+                m_MarginTop = CoreDoubleType::deserialize(reader);
+                return true;
+            case marginBottomPropertyKey:
+                m_MarginBottom = CoreDoubleType::deserialize(reader);
+                return true;
+            case paddingLeftPropertyKey:
+                m_PaddingLeft = CoreDoubleType::deserialize(reader);
+                return true;
+            case paddingRightPropertyKey:
+                m_PaddingRight = CoreDoubleType::deserialize(reader);
+                return true;
+            case paddingTopPropertyKey:
+                m_PaddingTop = CoreDoubleType::deserialize(reader);
+                return true;
+            case paddingBottomPropertyKey:
+                m_PaddingBottom = CoreDoubleType::deserialize(reader);
+                return true;
+            case positionLeftPropertyKey:
+                m_PositionLeft = CoreDoubleType::deserialize(reader);
+                return true;
+            case positionRightPropertyKey:
+                m_PositionRight = CoreDoubleType::deserialize(reader);
+                return true;
+            case positionTopPropertyKey:
+                m_PositionTop = CoreDoubleType::deserialize(reader);
+                return true;
+            case positionBottomPropertyKey:
+                m_PositionBottom = CoreDoubleType::deserialize(reader);
+                return true;
+            case flexPropertyKey:
+                m_Flex = CoreDoubleType::deserialize(reader);
+                return true;
+            case flexGrowPropertyKey:
+                m_FlexGrow = CoreDoubleType::deserialize(reader);
+                return true;
+            case flexShrinkPropertyKey:
+                m_FlexShrink = CoreDoubleType::deserialize(reader);
+                return true;
+            case flexBasisPropertyKey:
+                m_FlexBasis = CoreDoubleType::deserialize(reader);
+                return true;
+            case aspectRatioPropertyKey:
+                m_AspectRatio = CoreDoubleType::deserialize(reader);
+                return true;
+            case layoutWidthScaleTypePropertyKey:
+                m_LayoutWidthScaleType = CoreUintType::deserialize(reader);
+                return true;
+            case layoutHeightScaleTypePropertyKey:
+                m_LayoutHeightScaleType = CoreUintType::deserialize(reader);
+                return true;
+            case layoutAlignmentTypePropertyKey:
+                m_LayoutAlignmentType = CoreUintType::deserialize(reader);
+                return true;
+            case animationStyleTypePropertyKey:
+                m_AnimationStyleType = CoreUintType::deserialize(reader);
+                return true;
+            case interpolationTypePropertyKey:
+                m_InterpolationType = CoreUintType::deserialize(reader);
+                return true;
+            case interpolatorIdPropertyKey:
+                m_InterpolatorId = CoreUintType::deserialize(reader);
+                return true;
+            case interpolationTimePropertyKey:
+                m_InterpolationTime = CoreDoubleType::deserialize(reader);
+                return true;
+            case displayValuePropertyKey:
+                m_DisplayValue = CoreUintType::deserialize(reader);
+                return true;
+            case positionTypeValuePropertyKey:
+                m_PositionTypeValue = CoreUintType::deserialize(reader);
+                return true;
+            case flexDirectionValuePropertyKey:
+                m_FlexDirectionValue = CoreUintType::deserialize(reader);
+                return true;
+            case directionValuePropertyKey:
+                m_DirectionValue = CoreUintType::deserialize(reader);
+                return true;
+            case alignContentValuePropertyKey:
+                m_AlignContentValue = CoreUintType::deserialize(reader);
+                return true;
+            case alignItemsValuePropertyKey:
+                m_AlignItemsValue = CoreUintType::deserialize(reader);
+                return true;
+            case alignSelfValuePropertyKey:
+                m_AlignSelfValue = CoreUintType::deserialize(reader);
+                return true;
+            case justifyContentValuePropertyKey:
+                m_JustifyContentValue = CoreUintType::deserialize(reader);
+                return true;
+            case flexWrapValuePropertyKey:
+                m_FlexWrapValue = CoreUintType::deserialize(reader);
+                return true;
+            case overflowValuePropertyKey:
+                m_OverflowValue = CoreUintType::deserialize(reader);
+                return true;
+            case intrinsicallySizedValuePropertyKey:
+                m_IntrinsicallySizedValue = CoreBoolType::deserialize(reader);
+                return true;
+            case widthUnitsValuePropertyKey:
+                m_WidthUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case heightUnitsValuePropertyKey:
+                m_HeightUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case borderLeftUnitsValuePropertyKey:
+                m_BorderLeftUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case borderRightUnitsValuePropertyKey:
+                m_BorderRightUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case borderTopUnitsValuePropertyKey:
+                m_BorderTopUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case borderBottomUnitsValuePropertyKey:
+                m_BorderBottomUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case marginLeftUnitsValuePropertyKey:
+                m_MarginLeftUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case marginRightUnitsValuePropertyKey:
+                m_MarginRightUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case marginTopUnitsValuePropertyKey:
+                m_MarginTopUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case marginBottomUnitsValuePropertyKey:
+                m_MarginBottomUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case paddingLeftUnitsValuePropertyKey:
+                m_PaddingLeftUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case paddingRightUnitsValuePropertyKey:
+                m_PaddingRightUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case paddingTopUnitsValuePropertyKey:
+                m_PaddingTopUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case paddingBottomUnitsValuePropertyKey:
+                m_PaddingBottomUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case positionLeftUnitsValuePropertyKey:
+                m_PositionLeftUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case positionRightUnitsValuePropertyKey:
+                m_PositionRightUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case positionTopUnitsValuePropertyKey:
+                m_PositionTopUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case positionBottomUnitsValuePropertyKey:
+                m_PositionBottomUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case gapHorizontalUnitsValuePropertyKey:
+                m_GapHorizontalUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case gapVerticalUnitsValuePropertyKey:
+                m_GapVerticalUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case minWidthUnitsValuePropertyKey:
+                m_MinWidthUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case minHeightUnitsValuePropertyKey:
+                m_MinHeightUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case maxWidthUnitsValuePropertyKey:
+                m_MaxWidthUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case maxHeightUnitsValuePropertyKey:
+                m_MaxHeightUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case linkCornerRadiusPropertyKey:
+                m_LinkCornerRadius = CoreBoolType::deserialize(reader);
+                return true;
+            case cornerRadiusTLPropertyKey:
+                m_CornerRadiusTL = CoreDoubleType::deserialize(reader);
+                return true;
+            case cornerRadiusTRPropertyKey:
+                m_CornerRadiusTR = CoreDoubleType::deserialize(reader);
+                return true;
+            case cornerRadiusBLPropertyKey:
+                m_CornerRadiusBL = CoreDoubleType::deserialize(reader);
+                return true;
+            case cornerRadiusBRPropertyKey:
+                m_CornerRadiusBR = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return Component::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void gapHorizontalChanged() {}
+    virtual void gapVerticalChanged() {}
+    virtual void maxWidthChanged() {}
+    virtual void maxHeightChanged() {}
+    virtual void minWidthChanged() {}
+    virtual void minHeightChanged() {}
+    virtual void borderLeftChanged() {}
+    virtual void borderRightChanged() {}
+    virtual void borderTopChanged() {}
+    virtual void borderBottomChanged() {}
+    virtual void marginLeftChanged() {}
+    virtual void marginRightChanged() {}
+    virtual void marginTopChanged() {}
+    virtual void marginBottomChanged() {}
+    virtual void paddingLeftChanged() {}
+    virtual void paddingRightChanged() {}
+    virtual void paddingTopChanged() {}
+    virtual void paddingBottomChanged() {}
+    virtual void positionLeftChanged() {}
+    virtual void positionRightChanged() {}
+    virtual void positionTopChanged() {}
+    virtual void positionBottomChanged() {}
+    virtual void flexChanged() {}
+    virtual void flexGrowChanged() {}
+    virtual void flexShrinkChanged() {}
+    virtual void flexBasisChanged() {}
+    virtual void aspectRatioChanged() {}
+    virtual void layoutWidthScaleTypeChanged() {}
+    virtual void layoutHeightScaleTypeChanged() {}
+    virtual void layoutAlignmentTypeChanged() {}
+    virtual void animationStyleTypeChanged() {}
+    virtual void interpolationTypeChanged() {}
+    virtual void interpolatorIdChanged() {}
+    virtual void interpolationTimeChanged() {}
+    virtual void displayValueChanged() {}
+    virtual void positionTypeValueChanged() {}
+    virtual void flexDirectionValueChanged() {}
+    virtual void directionValueChanged() {}
+    virtual void alignContentValueChanged() {}
+    virtual void alignItemsValueChanged() {}
+    virtual void alignSelfValueChanged() {}
+    virtual void justifyContentValueChanged() {}
+    virtual void flexWrapValueChanged() {}
+    virtual void overflowValueChanged() {}
+    virtual void intrinsicallySizedValueChanged() {}
+    virtual void widthUnitsValueChanged() {}
+    virtual void heightUnitsValueChanged() {}
+    virtual void borderLeftUnitsValueChanged() {}
+    virtual void borderRightUnitsValueChanged() {}
+    virtual void borderTopUnitsValueChanged() {}
+    virtual void borderBottomUnitsValueChanged() {}
+    virtual void marginLeftUnitsValueChanged() {}
+    virtual void marginRightUnitsValueChanged() {}
+    virtual void marginTopUnitsValueChanged() {}
+    virtual void marginBottomUnitsValueChanged() {}
+    virtual void paddingLeftUnitsValueChanged() {}
+    virtual void paddingRightUnitsValueChanged() {}
+    virtual void paddingTopUnitsValueChanged() {}
+    virtual void paddingBottomUnitsValueChanged() {}
+    virtual void positionLeftUnitsValueChanged() {}
+    virtual void positionRightUnitsValueChanged() {}
+    virtual void positionTopUnitsValueChanged() {}
+    virtual void positionBottomUnitsValueChanged() {}
+    virtual void gapHorizontalUnitsValueChanged() {}
+    virtual void gapVerticalUnitsValueChanged() {}
+    virtual void minWidthUnitsValueChanged() {}
+    virtual void minHeightUnitsValueChanged() {}
+    virtual void maxWidthUnitsValueChanged() {}
+    virtual void maxHeightUnitsValueChanged() {}
+    virtual void linkCornerRadiusChanged() {}
+    virtual void cornerRadiusTLChanged() {}
+    virtual void cornerRadiusTRChanged() {}
+    virtual void cornerRadiusBLChanged() {}
+    virtual void cornerRadiusBRChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/layout/n_slicer_base.hpp b/include/rive/generated/layout/n_slicer_base.hpp
new file mode 100644
index 0000000..9ef88e9
--- /dev/null
+++ b/include/rive/generated/layout/n_slicer_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_N_SLICER_BASE_HPP_
+#define _RIVE_N_SLICER_BASE_HPP_
+#include "rive/container_component.hpp"
+namespace rive
+{
+class NSlicerBase : public ContainerComponent
+{
+protected:
+    typedef ContainerComponent Super;
+
+public:
+    static const uint16_t typeKey = 493;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case NSlicerBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/layout/n_slicer_tile_mode_base.hpp b/include/rive/generated/layout/n_slicer_tile_mode_base.hpp
new file mode 100644
index 0000000..c3673f0
--- /dev/null
+++ b/include/rive/generated/layout/n_slicer_tile_mode_base.hpp
@@ -0,0 +1,89 @@
+#ifndef _RIVE_N_SLICER_TILE_MODE_BASE_HPP_
+#define _RIVE_N_SLICER_TILE_MODE_BASE_HPP_
+#include "rive/component.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class NSlicerTileModeBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 491;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case NSlicerTileModeBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t patchIndexPropertyKey = 672;
+    static const uint16_t stylePropertyKey = 673;
+
+private:
+    uint32_t m_PatchIndex = 0;
+    uint32_t m_Style = 0;
+
+public:
+    inline uint32_t patchIndex() const { return m_PatchIndex; }
+    void patchIndex(uint32_t value)
+    {
+        if (m_PatchIndex == value)
+        {
+            return;
+        }
+        m_PatchIndex = value;
+        patchIndexChanged();
+    }
+
+    inline uint32_t style() const { return m_Style; }
+    void style(uint32_t value)
+    {
+        if (m_Style == value)
+        {
+            return;
+        }
+        m_Style = value;
+        styleChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const NSlicerTileModeBase& object)
+    {
+        m_PatchIndex = object.m_PatchIndex;
+        m_Style = object.m_Style;
+        Component::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case patchIndexPropertyKey:
+                m_PatchIndex = CoreUintType::deserialize(reader);
+                return true;
+            case stylePropertyKey:
+                m_Style = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return Component::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void patchIndexChanged() {}
+    virtual void styleChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/layout_component_base.hpp b/include/rive/generated/layout_component_base.hpp
new file mode 100644
index 0000000..680f031
--- /dev/null
+++ b/include/rive/generated/layout_component_base.hpp
@@ -0,0 +1,132 @@
+#ifndef _RIVE_LAYOUT_COMPONENT_BASE_HPP_
+#define _RIVE_LAYOUT_COMPONENT_BASE_HPP_
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/drawable.hpp"
+namespace rive
+{
+class LayoutComponentBase : public Drawable
+{
+protected:
+    typedef Drawable Super;
+
+public:
+    static const uint16_t typeKey = 409;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case LayoutComponentBase::typeKey:
+            case DrawableBase::typeKey:
+            case NodeBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t clipPropertyKey = 196;
+    static const uint16_t widthPropertyKey = 7;
+    static const uint16_t heightPropertyKey = 8;
+    static const uint16_t styleIdPropertyKey = 494;
+
+private:
+    bool m_Clip = true;
+    float m_Width = 0.0f;
+    float m_Height = 0.0f;
+    uint32_t m_StyleId = -1;
+
+public:
+    inline bool clip() const { return m_Clip; }
+    void clip(bool value)
+    {
+        if (m_Clip == value)
+        {
+            return;
+        }
+        m_Clip = value;
+        clipChanged();
+    }
+
+    inline float width() const { return m_Width; }
+    void width(float value)
+    {
+        if (m_Width == value)
+        {
+            return;
+        }
+        m_Width = value;
+        widthChanged();
+    }
+
+    inline float height() const { return m_Height; }
+    void height(float value)
+    {
+        if (m_Height == value)
+        {
+            return;
+        }
+        m_Height = value;
+        heightChanged();
+    }
+
+    inline uint32_t styleId() const { return m_StyleId; }
+    void styleId(uint32_t value)
+    {
+        if (m_StyleId == value)
+        {
+            return;
+        }
+        m_StyleId = value;
+        styleIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const LayoutComponentBase& object)
+    {
+        m_Clip = object.m_Clip;
+        m_Width = object.m_Width;
+        m_Height = object.m_Height;
+        m_StyleId = object.m_StyleId;
+        Drawable::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case clipPropertyKey:
+                m_Clip = CoreBoolType::deserialize(reader);
+                return true;
+            case widthPropertyKey:
+                m_Width = CoreDoubleType::deserialize(reader);
+                return true;
+            case heightPropertyKey:
+                m_Height = CoreDoubleType::deserialize(reader);
+                return true;
+            case styleIdPropertyKey:
+                m_StyleId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return Drawable::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void clipChanged() {}
+    virtual void widthChanged() {}
+    virtual void heightChanged() {}
+    virtual void styleIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/nested_animation_base.hpp b/include/rive/generated/nested_animation_base.hpp
new file mode 100644
index 0000000..74ff2d7
--- /dev/null
+++ b/include/rive/generated/nested_animation_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_NESTED_ANIMATION_BASE_HPP_
+#define _RIVE_NESTED_ANIMATION_BASE_HPP_
+#include "rive/container_component.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class NestedAnimationBase : public ContainerComponent
+{
+protected:
+    typedef ContainerComponent Super;
+
+public:
+    static const uint16_t typeKey = 93;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case NestedAnimationBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t animationIdPropertyKey = 198;
+
+private:
+    uint32_t m_AnimationId = -1;
+
+public:
+    inline uint32_t animationId() const { return m_AnimationId; }
+    void animationId(uint32_t value)
+    {
+        if (m_AnimationId == value)
+        {
+            return;
+        }
+        m_AnimationId = value;
+        animationIdChanged();
+    }
+
+    void copy(const NestedAnimationBase& object)
+    {
+        m_AnimationId = object.m_AnimationId;
+        ContainerComponent::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case animationIdPropertyKey:
+                m_AnimationId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return ContainerComponent::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void animationIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/nested_artboard_base.hpp b/include/rive/generated/nested_artboard_base.hpp
new file mode 100644
index 0000000..a0c203a
--- /dev/null
+++ b/include/rive/generated/nested_artboard_base.hpp
@@ -0,0 +1,87 @@
+#ifndef _RIVE_NESTED_ARTBOARD_BASE_HPP_
+#define _RIVE_NESTED_ARTBOARD_BASE_HPP_
+#include "rive/core/field_types/core_bytes_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/drawable.hpp"
+#include "rive/span.hpp"
+namespace rive
+{
+class NestedArtboardBase : public Drawable
+{
+protected:
+    typedef Drawable Super;
+
+public:
+    static const uint16_t typeKey = 92;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case NestedArtboardBase::typeKey:
+            case DrawableBase::typeKey:
+            case NodeBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t artboardIdPropertyKey = 197;
+    static const uint16_t dataBindPathIdsPropertyKey = 582;
+
+private:
+    uint32_t m_ArtboardId = -1;
+
+public:
+    inline uint32_t artboardId() const { return m_ArtboardId; }
+    void artboardId(uint32_t value)
+    {
+        if (m_ArtboardId == value)
+        {
+            return;
+        }
+        m_ArtboardId = value;
+        artboardIdChanged();
+    }
+
+    virtual void decodeDataBindPathIds(Span<const uint8_t> value) = 0;
+    virtual void copyDataBindPathIds(const NestedArtboardBase& object) = 0;
+
+    Core* clone() const override;
+    void copy(const NestedArtboardBase& object)
+    {
+        m_ArtboardId = object.m_ArtboardId;
+        copyDataBindPathIds(object);
+        Drawable::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case artboardIdPropertyKey:
+                m_ArtboardId = CoreUintType::deserialize(reader);
+                return true;
+            case dataBindPathIdsPropertyKey:
+                decodeDataBindPathIds(CoreBytesType::deserialize(reader));
+                return true;
+        }
+        return Drawable::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void artboardIdChanged() {}
+    virtual void dataBindPathIdsChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/nested_artboard_layout_base.hpp b/include/rive/generated/nested_artboard_layout_base.hpp
new file mode 100644
index 0000000..8bfa18d
--- /dev/null
+++ b/include/rive/generated/nested_artboard_layout_base.hpp
@@ -0,0 +1,168 @@
+#ifndef _RIVE_NESTED_ARTBOARD_LAYOUT_BASE_HPP_
+#define _RIVE_NESTED_ARTBOARD_LAYOUT_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/nested_artboard.hpp"
+namespace rive
+{
+class NestedArtboardLayoutBase : public NestedArtboard
+{
+protected:
+    typedef NestedArtboard Super;
+
+public:
+    static const uint16_t typeKey = 452;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case NestedArtboardLayoutBase::typeKey:
+            case NestedArtboardBase::typeKey:
+            case DrawableBase::typeKey:
+            case NodeBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t instanceWidthPropertyKey = 663;
+    static const uint16_t instanceHeightPropertyKey = 664;
+    static const uint16_t instanceWidthUnitsValuePropertyKey = 665;
+    static const uint16_t instanceHeightUnitsValuePropertyKey = 666;
+    static const uint16_t instanceWidthScaleTypePropertyKey = 667;
+    static const uint16_t instanceHeightScaleTypePropertyKey = 668;
+
+private:
+    float m_InstanceWidth = -1.0f;
+    float m_InstanceHeight = -1.0f;
+    uint32_t m_InstanceWidthUnitsValue = 1;
+    uint32_t m_InstanceHeightUnitsValue = 1;
+    uint32_t m_InstanceWidthScaleType = 0;
+    uint32_t m_InstanceHeightScaleType = 0;
+
+public:
+    inline float instanceWidth() const { return m_InstanceWidth; }
+    void instanceWidth(float value)
+    {
+        if (m_InstanceWidth == value)
+        {
+            return;
+        }
+        m_InstanceWidth = value;
+        instanceWidthChanged();
+    }
+
+    inline float instanceHeight() const { return m_InstanceHeight; }
+    void instanceHeight(float value)
+    {
+        if (m_InstanceHeight == value)
+        {
+            return;
+        }
+        m_InstanceHeight = value;
+        instanceHeightChanged();
+    }
+
+    inline uint32_t instanceWidthUnitsValue() const { return m_InstanceWidthUnitsValue; }
+    void instanceWidthUnitsValue(uint32_t value)
+    {
+        if (m_InstanceWidthUnitsValue == value)
+        {
+            return;
+        }
+        m_InstanceWidthUnitsValue = value;
+        instanceWidthUnitsValueChanged();
+    }
+
+    inline uint32_t instanceHeightUnitsValue() const { return m_InstanceHeightUnitsValue; }
+    void instanceHeightUnitsValue(uint32_t value)
+    {
+        if (m_InstanceHeightUnitsValue == value)
+        {
+            return;
+        }
+        m_InstanceHeightUnitsValue = value;
+        instanceHeightUnitsValueChanged();
+    }
+
+    inline uint32_t instanceWidthScaleType() const { return m_InstanceWidthScaleType; }
+    void instanceWidthScaleType(uint32_t value)
+    {
+        if (m_InstanceWidthScaleType == value)
+        {
+            return;
+        }
+        m_InstanceWidthScaleType = value;
+        instanceWidthScaleTypeChanged();
+    }
+
+    inline uint32_t instanceHeightScaleType() const { return m_InstanceHeightScaleType; }
+    void instanceHeightScaleType(uint32_t value)
+    {
+        if (m_InstanceHeightScaleType == value)
+        {
+            return;
+        }
+        m_InstanceHeightScaleType = value;
+        instanceHeightScaleTypeChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const NestedArtboardLayoutBase& object)
+    {
+        m_InstanceWidth = object.m_InstanceWidth;
+        m_InstanceHeight = object.m_InstanceHeight;
+        m_InstanceWidthUnitsValue = object.m_InstanceWidthUnitsValue;
+        m_InstanceHeightUnitsValue = object.m_InstanceHeightUnitsValue;
+        m_InstanceWidthScaleType = object.m_InstanceWidthScaleType;
+        m_InstanceHeightScaleType = object.m_InstanceHeightScaleType;
+        NestedArtboard::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case instanceWidthPropertyKey:
+                m_InstanceWidth = CoreDoubleType::deserialize(reader);
+                return true;
+            case instanceHeightPropertyKey:
+                m_InstanceHeight = CoreDoubleType::deserialize(reader);
+                return true;
+            case instanceWidthUnitsValuePropertyKey:
+                m_InstanceWidthUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case instanceHeightUnitsValuePropertyKey:
+                m_InstanceHeightUnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case instanceWidthScaleTypePropertyKey:
+                m_InstanceWidthScaleType = CoreUintType::deserialize(reader);
+                return true;
+            case instanceHeightScaleTypePropertyKey:
+                m_InstanceHeightScaleType = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return NestedArtboard::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void instanceWidthChanged() {}
+    virtual void instanceHeightChanged() {}
+    virtual void instanceWidthUnitsValueChanged() {}
+    virtual void instanceHeightUnitsValueChanged() {}
+    virtual void instanceWidthScaleTypeChanged() {}
+    virtual void instanceHeightScaleTypeChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/nested_artboard_leaf_base.hpp b/include/rive/generated/nested_artboard_leaf_base.hpp
new file mode 100644
index 0000000..d72fa60
--- /dev/null
+++ b/include/rive/generated/nested_artboard_leaf_base.hpp
@@ -0,0 +1,114 @@
+#ifndef _RIVE_NESTED_ARTBOARD_LEAF_BASE_HPP_
+#define _RIVE_NESTED_ARTBOARD_LEAF_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/nested_artboard.hpp"
+namespace rive
+{
+class NestedArtboardLeafBase : public NestedArtboard
+{
+protected:
+    typedef NestedArtboard Super;
+
+public:
+    static const uint16_t typeKey = 451;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case NestedArtboardLeafBase::typeKey:
+            case NestedArtboardBase::typeKey:
+            case DrawableBase::typeKey:
+            case NodeBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t fitPropertyKey = 538;
+    static const uint16_t alignmentXPropertyKey = 644;
+    static const uint16_t alignmentYPropertyKey = 645;
+
+private:
+    uint32_t m_Fit = 0;
+    float m_AlignmentX = 0.0f;
+    float m_AlignmentY = 0.0f;
+
+public:
+    inline uint32_t fit() const { return m_Fit; }
+    void fit(uint32_t value)
+    {
+        if (m_Fit == value)
+        {
+            return;
+        }
+        m_Fit = value;
+        fitChanged();
+    }
+
+    inline float alignmentX() const { return m_AlignmentX; }
+    void alignmentX(float value)
+    {
+        if (m_AlignmentX == value)
+        {
+            return;
+        }
+        m_AlignmentX = value;
+        alignmentXChanged();
+    }
+
+    inline float alignmentY() const { return m_AlignmentY; }
+    void alignmentY(float value)
+    {
+        if (m_AlignmentY == value)
+        {
+            return;
+        }
+        m_AlignmentY = value;
+        alignmentYChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const NestedArtboardLeafBase& object)
+    {
+        m_Fit = object.m_Fit;
+        m_AlignmentX = object.m_AlignmentX;
+        m_AlignmentY = object.m_AlignmentY;
+        NestedArtboard::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case fitPropertyKey:
+                m_Fit = CoreUintType::deserialize(reader);
+                return true;
+            case alignmentXPropertyKey:
+                m_AlignmentX = CoreDoubleType::deserialize(reader);
+                return true;
+            case alignmentYPropertyKey:
+                m_AlignmentY = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return NestedArtboard::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void fitChanged() {}
+    virtual void alignmentXChanged() {}
+    virtual void alignmentYChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/node_base.hpp b/include/rive/generated/node_base.hpp
new file mode 100644
index 0000000..38c4f60
--- /dev/null
+++ b/include/rive/generated/node_base.hpp
@@ -0,0 +1,94 @@
+#ifndef _RIVE_NODE_BASE_HPP_
+#define _RIVE_NODE_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/transform_component.hpp"
+namespace rive
+{
+class NodeBase : public TransformComponent
+{
+protected:
+    typedef TransformComponent Super;
+
+public:
+    static const uint16_t typeKey = 2;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case NodeBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t xPropertyKey = 13;
+    static const uint16_t xArtboardPropertyKey = 9;
+    static const uint16_t yPropertyKey = 14;
+    static const uint16_t yArtboardPropertyKey = 10;
+
+private:
+    float m_X = 0.0f;
+    float m_Y = 0.0f;
+
+public:
+    inline float x() const override { return m_X; }
+    void x(float value)
+    {
+        if (m_X == value)
+        {
+            return;
+        }
+        m_X = value;
+        xChanged();
+    }
+
+    inline float y() const override { return m_Y; }
+    void y(float value)
+    {
+        if (m_Y == value)
+        {
+            return;
+        }
+        m_Y = value;
+        yChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const NodeBase& object)
+    {
+        m_X = object.m_X;
+        m_Y = object.m_Y;
+        TransformComponent::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case xPropertyKey:
+                m_X = CoreDoubleType::deserialize(reader);
+                return true;
+            case yPropertyKey:
+                m_Y = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return TransformComponent::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void xChanged() {}
+    virtual void yChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/open_url_event_base.hpp b/include/rive/generated/open_url_event_base.hpp
new file mode 100644
index 0000000..ec53ae7
--- /dev/null
+++ b/include/rive/generated/open_url_event_base.hpp
@@ -0,0 +1,93 @@
+#ifndef _RIVE_OPEN_URL_EVENT_BASE_HPP_
+#define _RIVE_OPEN_URL_EVENT_BASE_HPP_
+#include <string>
+#include "rive/core/field_types/core_string_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/event.hpp"
+namespace rive
+{
+class OpenUrlEventBase : public Event
+{
+protected:
+    typedef Event Super;
+
+public:
+    static const uint16_t typeKey = 131;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case OpenUrlEventBase::typeKey:
+            case EventBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t urlPropertyKey = 248;
+    static const uint16_t targetValuePropertyKey = 249;
+
+private:
+    std::string m_Url = "";
+    uint32_t m_TargetValue = 0;
+
+public:
+    inline const std::string& url() const { return m_Url; }
+    void url(std::string value)
+    {
+        if (m_Url == value)
+        {
+            return;
+        }
+        m_Url = value;
+        urlChanged();
+    }
+
+    inline uint32_t targetValue() const { return m_TargetValue; }
+    void targetValue(uint32_t value)
+    {
+        if (m_TargetValue == value)
+        {
+            return;
+        }
+        m_TargetValue = value;
+        targetValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const OpenUrlEventBase& object)
+    {
+        m_Url = object.m_Url;
+        m_TargetValue = object.m_TargetValue;
+        Event::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case urlPropertyKey:
+                m_Url = CoreStringType::deserialize(reader);
+                return true;
+            case targetValuePropertyKey:
+                m_TargetValue = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return Event::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void urlChanged() {}
+    virtual void targetValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/clipping_shape_base.hpp b/include/rive/generated/shapes/clipping_shape_base.hpp
new file mode 100644
index 0000000..0f9a4c5
--- /dev/null
+++ b/include/rive/generated/shapes/clipping_shape_base.hpp
@@ -0,0 +1,108 @@
+#ifndef _RIVE_CLIPPING_SHAPE_BASE_HPP_
+#define _RIVE_CLIPPING_SHAPE_BASE_HPP_
+#include "rive/component.hpp"
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class ClippingShapeBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 42;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ClippingShapeBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t sourceIdPropertyKey = 92;
+    static const uint16_t fillRulePropertyKey = 93;
+    static const uint16_t isVisiblePropertyKey = 94;
+
+private:
+    uint32_t m_SourceId = -1;
+    uint32_t m_FillRule = 0;
+    bool m_IsVisible = true;
+
+public:
+    inline uint32_t sourceId() const { return m_SourceId; }
+    void sourceId(uint32_t value)
+    {
+        if (m_SourceId == value)
+        {
+            return;
+        }
+        m_SourceId = value;
+        sourceIdChanged();
+    }
+
+    inline uint32_t fillRule() const { return m_FillRule; }
+    void fillRule(uint32_t value)
+    {
+        if (m_FillRule == value)
+        {
+            return;
+        }
+        m_FillRule = value;
+        fillRuleChanged();
+    }
+
+    inline bool isVisible() const { return m_IsVisible; }
+    void isVisible(bool value)
+    {
+        if (m_IsVisible == value)
+        {
+            return;
+        }
+        m_IsVisible = value;
+        isVisibleChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ClippingShapeBase& object)
+    {
+        m_SourceId = object.m_SourceId;
+        m_FillRule = object.m_FillRule;
+        m_IsVisible = object.m_IsVisible;
+        Component::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case sourceIdPropertyKey:
+                m_SourceId = CoreUintType::deserialize(reader);
+                return true;
+            case fillRulePropertyKey:
+                m_FillRule = CoreUintType::deserialize(reader);
+                return true;
+            case isVisiblePropertyKey:
+                m_IsVisible = CoreBoolType::deserialize(reader);
+                return true;
+        }
+        return Component::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void sourceIdChanged() {}
+    virtual void fillRuleChanged() {}
+    virtual void isVisibleChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/contour_mesh_vertex_base.hpp b/include/rive/generated/shapes/contour_mesh_vertex_base.hpp
new file mode 100644
index 0000000..c4674b9
--- /dev/null
+++ b/include/rive/generated/shapes/contour_mesh_vertex_base.hpp
@@ -0,0 +1,39 @@
+#ifndef _RIVE_CONTOUR_MESH_VERTEX_BASE_HPP_
+#define _RIVE_CONTOUR_MESH_VERTEX_BASE_HPP_
+#include "rive/shapes/mesh_vertex.hpp"
+namespace rive
+{
+class ContourMeshVertexBase : public MeshVertex
+{
+protected:
+    typedef MeshVertex Super;
+
+public:
+    static const uint16_t typeKey = 111;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ContourMeshVertexBase::typeKey:
+            case MeshVertexBase::typeKey:
+            case VertexBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/cubic_asymmetric_vertex_base.hpp b/include/rive/generated/shapes/cubic_asymmetric_vertex_base.hpp
new file mode 100644
index 0000000..e9b12cf
--- /dev/null
+++ b/include/rive/generated/shapes/cubic_asymmetric_vertex_base.hpp
@@ -0,0 +1,111 @@
+#ifndef _RIVE_CUBIC_ASYMMETRIC_VERTEX_BASE_HPP_
+#define _RIVE_CUBIC_ASYMMETRIC_VERTEX_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/shapes/cubic_vertex.hpp"
+namespace rive
+{
+class CubicAsymmetricVertexBase : public CubicVertex
+{
+protected:
+    typedef CubicVertex Super;
+
+public:
+    static const uint16_t typeKey = 34;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case CubicAsymmetricVertexBase::typeKey:
+            case CubicVertexBase::typeKey:
+            case PathVertexBase::typeKey:
+            case VertexBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t rotationPropertyKey = 79;
+    static const uint16_t inDistancePropertyKey = 80;
+    static const uint16_t outDistancePropertyKey = 81;
+
+private:
+    float m_Rotation = 0.0f;
+    float m_InDistance = 0.0f;
+    float m_OutDistance = 0.0f;
+
+public:
+    inline float rotation() const { return m_Rotation; }
+    void rotation(float value)
+    {
+        if (m_Rotation == value)
+        {
+            return;
+        }
+        m_Rotation = value;
+        rotationChanged();
+    }
+
+    inline float inDistance() const { return m_InDistance; }
+    void inDistance(float value)
+    {
+        if (m_InDistance == value)
+        {
+            return;
+        }
+        m_InDistance = value;
+        inDistanceChanged();
+    }
+
+    inline float outDistance() const { return m_OutDistance; }
+    void outDistance(float value)
+    {
+        if (m_OutDistance == value)
+        {
+            return;
+        }
+        m_OutDistance = value;
+        outDistanceChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const CubicAsymmetricVertexBase& object)
+    {
+        m_Rotation = object.m_Rotation;
+        m_InDistance = object.m_InDistance;
+        m_OutDistance = object.m_OutDistance;
+        CubicVertex::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case rotationPropertyKey:
+                m_Rotation = CoreDoubleType::deserialize(reader);
+                return true;
+            case inDistancePropertyKey:
+                m_InDistance = CoreDoubleType::deserialize(reader);
+                return true;
+            case outDistancePropertyKey:
+                m_OutDistance = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return CubicVertex::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void rotationChanged() {}
+    virtual void inDistanceChanged() {}
+    virtual void outDistanceChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/cubic_detached_vertex_base.hpp b/include/rive/generated/shapes/cubic_detached_vertex_base.hpp
new file mode 100644
index 0000000..cc0feb7
--- /dev/null
+++ b/include/rive/generated/shapes/cubic_detached_vertex_base.hpp
@@ -0,0 +1,129 @@
+#ifndef _RIVE_CUBIC_DETACHED_VERTEX_BASE_HPP_
+#define _RIVE_CUBIC_DETACHED_VERTEX_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/shapes/cubic_vertex.hpp"
+namespace rive
+{
+class CubicDetachedVertexBase : public CubicVertex
+{
+protected:
+    typedef CubicVertex Super;
+
+public:
+    static const uint16_t typeKey = 6;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case CubicDetachedVertexBase::typeKey:
+            case CubicVertexBase::typeKey:
+            case PathVertexBase::typeKey:
+            case VertexBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t inRotationPropertyKey = 84;
+    static const uint16_t inDistancePropertyKey = 85;
+    static const uint16_t outRotationPropertyKey = 86;
+    static const uint16_t outDistancePropertyKey = 87;
+
+private:
+    float m_InRotation = 0.0f;
+    float m_InDistance = 0.0f;
+    float m_OutRotation = 0.0f;
+    float m_OutDistance = 0.0f;
+
+public:
+    inline float inRotation() const { return m_InRotation; }
+    void inRotation(float value)
+    {
+        if (m_InRotation == value)
+        {
+            return;
+        }
+        m_InRotation = value;
+        inRotationChanged();
+    }
+
+    inline float inDistance() const { return m_InDistance; }
+    void inDistance(float value)
+    {
+        if (m_InDistance == value)
+        {
+            return;
+        }
+        m_InDistance = value;
+        inDistanceChanged();
+    }
+
+    inline float outRotation() const { return m_OutRotation; }
+    void outRotation(float value)
+    {
+        if (m_OutRotation == value)
+        {
+            return;
+        }
+        m_OutRotation = value;
+        outRotationChanged();
+    }
+
+    inline float outDistance() const { return m_OutDistance; }
+    void outDistance(float value)
+    {
+        if (m_OutDistance == value)
+        {
+            return;
+        }
+        m_OutDistance = value;
+        outDistanceChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const CubicDetachedVertexBase& object)
+    {
+        m_InRotation = object.m_InRotation;
+        m_InDistance = object.m_InDistance;
+        m_OutRotation = object.m_OutRotation;
+        m_OutDistance = object.m_OutDistance;
+        CubicVertex::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case inRotationPropertyKey:
+                m_InRotation = CoreDoubleType::deserialize(reader);
+                return true;
+            case inDistancePropertyKey:
+                m_InDistance = CoreDoubleType::deserialize(reader);
+                return true;
+            case outRotationPropertyKey:
+                m_OutRotation = CoreDoubleType::deserialize(reader);
+                return true;
+            case outDistancePropertyKey:
+                m_OutDistance = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return CubicVertex::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void inRotationChanged() {}
+    virtual void inDistanceChanged() {}
+    virtual void outRotationChanged() {}
+    virtual void outDistanceChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/cubic_mirrored_vertex_base.hpp b/include/rive/generated/shapes/cubic_mirrored_vertex_base.hpp
new file mode 100644
index 0000000..e419752
--- /dev/null
+++ b/include/rive/generated/shapes/cubic_mirrored_vertex_base.hpp
@@ -0,0 +1,93 @@
+#ifndef _RIVE_CUBIC_MIRRORED_VERTEX_BASE_HPP_
+#define _RIVE_CUBIC_MIRRORED_VERTEX_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/shapes/cubic_vertex.hpp"
+namespace rive
+{
+class CubicMirroredVertexBase : public CubicVertex
+{
+protected:
+    typedef CubicVertex Super;
+
+public:
+    static const uint16_t typeKey = 35;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case CubicMirroredVertexBase::typeKey:
+            case CubicVertexBase::typeKey:
+            case PathVertexBase::typeKey:
+            case VertexBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t rotationPropertyKey = 82;
+    static const uint16_t distancePropertyKey = 83;
+
+private:
+    float m_Rotation = 0.0f;
+    float m_Distance = 0.0f;
+
+public:
+    inline float rotation() const { return m_Rotation; }
+    void rotation(float value)
+    {
+        if (m_Rotation == value)
+        {
+            return;
+        }
+        m_Rotation = value;
+        rotationChanged();
+    }
+
+    inline float distance() const { return m_Distance; }
+    void distance(float value)
+    {
+        if (m_Distance == value)
+        {
+            return;
+        }
+        m_Distance = value;
+        distanceChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const CubicMirroredVertexBase& object)
+    {
+        m_Rotation = object.m_Rotation;
+        m_Distance = object.m_Distance;
+        CubicVertex::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case rotationPropertyKey:
+                m_Rotation = CoreDoubleType::deserialize(reader);
+                return true;
+            case distancePropertyKey:
+                m_Distance = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return CubicVertex::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void rotationChanged() {}
+    virtual void distanceChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/cubic_vertex_base.hpp b/include/rive/generated/shapes/cubic_vertex_base.hpp
new file mode 100644
index 0000000..4ac7241
--- /dev/null
+++ b/include/rive/generated/shapes/cubic_vertex_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_CUBIC_VERTEX_BASE_HPP_
+#define _RIVE_CUBIC_VERTEX_BASE_HPP_
+#include "rive/shapes/path_vertex.hpp"
+namespace rive
+{
+class CubicVertexBase : public PathVertex
+{
+protected:
+    typedef PathVertex Super;
+
+public:
+    static const uint16_t typeKey = 36;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case CubicVertexBase::typeKey:
+            case PathVertexBase::typeKey:
+            case VertexBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/ellipse_base.hpp b/include/rive/generated/shapes/ellipse_base.hpp
new file mode 100644
index 0000000..015e763
--- /dev/null
+++ b/include/rive/generated/shapes/ellipse_base.hpp
@@ -0,0 +1,42 @@
+#ifndef _RIVE_ELLIPSE_BASE_HPP_
+#define _RIVE_ELLIPSE_BASE_HPP_
+#include "rive/shapes/parametric_path.hpp"
+namespace rive
+{
+class EllipseBase : public ParametricPath
+{
+protected:
+    typedef ParametricPath Super;
+
+public:
+    static const uint16_t typeKey = 4;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case EllipseBase::typeKey:
+            case ParametricPathBase::typeKey:
+            case PathBase::typeKey:
+            case NodeBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/image_base.hpp b/include/rive/generated/shapes/image_base.hpp
new file mode 100644
index 0000000..7a6e4a6
--- /dev/null
+++ b/include/rive/generated/shapes/image_base.hpp
@@ -0,0 +1,113 @@
+#ifndef _RIVE_IMAGE_BASE_HPP_
+#define _RIVE_IMAGE_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/drawable.hpp"
+namespace rive
+{
+class ImageBase : public Drawable
+{
+protected:
+    typedef Drawable Super;
+
+public:
+    static const uint16_t typeKey = 100;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ImageBase::typeKey:
+            case DrawableBase::typeKey:
+            case NodeBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t assetIdPropertyKey = 206;
+    static const uint16_t originXPropertyKey = 380;
+    static const uint16_t originYPropertyKey = 381;
+
+private:
+    uint32_t m_AssetId = -1;
+    float m_OriginX = 0.5f;
+    float m_OriginY = 0.5f;
+
+public:
+    inline uint32_t assetId() const { return m_AssetId; }
+    void assetId(uint32_t value)
+    {
+        if (m_AssetId == value)
+        {
+            return;
+        }
+        m_AssetId = value;
+        assetIdChanged();
+    }
+
+    inline float originX() const { return m_OriginX; }
+    void originX(float value)
+    {
+        if (m_OriginX == value)
+        {
+            return;
+        }
+        m_OriginX = value;
+        originXChanged();
+    }
+
+    inline float originY() const { return m_OriginY; }
+    void originY(float value)
+    {
+        if (m_OriginY == value)
+        {
+            return;
+        }
+        m_OriginY = value;
+        originYChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ImageBase& object)
+    {
+        m_AssetId = object.m_AssetId;
+        m_OriginX = object.m_OriginX;
+        m_OriginY = object.m_OriginY;
+        Drawable::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case assetIdPropertyKey:
+                m_AssetId = CoreUintType::deserialize(reader);
+                return true;
+            case originXPropertyKey:
+                m_OriginX = CoreDoubleType::deserialize(reader);
+                return true;
+            case originYPropertyKey:
+                m_OriginY = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return Drawable::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void assetIdChanged() {}
+    virtual void originXChanged() {}
+    virtual void originYChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/mesh_base.hpp b/include/rive/generated/shapes/mesh_base.hpp
new file mode 100644
index 0000000..c34e88a
--- /dev/null
+++ b/include/rive/generated/shapes/mesh_base.hpp
@@ -0,0 +1,62 @@
+#ifndef _RIVE_MESH_BASE_HPP_
+#define _RIVE_MESH_BASE_HPP_
+#include "rive/container_component.hpp"
+#include "rive/core/field_types/core_bytes_type.hpp"
+#include "rive/span.hpp"
+namespace rive
+{
+class MeshBase : public ContainerComponent
+{
+protected:
+    typedef ContainerComponent Super;
+
+public:
+    static const uint16_t typeKey = 109;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case MeshBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t triangleIndexBytesPropertyKey = 223;
+
+public:
+    virtual void decodeTriangleIndexBytes(Span<const uint8_t> value) = 0;
+    virtual void copyTriangleIndexBytes(const MeshBase& object) = 0;
+
+    Core* clone() const override;
+    void copy(const MeshBase& object)
+    {
+        copyTriangleIndexBytes(object);
+        ContainerComponent::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case triangleIndexBytesPropertyKey:
+                decodeTriangleIndexBytes(CoreBytesType::deserialize(reader));
+                return true;
+        }
+        return ContainerComponent::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void triangleIndexBytesChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/mesh_vertex_base.hpp b/include/rive/generated/shapes/mesh_vertex_base.hpp
new file mode 100644
index 0000000..196350e
--- /dev/null
+++ b/include/rive/generated/shapes/mesh_vertex_base.hpp
@@ -0,0 +1,91 @@
+#ifndef _RIVE_MESH_VERTEX_BASE_HPP_
+#define _RIVE_MESH_VERTEX_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/shapes/vertex.hpp"
+namespace rive
+{
+class MeshVertexBase : public Vertex
+{
+protected:
+    typedef Vertex Super;
+
+public:
+    static const uint16_t typeKey = 108;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case MeshVertexBase::typeKey:
+            case VertexBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t uPropertyKey = 215;
+    static const uint16_t vPropertyKey = 216;
+
+private:
+    float m_U = 0.0f;
+    float m_V = 0.0f;
+
+public:
+    inline float u() const { return m_U; }
+    void u(float value)
+    {
+        if (m_U == value)
+        {
+            return;
+        }
+        m_U = value;
+        uChanged();
+    }
+
+    inline float v() const { return m_V; }
+    void v(float value)
+    {
+        if (m_V == value)
+        {
+            return;
+        }
+        m_V = value;
+        vChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const MeshVertexBase& object)
+    {
+        m_U = object.m_U;
+        m_V = object.m_V;
+        Vertex::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case uPropertyKey:
+                m_U = CoreDoubleType::deserialize(reader);
+                return true;
+            case vPropertyKey:
+                m_V = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return Vertex::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void uChanged() {}
+    virtual void vChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/paint/fill_base.hpp b/include/rive/generated/shapes/paint/fill_base.hpp
new file mode 100644
index 0000000..ea1f32e
--- /dev/null
+++ b/include/rive/generated/shapes/paint/fill_base.hpp
@@ -0,0 +1,73 @@
+#ifndef _RIVE_FILL_BASE_HPP_
+#define _RIVE_FILL_BASE_HPP_
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/shapes/paint/shape_paint.hpp"
+namespace rive
+{
+class FillBase : public ShapePaint
+{
+protected:
+    typedef ShapePaint Super;
+
+public:
+    static const uint16_t typeKey = 20;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case FillBase::typeKey:
+            case ShapePaintBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t fillRulePropertyKey = 40;
+
+private:
+    uint32_t m_FillRule = 0;
+
+public:
+    inline uint32_t fillRule() const { return m_FillRule; }
+    void fillRule(uint32_t value)
+    {
+        if (m_FillRule == value)
+        {
+            return;
+        }
+        m_FillRule = value;
+        fillRuleChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const FillBase& object)
+    {
+        m_FillRule = object.m_FillRule;
+        ShapePaint::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case fillRulePropertyKey:
+                m_FillRule = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return ShapePaint::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void fillRuleChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/paint/gradient_stop_base.hpp b/include/rive/generated/shapes/paint/gradient_stop_base.hpp
new file mode 100644
index 0000000..e33431c
--- /dev/null
+++ b/include/rive/generated/shapes/paint/gradient_stop_base.hpp
@@ -0,0 +1,90 @@
+#ifndef _RIVE_GRADIENT_STOP_BASE_HPP_
+#define _RIVE_GRADIENT_STOP_BASE_HPP_
+#include "rive/component.hpp"
+#include "rive/core/field_types/core_color_type.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class GradientStopBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 19;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case GradientStopBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t colorValuePropertyKey = 38;
+    static const uint16_t positionPropertyKey = 39;
+
+private:
+    int m_ColorValue = 0xFFFFFFFF;
+    float m_Position = 0.0f;
+
+public:
+    inline int colorValue() const { return m_ColorValue; }
+    void colorValue(int value)
+    {
+        if (m_ColorValue == value)
+        {
+            return;
+        }
+        m_ColorValue = value;
+        colorValueChanged();
+    }
+
+    inline float position() const { return m_Position; }
+    void position(float value)
+    {
+        if (m_Position == value)
+        {
+            return;
+        }
+        m_Position = value;
+        positionChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const GradientStopBase& object)
+    {
+        m_ColorValue = object.m_ColorValue;
+        m_Position = object.m_Position;
+        Component::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case colorValuePropertyKey:
+                m_ColorValue = CoreColorType::deserialize(reader);
+                return true;
+            case positionPropertyKey:
+                m_Position = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return Component::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void colorValueChanged() {}
+    virtual void positionChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/paint/linear_gradient_base.hpp b/include/rive/generated/shapes/paint/linear_gradient_base.hpp
new file mode 100644
index 0000000..95fd3ff
--- /dev/null
+++ b/include/rive/generated/shapes/paint/linear_gradient_base.hpp
@@ -0,0 +1,144 @@
+#ifndef _RIVE_LINEAR_GRADIENT_BASE_HPP_
+#define _RIVE_LINEAR_GRADIENT_BASE_HPP_
+#include "rive/container_component.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class LinearGradientBase : public ContainerComponent
+{
+protected:
+    typedef ContainerComponent Super;
+
+public:
+    static const uint16_t typeKey = 22;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case LinearGradientBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t startXPropertyKey = 42;
+    static const uint16_t startYPropertyKey = 33;
+    static const uint16_t endXPropertyKey = 34;
+    static const uint16_t endYPropertyKey = 35;
+    static const uint16_t opacityPropertyKey = 46;
+
+private:
+    float m_StartX = 0.0f;
+    float m_StartY = 0.0f;
+    float m_EndX = 0.0f;
+    float m_EndY = 0.0f;
+    float m_Opacity = 1.0f;
+
+public:
+    inline float startX() const { return m_StartX; }
+    void startX(float value)
+    {
+        if (m_StartX == value)
+        {
+            return;
+        }
+        m_StartX = value;
+        startXChanged();
+    }
+
+    inline float startY() const { return m_StartY; }
+    void startY(float value)
+    {
+        if (m_StartY == value)
+        {
+            return;
+        }
+        m_StartY = value;
+        startYChanged();
+    }
+
+    inline float endX() const { return m_EndX; }
+    void endX(float value)
+    {
+        if (m_EndX == value)
+        {
+            return;
+        }
+        m_EndX = value;
+        endXChanged();
+    }
+
+    inline float endY() const { return m_EndY; }
+    void endY(float value)
+    {
+        if (m_EndY == value)
+        {
+            return;
+        }
+        m_EndY = value;
+        endYChanged();
+    }
+
+    inline float opacity() const { return m_Opacity; }
+    void opacity(float value)
+    {
+        if (m_Opacity == value)
+        {
+            return;
+        }
+        m_Opacity = value;
+        opacityChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const LinearGradientBase& object)
+    {
+        m_StartX = object.m_StartX;
+        m_StartY = object.m_StartY;
+        m_EndX = object.m_EndX;
+        m_EndY = object.m_EndY;
+        m_Opacity = object.m_Opacity;
+        ContainerComponent::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case startXPropertyKey:
+                m_StartX = CoreDoubleType::deserialize(reader);
+                return true;
+            case startYPropertyKey:
+                m_StartY = CoreDoubleType::deserialize(reader);
+                return true;
+            case endXPropertyKey:
+                m_EndX = CoreDoubleType::deserialize(reader);
+                return true;
+            case endYPropertyKey:
+                m_EndY = CoreDoubleType::deserialize(reader);
+                return true;
+            case opacityPropertyKey:
+                m_Opacity = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return ContainerComponent::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void startXChanged() {}
+    virtual void startYChanged() {}
+    virtual void endXChanged() {}
+    virtual void endYChanged() {}
+    virtual void opacityChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/paint/radial_gradient_base.hpp b/include/rive/generated/shapes/paint/radial_gradient_base.hpp
new file mode 100644
index 0000000..aca9673
--- /dev/null
+++ b/include/rive/generated/shapes/paint/radial_gradient_base.hpp
@@ -0,0 +1,38 @@
+#ifndef _RIVE_RADIAL_GRADIENT_BASE_HPP_
+#define _RIVE_RADIAL_GRADIENT_BASE_HPP_
+#include "rive/shapes/paint/linear_gradient.hpp"
+namespace rive
+{
+class RadialGradientBase : public LinearGradient
+{
+protected:
+    typedef LinearGradient Super;
+
+public:
+    static const uint16_t typeKey = 17;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case RadialGradientBase::typeKey:
+            case LinearGradientBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/paint/shape_paint_base.hpp b/include/rive/generated/shapes/paint/shape_paint_base.hpp
new file mode 100644
index 0000000..27cdfa4
--- /dev/null
+++ b/include/rive/generated/shapes/paint/shape_paint_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_SHAPE_PAINT_BASE_HPP_
+#define _RIVE_SHAPE_PAINT_BASE_HPP_
+#include "rive/container_component.hpp"
+#include "rive/core/field_types/core_bool_type.hpp"
+namespace rive
+{
+class ShapePaintBase : public ContainerComponent
+{
+protected:
+    typedef ContainerComponent Super;
+
+public:
+    static const uint16_t typeKey = 21;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ShapePaintBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t isVisiblePropertyKey = 41;
+
+private:
+    bool m_IsVisible = true;
+
+public:
+    virtual bool isVisible() const { return m_IsVisible; }
+    void isVisible(bool value)
+    {
+        if (m_IsVisible == value)
+        {
+            return;
+        }
+        m_IsVisible = value;
+        isVisibleChanged();
+    }
+
+    void copy(const ShapePaintBase& object)
+    {
+        m_IsVisible = object.m_IsVisible;
+        ContainerComponent::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case isVisiblePropertyKey:
+                m_IsVisible = CoreBoolType::deserialize(reader);
+                return true;
+        }
+        return ContainerComponent::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void isVisibleChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/paint/solid_color_base.hpp b/include/rive/generated/shapes/paint/solid_color_base.hpp
new file mode 100644
index 0000000..2ebc9d3
--- /dev/null
+++ b/include/rive/generated/shapes/paint/solid_color_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_SOLID_COLOR_BASE_HPP_
+#define _RIVE_SOLID_COLOR_BASE_HPP_
+#include "rive/component.hpp"
+#include "rive/core/field_types/core_color_type.hpp"
+namespace rive
+{
+class SolidColorBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 18;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case SolidColorBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t colorValuePropertyKey = 37;
+
+private:
+    int m_ColorValue = 0xFF747474;
+
+public:
+    inline int colorValue() const { return m_ColorValue; }
+    void colorValue(int value)
+    {
+        if (m_ColorValue == value)
+        {
+            return;
+        }
+        m_ColorValue = value;
+        colorValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const SolidColorBase& object)
+    {
+        m_ColorValue = object.m_ColorValue;
+        Component::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case colorValuePropertyKey:
+                m_ColorValue = CoreColorType::deserialize(reader);
+                return true;
+        }
+        return Component::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void colorValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/paint/stroke_base.hpp b/include/rive/generated/shapes/paint/stroke_base.hpp
new file mode 100644
index 0000000..b23107f
--- /dev/null
+++ b/include/rive/generated/shapes/paint/stroke_base.hpp
@@ -0,0 +1,129 @@
+#ifndef _RIVE_STROKE_BASE_HPP_
+#define _RIVE_STROKE_BASE_HPP_
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/shapes/paint/shape_paint.hpp"
+namespace rive
+{
+class StrokeBase : public ShapePaint
+{
+protected:
+    typedef ShapePaint Super;
+
+public:
+    static const uint16_t typeKey = 24;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case StrokeBase::typeKey:
+            case ShapePaintBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t thicknessPropertyKey = 47;
+    static const uint16_t capPropertyKey = 48;
+    static const uint16_t joinPropertyKey = 49;
+    static const uint16_t transformAffectsStrokePropertyKey = 50;
+
+private:
+    float m_Thickness = 1.0f;
+    uint32_t m_Cap = 0;
+    uint32_t m_Join = 0;
+    bool m_TransformAffectsStroke = true;
+
+public:
+    inline float thickness() const { return m_Thickness; }
+    void thickness(float value)
+    {
+        if (m_Thickness == value)
+        {
+            return;
+        }
+        m_Thickness = value;
+        thicknessChanged();
+    }
+
+    inline uint32_t cap() const { return m_Cap; }
+    void cap(uint32_t value)
+    {
+        if (m_Cap == value)
+        {
+            return;
+        }
+        m_Cap = value;
+        capChanged();
+    }
+
+    inline uint32_t join() const { return m_Join; }
+    void join(uint32_t value)
+    {
+        if (m_Join == value)
+        {
+            return;
+        }
+        m_Join = value;
+        joinChanged();
+    }
+
+    inline bool transformAffectsStroke() const { return m_TransformAffectsStroke; }
+    void transformAffectsStroke(bool value)
+    {
+        if (m_TransformAffectsStroke == value)
+        {
+            return;
+        }
+        m_TransformAffectsStroke = value;
+        transformAffectsStrokeChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const StrokeBase& object)
+    {
+        m_Thickness = object.m_Thickness;
+        m_Cap = object.m_Cap;
+        m_Join = object.m_Join;
+        m_TransformAffectsStroke = object.m_TransformAffectsStroke;
+        ShapePaint::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case thicknessPropertyKey:
+                m_Thickness = CoreDoubleType::deserialize(reader);
+                return true;
+            case capPropertyKey:
+                m_Cap = CoreUintType::deserialize(reader);
+                return true;
+            case joinPropertyKey:
+                m_Join = CoreUintType::deserialize(reader);
+                return true;
+            case transformAffectsStrokePropertyKey:
+                m_TransformAffectsStroke = CoreBoolType::deserialize(reader);
+                return true;
+        }
+        return ShapePaint::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void thicknessChanged() {}
+    virtual void capChanged() {}
+    virtual void joinChanged() {}
+    virtual void transformAffectsStrokeChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/paint/trim_path_base.hpp b/include/rive/generated/shapes/paint/trim_path_base.hpp
new file mode 100644
index 0000000..4838269
--- /dev/null
+++ b/include/rive/generated/shapes/paint/trim_path_base.hpp
@@ -0,0 +1,126 @@
+#ifndef _RIVE_TRIM_PATH_BASE_HPP_
+#define _RIVE_TRIM_PATH_BASE_HPP_
+#include "rive/component.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class TrimPathBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 47;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TrimPathBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t startPropertyKey = 114;
+    static const uint16_t endPropertyKey = 115;
+    static const uint16_t offsetPropertyKey = 116;
+    static const uint16_t modeValuePropertyKey = 117;
+
+private:
+    float m_Start = 0.0f;
+    float m_End = 0.0f;
+    float m_Offset = 0.0f;
+    uint32_t m_ModeValue = 0;
+
+public:
+    inline float start() const { return m_Start; }
+    void start(float value)
+    {
+        if (m_Start == value)
+        {
+            return;
+        }
+        m_Start = value;
+        startChanged();
+    }
+
+    inline float end() const { return m_End; }
+    void end(float value)
+    {
+        if (m_End == value)
+        {
+            return;
+        }
+        m_End = value;
+        endChanged();
+    }
+
+    inline float offset() const { return m_Offset; }
+    void offset(float value)
+    {
+        if (m_Offset == value)
+        {
+            return;
+        }
+        m_Offset = value;
+        offsetChanged();
+    }
+
+    inline uint32_t modeValue() const { return m_ModeValue; }
+    void modeValue(uint32_t value)
+    {
+        if (m_ModeValue == value)
+        {
+            return;
+        }
+        m_ModeValue = value;
+        modeValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const TrimPathBase& object)
+    {
+        m_Start = object.m_Start;
+        m_End = object.m_End;
+        m_Offset = object.m_Offset;
+        m_ModeValue = object.m_ModeValue;
+        Component::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case startPropertyKey:
+                m_Start = CoreDoubleType::deserialize(reader);
+                return true;
+            case endPropertyKey:
+                m_End = CoreDoubleType::deserialize(reader);
+                return true;
+            case offsetPropertyKey:
+                m_Offset = CoreDoubleType::deserialize(reader);
+                return true;
+            case modeValuePropertyKey:
+                m_ModeValue = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return Component::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void startChanged() {}
+    virtual void endChanged() {}
+    virtual void offsetChanged() {}
+    virtual void modeValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/parametric_path_base.hpp b/include/rive/generated/shapes/parametric_path_base.hpp
new file mode 100644
index 0000000..2f87b87
--- /dev/null
+++ b/include/rive/generated/shapes/parametric_path_base.hpp
@@ -0,0 +1,129 @@
+#ifndef _RIVE_PARAMETRIC_PATH_BASE_HPP_
+#define _RIVE_PARAMETRIC_PATH_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/shapes/path.hpp"
+namespace rive
+{
+class ParametricPathBase : public Path
+{
+protected:
+    typedef Path Super;
+
+public:
+    static const uint16_t typeKey = 15;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ParametricPathBase::typeKey:
+            case PathBase::typeKey:
+            case NodeBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t widthPropertyKey = 20;
+    static const uint16_t heightPropertyKey = 21;
+    static const uint16_t originXPropertyKey = 123;
+    static const uint16_t originYPropertyKey = 124;
+
+private:
+    float m_Width = 0.0f;
+    float m_Height = 0.0f;
+    float m_OriginX = 0.5f;
+    float m_OriginY = 0.5f;
+
+public:
+    inline float width() const { return m_Width; }
+    void width(float value)
+    {
+        if (m_Width == value)
+        {
+            return;
+        }
+        m_Width = value;
+        widthChanged();
+    }
+
+    inline float height() const { return m_Height; }
+    void height(float value)
+    {
+        if (m_Height == value)
+        {
+            return;
+        }
+        m_Height = value;
+        heightChanged();
+    }
+
+    inline float originX() const { return m_OriginX; }
+    void originX(float value)
+    {
+        if (m_OriginX == value)
+        {
+            return;
+        }
+        m_OriginX = value;
+        originXChanged();
+    }
+
+    inline float originY() const { return m_OriginY; }
+    void originY(float value)
+    {
+        if (m_OriginY == value)
+        {
+            return;
+        }
+        m_OriginY = value;
+        originYChanged();
+    }
+
+    void copy(const ParametricPathBase& object)
+    {
+        m_Width = object.m_Width;
+        m_Height = object.m_Height;
+        m_OriginX = object.m_OriginX;
+        m_OriginY = object.m_OriginY;
+        Path::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case widthPropertyKey:
+                m_Width = CoreDoubleType::deserialize(reader);
+                return true;
+            case heightPropertyKey:
+                m_Height = CoreDoubleType::deserialize(reader);
+                return true;
+            case originXPropertyKey:
+                m_OriginX = CoreDoubleType::deserialize(reader);
+                return true;
+            case originYPropertyKey:
+                m_OriginY = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return Path::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void widthChanged() {}
+    virtual void heightChanged() {}
+    virtual void originXChanged() {}
+    virtual void originYChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/path_base.hpp b/include/rive/generated/shapes/path_base.hpp
new file mode 100644
index 0000000..c90d605
--- /dev/null
+++ b/include/rive/generated/shapes/path_base.hpp
@@ -0,0 +1,74 @@
+#ifndef _RIVE_PATH_BASE_HPP_
+#define _RIVE_PATH_BASE_HPP_
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/node.hpp"
+namespace rive
+{
+class PathBase : public Node
+{
+protected:
+    typedef Node Super;
+
+public:
+    static const uint16_t typeKey = 12;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case PathBase::typeKey:
+            case NodeBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t pathFlagsPropertyKey = 128;
+
+private:
+    uint32_t m_PathFlags = 0;
+
+public:
+    inline uint32_t pathFlags() const { return m_PathFlags; }
+    void pathFlags(uint32_t value)
+    {
+        if (m_PathFlags == value)
+        {
+            return;
+        }
+        m_PathFlags = value;
+        pathFlagsChanged();
+    }
+
+    void copy(const PathBase& object)
+    {
+        m_PathFlags = object.m_PathFlags;
+        Node::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case pathFlagsPropertyKey:
+                m_PathFlags = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return Node::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void pathFlagsChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/path_vertex_base.hpp b/include/rive/generated/shapes/path_vertex_base.hpp
new file mode 100644
index 0000000..86de3a5
--- /dev/null
+++ b/include/rive/generated/shapes/path_vertex_base.hpp
@@ -0,0 +1,36 @@
+#ifndef _RIVE_PATH_VERTEX_BASE_HPP_
+#define _RIVE_PATH_VERTEX_BASE_HPP_
+#include "rive/shapes/vertex.hpp"
+namespace rive
+{
+class PathVertexBase : public Vertex
+{
+protected:
+    typedef Vertex Super;
+
+public:
+    static const uint16_t typeKey = 14;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case PathVertexBase::typeKey:
+            case VertexBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/points_path_base.hpp b/include/rive/generated/shapes/points_path_base.hpp
new file mode 100644
index 0000000..a999514
--- /dev/null
+++ b/include/rive/generated/shapes/points_path_base.hpp
@@ -0,0 +1,76 @@
+#ifndef _RIVE_POINTS_PATH_BASE_HPP_
+#define _RIVE_POINTS_PATH_BASE_HPP_
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/shapes/path.hpp"
+namespace rive
+{
+class PointsPathBase : public Path
+{
+protected:
+    typedef Path Super;
+
+public:
+    static const uint16_t typeKey = 16;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case PointsPathBase::typeKey:
+            case PathBase::typeKey:
+            case NodeBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t isClosedPropertyKey = 32;
+
+private:
+    bool m_IsClosed = false;
+
+public:
+    inline bool isClosed() const { return m_IsClosed; }
+    void isClosed(bool value)
+    {
+        if (m_IsClosed == value)
+        {
+            return;
+        }
+        m_IsClosed = value;
+        isClosedChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const PointsPathBase& object)
+    {
+        m_IsClosed = object.m_IsClosed;
+        Path::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case isClosedPropertyKey:
+                m_IsClosed = CoreBoolType::deserialize(reader);
+                return true;
+        }
+        return Path::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void isClosedChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/polygon_base.hpp b/include/rive/generated/shapes/polygon_base.hpp
new file mode 100644
index 0000000..e71f8f5
--- /dev/null
+++ b/include/rive/generated/shapes/polygon_base.hpp
@@ -0,0 +1,96 @@
+#ifndef _RIVE_POLYGON_BASE_HPP_
+#define _RIVE_POLYGON_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/shapes/parametric_path.hpp"
+namespace rive
+{
+class PolygonBase : public ParametricPath
+{
+protected:
+    typedef ParametricPath Super;
+
+public:
+    static const uint16_t typeKey = 51;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case PolygonBase::typeKey:
+            case ParametricPathBase::typeKey:
+            case PathBase::typeKey:
+            case NodeBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t pointsPropertyKey = 125;
+    static const uint16_t cornerRadiusPropertyKey = 126;
+
+private:
+    uint32_t m_Points = 5;
+    float m_CornerRadius = 0.0f;
+
+public:
+    inline uint32_t points() const { return m_Points; }
+    void points(uint32_t value)
+    {
+        if (m_Points == value)
+        {
+            return;
+        }
+        m_Points = value;
+        pointsChanged();
+    }
+
+    inline float cornerRadius() const { return m_CornerRadius; }
+    void cornerRadius(float value)
+    {
+        if (m_CornerRadius == value)
+        {
+            return;
+        }
+        m_CornerRadius = value;
+        cornerRadiusChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const PolygonBase& object)
+    {
+        m_Points = object.m_Points;
+        m_CornerRadius = object.m_CornerRadius;
+        ParametricPath::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case pointsPropertyKey:
+                m_Points = CoreUintType::deserialize(reader);
+                return true;
+            case cornerRadiusPropertyKey:
+                m_CornerRadius = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return ParametricPath::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void pointsChanged() {}
+    virtual void cornerRadiusChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/rectangle_base.hpp b/include/rive/generated/shapes/rectangle_base.hpp
new file mode 100644
index 0000000..c85ac93
--- /dev/null
+++ b/include/rive/generated/shapes/rectangle_base.hpp
@@ -0,0 +1,150 @@
+#ifndef _RIVE_RECTANGLE_BASE_HPP_
+#define _RIVE_RECTANGLE_BASE_HPP_
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/shapes/parametric_path.hpp"
+namespace rive
+{
+class RectangleBase : public ParametricPath
+{
+protected:
+    typedef ParametricPath Super;
+
+public:
+    static const uint16_t typeKey = 7;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case RectangleBase::typeKey:
+            case ParametricPathBase::typeKey:
+            case PathBase::typeKey:
+            case NodeBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t linkCornerRadiusPropertyKey = 164;
+    static const uint16_t cornerRadiusTLPropertyKey = 31;
+    static const uint16_t cornerRadiusTRPropertyKey = 161;
+    static const uint16_t cornerRadiusBLPropertyKey = 162;
+    static const uint16_t cornerRadiusBRPropertyKey = 163;
+
+private:
+    bool m_LinkCornerRadius = true;
+    float m_CornerRadiusTL = 0.0f;
+    float m_CornerRadiusTR = 0.0f;
+    float m_CornerRadiusBL = 0.0f;
+    float m_CornerRadiusBR = 0.0f;
+
+public:
+    inline bool linkCornerRadius() const { return m_LinkCornerRadius; }
+    void linkCornerRadius(bool value)
+    {
+        if (m_LinkCornerRadius == value)
+        {
+            return;
+        }
+        m_LinkCornerRadius = value;
+        linkCornerRadiusChanged();
+    }
+
+    inline float cornerRadiusTL() const { return m_CornerRadiusTL; }
+    void cornerRadiusTL(float value)
+    {
+        if (m_CornerRadiusTL == value)
+        {
+            return;
+        }
+        m_CornerRadiusTL = value;
+        cornerRadiusTLChanged();
+    }
+
+    inline float cornerRadiusTR() const { return m_CornerRadiusTR; }
+    void cornerRadiusTR(float value)
+    {
+        if (m_CornerRadiusTR == value)
+        {
+            return;
+        }
+        m_CornerRadiusTR = value;
+        cornerRadiusTRChanged();
+    }
+
+    inline float cornerRadiusBL() const { return m_CornerRadiusBL; }
+    void cornerRadiusBL(float value)
+    {
+        if (m_CornerRadiusBL == value)
+        {
+            return;
+        }
+        m_CornerRadiusBL = value;
+        cornerRadiusBLChanged();
+    }
+
+    inline float cornerRadiusBR() const { return m_CornerRadiusBR; }
+    void cornerRadiusBR(float value)
+    {
+        if (m_CornerRadiusBR == value)
+        {
+            return;
+        }
+        m_CornerRadiusBR = value;
+        cornerRadiusBRChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const RectangleBase& object)
+    {
+        m_LinkCornerRadius = object.m_LinkCornerRadius;
+        m_CornerRadiusTL = object.m_CornerRadiusTL;
+        m_CornerRadiusTR = object.m_CornerRadiusTR;
+        m_CornerRadiusBL = object.m_CornerRadiusBL;
+        m_CornerRadiusBR = object.m_CornerRadiusBR;
+        ParametricPath::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case linkCornerRadiusPropertyKey:
+                m_LinkCornerRadius = CoreBoolType::deserialize(reader);
+                return true;
+            case cornerRadiusTLPropertyKey:
+                m_CornerRadiusTL = CoreDoubleType::deserialize(reader);
+                return true;
+            case cornerRadiusTRPropertyKey:
+                m_CornerRadiusTR = CoreDoubleType::deserialize(reader);
+                return true;
+            case cornerRadiusBLPropertyKey:
+                m_CornerRadiusBL = CoreDoubleType::deserialize(reader);
+                return true;
+            case cornerRadiusBRPropertyKey:
+                m_CornerRadiusBR = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return ParametricPath::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void linkCornerRadiusChanged() {}
+    virtual void cornerRadiusTLChanged() {}
+    virtual void cornerRadiusTRChanged() {}
+    virtual void cornerRadiusBLChanged() {}
+    virtual void cornerRadiusBRChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/shape_base.hpp b/include/rive/generated/shapes/shape_base.hpp
new file mode 100644
index 0000000..c73b220
--- /dev/null
+++ b/include/rive/generated/shapes/shape_base.hpp
@@ -0,0 +1,41 @@
+#ifndef _RIVE_SHAPE_BASE_HPP_
+#define _RIVE_SHAPE_BASE_HPP_
+#include "rive/drawable.hpp"
+namespace rive
+{
+class ShapeBase : public Drawable
+{
+protected:
+    typedef Drawable Super;
+
+public:
+    static const uint16_t typeKey = 3;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ShapeBase::typeKey:
+            case DrawableBase::typeKey:
+            case NodeBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/star_base.hpp b/include/rive/generated/shapes/star_base.hpp
new file mode 100644
index 0000000..3d95448
--- /dev/null
+++ b/include/rive/generated/shapes/star_base.hpp
@@ -0,0 +1,78 @@
+#ifndef _RIVE_STAR_BASE_HPP_
+#define _RIVE_STAR_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/shapes/polygon.hpp"
+namespace rive
+{
+class StarBase : public Polygon
+{
+protected:
+    typedef Polygon Super;
+
+public:
+    static const uint16_t typeKey = 52;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case StarBase::typeKey:
+            case PolygonBase::typeKey:
+            case ParametricPathBase::typeKey:
+            case PathBase::typeKey:
+            case NodeBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t innerRadiusPropertyKey = 127;
+
+private:
+    float m_InnerRadius = 0.5f;
+
+public:
+    inline float innerRadius() const { return m_InnerRadius; }
+    void innerRadius(float value)
+    {
+        if (m_InnerRadius == value)
+        {
+            return;
+        }
+        m_InnerRadius = value;
+        innerRadiusChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const StarBase& object)
+    {
+        m_InnerRadius = object.m_InnerRadius;
+        Polygon::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case innerRadiusPropertyKey:
+                m_InnerRadius = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return Polygon::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void innerRadiusChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/straight_vertex_base.hpp b/include/rive/generated/shapes/straight_vertex_base.hpp
new file mode 100644
index 0000000..df1f886
--- /dev/null
+++ b/include/rive/generated/shapes/straight_vertex_base.hpp
@@ -0,0 +1,74 @@
+#ifndef _RIVE_STRAIGHT_VERTEX_BASE_HPP_
+#define _RIVE_STRAIGHT_VERTEX_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/shapes/path_vertex.hpp"
+namespace rive
+{
+class StraightVertexBase : public PathVertex
+{
+protected:
+    typedef PathVertex Super;
+
+public:
+    static const uint16_t typeKey = 5;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case StraightVertexBase::typeKey:
+            case PathVertexBase::typeKey:
+            case VertexBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t radiusPropertyKey = 26;
+
+private:
+    float m_Radius = 0.0f;
+
+public:
+    inline float radius() const { return m_Radius; }
+    void radius(float value)
+    {
+        if (m_Radius == value)
+        {
+            return;
+        }
+        m_Radius = value;
+        radiusChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const StraightVertexBase& object)
+    {
+        m_Radius = object.m_Radius;
+        PathVertex::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case radiusPropertyKey:
+                m_Radius = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return PathVertex::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void radiusChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/triangle_base.hpp b/include/rive/generated/shapes/triangle_base.hpp
new file mode 100644
index 0000000..3b37b53
--- /dev/null
+++ b/include/rive/generated/shapes/triangle_base.hpp
@@ -0,0 +1,42 @@
+#ifndef _RIVE_TRIANGLE_BASE_HPP_
+#define _RIVE_TRIANGLE_BASE_HPP_
+#include "rive/shapes/parametric_path.hpp"
+namespace rive
+{
+class TriangleBase : public ParametricPath
+{
+protected:
+    typedef ParametricPath Super;
+
+public:
+    static const uint16_t typeKey = 8;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TriangleBase::typeKey:
+            case ParametricPathBase::typeKey:
+            case PathBase::typeKey:
+            case NodeBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/shapes/vertex_base.hpp b/include/rive/generated/shapes/vertex_base.hpp
new file mode 100644
index 0000000..bd872c1
--- /dev/null
+++ b/include/rive/generated/shapes/vertex_base.hpp
@@ -0,0 +1,89 @@
+#ifndef _RIVE_VERTEX_BASE_HPP_
+#define _RIVE_VERTEX_BASE_HPP_
+#include "rive/container_component.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class VertexBase : public ContainerComponent
+{
+protected:
+    typedef ContainerComponent Super;
+
+public:
+    static const uint16_t typeKey = 107;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case VertexBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t xPropertyKey = 24;
+    static const uint16_t yPropertyKey = 25;
+
+private:
+    float m_X = 0.0f;
+    float m_Y = 0.0f;
+
+public:
+    inline float x() const { return m_X; }
+    void x(float value)
+    {
+        if (m_X == value)
+        {
+            return;
+        }
+        m_X = value;
+        xChanged();
+    }
+
+    inline float y() const { return m_Y; }
+    void y(float value)
+    {
+        if (m_Y == value)
+        {
+            return;
+        }
+        m_Y = value;
+        yChanged();
+    }
+
+    void copy(const VertexBase& object)
+    {
+        m_X = object.m_X;
+        m_Y = object.m_Y;
+        ContainerComponent::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case xPropertyKey:
+                m_X = CoreDoubleType::deserialize(reader);
+                return true;
+            case yPropertyKey:
+                m_Y = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return ContainerComponent::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void xChanged() {}
+    virtual void yChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/solo_base.hpp b/include/rive/generated/solo_base.hpp
new file mode 100644
index 0000000..2923702
--- /dev/null
+++ b/include/rive/generated/solo_base.hpp
@@ -0,0 +1,75 @@
+#ifndef _RIVE_SOLO_BASE_HPP_
+#define _RIVE_SOLO_BASE_HPP_
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/node.hpp"
+namespace rive
+{
+class SoloBase : public Node
+{
+protected:
+    typedef Node Super;
+
+public:
+    static const uint16_t typeKey = 147;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case SoloBase::typeKey:
+            case NodeBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t activeComponentIdPropertyKey = 296;
+
+private:
+    uint32_t m_ActiveComponentId = 0;
+
+public:
+    inline uint32_t activeComponentId() const { return m_ActiveComponentId; }
+    void activeComponentId(uint32_t value)
+    {
+        if (m_ActiveComponentId == value)
+        {
+            return;
+        }
+        m_ActiveComponentId = value;
+        activeComponentIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const SoloBase& object)
+    {
+        m_ActiveComponentId = object.m_ActiveComponentId;
+        Node::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case activeComponentIdPropertyKey:
+                m_ActiveComponentId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return Node::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void activeComponentIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/text/text_base.hpp b/include/rive/generated/text/text_base.hpp
new file mode 100644
index 0000000..222c16c
--- /dev/null
+++ b/include/rive/generated/text/text_base.hpp
@@ -0,0 +1,221 @@
+#ifndef _RIVE_TEXT_BASE_HPP_
+#define _RIVE_TEXT_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/drawable.hpp"
+namespace rive
+{
+class TextBase : public Drawable
+{
+protected:
+    typedef Drawable Super;
+
+public:
+    static const uint16_t typeKey = 134;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TextBase::typeKey:
+            case DrawableBase::typeKey:
+            case NodeBase::typeKey:
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t alignValuePropertyKey = 281;
+    static const uint16_t sizingValuePropertyKey = 284;
+    static const uint16_t overflowValuePropertyKey = 287;
+    static const uint16_t widthPropertyKey = 285;
+    static const uint16_t heightPropertyKey = 286;
+    static const uint16_t originXPropertyKey = 366;
+    static const uint16_t originYPropertyKey = 367;
+    static const uint16_t paragraphSpacingPropertyKey = 371;
+    static const uint16_t originValuePropertyKey = 377;
+
+private:
+    uint32_t m_AlignValue = 0;
+    uint32_t m_SizingValue = 0;
+    uint32_t m_OverflowValue = 0;
+    float m_Width = 0.0f;
+    float m_Height = 0.0f;
+    float m_OriginX = 0.0f;
+    float m_OriginY = 0.0f;
+    float m_ParagraphSpacing = 0.0f;
+    uint32_t m_OriginValue = 0;
+
+public:
+    inline uint32_t alignValue() const { return m_AlignValue; }
+    void alignValue(uint32_t value)
+    {
+        if (m_AlignValue == value)
+        {
+            return;
+        }
+        m_AlignValue = value;
+        alignValueChanged();
+    }
+
+    inline uint32_t sizingValue() const { return m_SizingValue; }
+    void sizingValue(uint32_t value)
+    {
+        if (m_SizingValue == value)
+        {
+            return;
+        }
+        m_SizingValue = value;
+        sizingValueChanged();
+    }
+
+    inline uint32_t overflowValue() const { return m_OverflowValue; }
+    void overflowValue(uint32_t value)
+    {
+        if (m_OverflowValue == value)
+        {
+            return;
+        }
+        m_OverflowValue = value;
+        overflowValueChanged();
+    }
+
+    inline float width() const { return m_Width; }
+    void width(float value)
+    {
+        if (m_Width == value)
+        {
+            return;
+        }
+        m_Width = value;
+        widthChanged();
+    }
+
+    inline float height() const { return m_Height; }
+    void height(float value)
+    {
+        if (m_Height == value)
+        {
+            return;
+        }
+        m_Height = value;
+        heightChanged();
+    }
+
+    inline float originX() const { return m_OriginX; }
+    void originX(float value)
+    {
+        if (m_OriginX == value)
+        {
+            return;
+        }
+        m_OriginX = value;
+        originXChanged();
+    }
+
+    inline float originY() const { return m_OriginY; }
+    void originY(float value)
+    {
+        if (m_OriginY == value)
+        {
+            return;
+        }
+        m_OriginY = value;
+        originYChanged();
+    }
+
+    inline float paragraphSpacing() const { return m_ParagraphSpacing; }
+    void paragraphSpacing(float value)
+    {
+        if (m_ParagraphSpacing == value)
+        {
+            return;
+        }
+        m_ParagraphSpacing = value;
+        paragraphSpacingChanged();
+    }
+
+    inline uint32_t originValue() const { return m_OriginValue; }
+    void originValue(uint32_t value)
+    {
+        if (m_OriginValue == value)
+        {
+            return;
+        }
+        m_OriginValue = value;
+        originValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const TextBase& object)
+    {
+        m_AlignValue = object.m_AlignValue;
+        m_SizingValue = object.m_SizingValue;
+        m_OverflowValue = object.m_OverflowValue;
+        m_Width = object.m_Width;
+        m_Height = object.m_Height;
+        m_OriginX = object.m_OriginX;
+        m_OriginY = object.m_OriginY;
+        m_ParagraphSpacing = object.m_ParagraphSpacing;
+        m_OriginValue = object.m_OriginValue;
+        Drawable::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case alignValuePropertyKey:
+                m_AlignValue = CoreUintType::deserialize(reader);
+                return true;
+            case sizingValuePropertyKey:
+                m_SizingValue = CoreUintType::deserialize(reader);
+                return true;
+            case overflowValuePropertyKey:
+                m_OverflowValue = CoreUintType::deserialize(reader);
+                return true;
+            case widthPropertyKey:
+                m_Width = CoreDoubleType::deserialize(reader);
+                return true;
+            case heightPropertyKey:
+                m_Height = CoreDoubleType::deserialize(reader);
+                return true;
+            case originXPropertyKey:
+                m_OriginX = CoreDoubleType::deserialize(reader);
+                return true;
+            case originYPropertyKey:
+                m_OriginY = CoreDoubleType::deserialize(reader);
+                return true;
+            case paragraphSpacingPropertyKey:
+                m_ParagraphSpacing = CoreDoubleType::deserialize(reader);
+                return true;
+            case originValuePropertyKey:
+                m_OriginValue = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return Drawable::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void alignValueChanged() {}
+    virtual void sizingValueChanged() {}
+    virtual void overflowValueChanged() {}
+    virtual void widthChanged() {}
+    virtual void heightChanged() {}
+    virtual void originXChanged() {}
+    virtual void originYChanged() {}
+    virtual void paragraphSpacingChanged() {}
+    virtual void originValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/text/text_modifier_base.hpp b/include/rive/generated/text/text_modifier_base.hpp
new file mode 100644
index 0000000..f2863c9
--- /dev/null
+++ b/include/rive/generated/text/text_modifier_base.hpp
@@ -0,0 +1,34 @@
+#ifndef _RIVE_TEXT_MODIFIER_BASE_HPP_
+#define _RIVE_TEXT_MODIFIER_BASE_HPP_
+#include "rive/component.hpp"
+namespace rive
+{
+class TextModifierBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 160;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TextModifierBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/text/text_modifier_group_base.hpp b/include/rive/generated/text/text_modifier_group_base.hpp
new file mode 100644
index 0000000..4c4df65
--- /dev/null
+++ b/include/rive/generated/text/text_modifier_group_base.hpp
@@ -0,0 +1,217 @@
+#ifndef _RIVE_TEXT_MODIFIER_GROUP_BASE_HPP_
+#define _RIVE_TEXT_MODIFIER_GROUP_BASE_HPP_
+#include "rive/container_component.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class TextModifierGroupBase : public ContainerComponent
+{
+protected:
+    typedef ContainerComponent Super;
+
+public:
+    static const uint16_t typeKey = 159;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TextModifierGroupBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t modifierFlagsPropertyKey = 335;
+    static const uint16_t originXPropertyKey = 328;
+    static const uint16_t originYPropertyKey = 329;
+    static const uint16_t opacityPropertyKey = 324;
+    static const uint16_t xPropertyKey = 322;
+    static const uint16_t yPropertyKey = 323;
+    static const uint16_t rotationPropertyKey = 332;
+    static const uint16_t scaleXPropertyKey = 330;
+    static const uint16_t scaleYPropertyKey = 331;
+
+private:
+    uint32_t m_ModifierFlags = 0;
+    float m_OriginX = 0.0f;
+    float m_OriginY = 0.0f;
+    float m_Opacity = 1.0f;
+    float m_X = 0.0f;
+    float m_Y = 0.0f;
+    float m_Rotation = 0.0f;
+    float m_ScaleX = 1.0f;
+    float m_ScaleY = 1.0f;
+
+public:
+    inline uint32_t modifierFlags() const { return m_ModifierFlags; }
+    void modifierFlags(uint32_t value)
+    {
+        if (m_ModifierFlags == value)
+        {
+            return;
+        }
+        m_ModifierFlags = value;
+        modifierFlagsChanged();
+    }
+
+    inline float originX() const { return m_OriginX; }
+    void originX(float value)
+    {
+        if (m_OriginX == value)
+        {
+            return;
+        }
+        m_OriginX = value;
+        originXChanged();
+    }
+
+    inline float originY() const { return m_OriginY; }
+    void originY(float value)
+    {
+        if (m_OriginY == value)
+        {
+            return;
+        }
+        m_OriginY = value;
+        originYChanged();
+    }
+
+    inline float opacity() const { return m_Opacity; }
+    void opacity(float value)
+    {
+        if (m_Opacity == value)
+        {
+            return;
+        }
+        m_Opacity = value;
+        opacityChanged();
+    }
+
+    inline float x() const { return m_X; }
+    void x(float value)
+    {
+        if (m_X == value)
+        {
+            return;
+        }
+        m_X = value;
+        xChanged();
+    }
+
+    inline float y() const { return m_Y; }
+    void y(float value)
+    {
+        if (m_Y == value)
+        {
+            return;
+        }
+        m_Y = value;
+        yChanged();
+    }
+
+    inline float rotation() const { return m_Rotation; }
+    void rotation(float value)
+    {
+        if (m_Rotation == value)
+        {
+            return;
+        }
+        m_Rotation = value;
+        rotationChanged();
+    }
+
+    inline float scaleX() const { return m_ScaleX; }
+    void scaleX(float value)
+    {
+        if (m_ScaleX == value)
+        {
+            return;
+        }
+        m_ScaleX = value;
+        scaleXChanged();
+    }
+
+    inline float scaleY() const { return m_ScaleY; }
+    void scaleY(float value)
+    {
+        if (m_ScaleY == value)
+        {
+            return;
+        }
+        m_ScaleY = value;
+        scaleYChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const TextModifierGroupBase& object)
+    {
+        m_ModifierFlags = object.m_ModifierFlags;
+        m_OriginX = object.m_OriginX;
+        m_OriginY = object.m_OriginY;
+        m_Opacity = object.m_Opacity;
+        m_X = object.m_X;
+        m_Y = object.m_Y;
+        m_Rotation = object.m_Rotation;
+        m_ScaleX = object.m_ScaleX;
+        m_ScaleY = object.m_ScaleY;
+        ContainerComponent::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case modifierFlagsPropertyKey:
+                m_ModifierFlags = CoreUintType::deserialize(reader);
+                return true;
+            case originXPropertyKey:
+                m_OriginX = CoreDoubleType::deserialize(reader);
+                return true;
+            case originYPropertyKey:
+                m_OriginY = CoreDoubleType::deserialize(reader);
+                return true;
+            case opacityPropertyKey:
+                m_Opacity = CoreDoubleType::deserialize(reader);
+                return true;
+            case xPropertyKey:
+                m_X = CoreDoubleType::deserialize(reader);
+                return true;
+            case yPropertyKey:
+                m_Y = CoreDoubleType::deserialize(reader);
+                return true;
+            case rotationPropertyKey:
+                m_Rotation = CoreDoubleType::deserialize(reader);
+                return true;
+            case scaleXPropertyKey:
+                m_ScaleX = CoreDoubleType::deserialize(reader);
+                return true;
+            case scaleYPropertyKey:
+                m_ScaleY = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return ContainerComponent::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void modifierFlagsChanged() {}
+    virtual void originXChanged() {}
+    virtual void originYChanged() {}
+    virtual void opacityChanged() {}
+    virtual void xChanged() {}
+    virtual void yChanged() {}
+    virtual void rotationChanged() {}
+    virtual void scaleXChanged() {}
+    virtual void scaleYChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/text/text_modifier_range_base.hpp b/include/rive/generated/text/text_modifier_range_base.hpp
new file mode 100644
index 0000000..f44ca68
--- /dev/null
+++ b/include/rive/generated/text/text_modifier_range_base.hpp
@@ -0,0 +1,254 @@
+#ifndef _RIVE_TEXT_MODIFIER_RANGE_BASE_HPP_
+#define _RIVE_TEXT_MODIFIER_RANGE_BASE_HPP_
+#include "rive/container_component.hpp"
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class TextModifierRangeBase : public ContainerComponent
+{
+protected:
+    typedef ContainerComponent Super;
+
+public:
+    static const uint16_t typeKey = 158;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TextModifierRangeBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t modifyFromPropertyKey = 327;
+    static const uint16_t modifyToPropertyKey = 336;
+    static const uint16_t strengthPropertyKey = 334;
+    static const uint16_t unitsValuePropertyKey = 316;
+    static const uint16_t typeValuePropertyKey = 325;
+    static const uint16_t modeValuePropertyKey = 326;
+    static const uint16_t clampPropertyKey = 333;
+    static const uint16_t falloffFromPropertyKey = 317;
+    static const uint16_t falloffToPropertyKey = 318;
+    static const uint16_t offsetPropertyKey = 319;
+    static const uint16_t runIdPropertyKey = 378;
+
+private:
+    float m_ModifyFrom = 0.0f;
+    float m_ModifyTo = 1.0f;
+    float m_Strength = 1.0f;
+    uint32_t m_UnitsValue = 0;
+    uint32_t m_TypeValue = 0;
+    uint32_t m_ModeValue = 0;
+    bool m_Clamp = false;
+    float m_FalloffFrom = 0.0f;
+    float m_FalloffTo = 1.0f;
+    float m_Offset = 0.0f;
+    uint32_t m_RunId = -1;
+
+public:
+    inline float modifyFrom() const { return m_ModifyFrom; }
+    void modifyFrom(float value)
+    {
+        if (m_ModifyFrom == value)
+        {
+            return;
+        }
+        m_ModifyFrom = value;
+        modifyFromChanged();
+    }
+
+    inline float modifyTo() const { return m_ModifyTo; }
+    void modifyTo(float value)
+    {
+        if (m_ModifyTo == value)
+        {
+            return;
+        }
+        m_ModifyTo = value;
+        modifyToChanged();
+    }
+
+    inline float strength() const { return m_Strength; }
+    void strength(float value)
+    {
+        if (m_Strength == value)
+        {
+            return;
+        }
+        m_Strength = value;
+        strengthChanged();
+    }
+
+    inline uint32_t unitsValue() const { return m_UnitsValue; }
+    void unitsValue(uint32_t value)
+    {
+        if (m_UnitsValue == value)
+        {
+            return;
+        }
+        m_UnitsValue = value;
+        unitsValueChanged();
+    }
+
+    inline uint32_t typeValue() const { return m_TypeValue; }
+    void typeValue(uint32_t value)
+    {
+        if (m_TypeValue == value)
+        {
+            return;
+        }
+        m_TypeValue = value;
+        typeValueChanged();
+    }
+
+    inline uint32_t modeValue() const { return m_ModeValue; }
+    void modeValue(uint32_t value)
+    {
+        if (m_ModeValue == value)
+        {
+            return;
+        }
+        m_ModeValue = value;
+        modeValueChanged();
+    }
+
+    inline bool clamp() const { return m_Clamp; }
+    void clamp(bool value)
+    {
+        if (m_Clamp == value)
+        {
+            return;
+        }
+        m_Clamp = value;
+        clampChanged();
+    }
+
+    inline float falloffFrom() const { return m_FalloffFrom; }
+    void falloffFrom(float value)
+    {
+        if (m_FalloffFrom == value)
+        {
+            return;
+        }
+        m_FalloffFrom = value;
+        falloffFromChanged();
+    }
+
+    inline float falloffTo() const { return m_FalloffTo; }
+    void falloffTo(float value)
+    {
+        if (m_FalloffTo == value)
+        {
+            return;
+        }
+        m_FalloffTo = value;
+        falloffToChanged();
+    }
+
+    inline float offset() const { return m_Offset; }
+    void offset(float value)
+    {
+        if (m_Offset == value)
+        {
+            return;
+        }
+        m_Offset = value;
+        offsetChanged();
+    }
+
+    inline uint32_t runId() const { return m_RunId; }
+    void runId(uint32_t value)
+    {
+        if (m_RunId == value)
+        {
+            return;
+        }
+        m_RunId = value;
+        runIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const TextModifierRangeBase& object)
+    {
+        m_ModifyFrom = object.m_ModifyFrom;
+        m_ModifyTo = object.m_ModifyTo;
+        m_Strength = object.m_Strength;
+        m_UnitsValue = object.m_UnitsValue;
+        m_TypeValue = object.m_TypeValue;
+        m_ModeValue = object.m_ModeValue;
+        m_Clamp = object.m_Clamp;
+        m_FalloffFrom = object.m_FalloffFrom;
+        m_FalloffTo = object.m_FalloffTo;
+        m_Offset = object.m_Offset;
+        m_RunId = object.m_RunId;
+        ContainerComponent::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case modifyFromPropertyKey:
+                m_ModifyFrom = CoreDoubleType::deserialize(reader);
+                return true;
+            case modifyToPropertyKey:
+                m_ModifyTo = CoreDoubleType::deserialize(reader);
+                return true;
+            case strengthPropertyKey:
+                m_Strength = CoreDoubleType::deserialize(reader);
+                return true;
+            case unitsValuePropertyKey:
+                m_UnitsValue = CoreUintType::deserialize(reader);
+                return true;
+            case typeValuePropertyKey:
+                m_TypeValue = CoreUintType::deserialize(reader);
+                return true;
+            case modeValuePropertyKey:
+                m_ModeValue = CoreUintType::deserialize(reader);
+                return true;
+            case clampPropertyKey:
+                m_Clamp = CoreBoolType::deserialize(reader);
+                return true;
+            case falloffFromPropertyKey:
+                m_FalloffFrom = CoreDoubleType::deserialize(reader);
+                return true;
+            case falloffToPropertyKey:
+                m_FalloffTo = CoreDoubleType::deserialize(reader);
+                return true;
+            case offsetPropertyKey:
+                m_Offset = CoreDoubleType::deserialize(reader);
+                return true;
+            case runIdPropertyKey:
+                m_RunId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return ContainerComponent::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void modifyFromChanged() {}
+    virtual void modifyToChanged() {}
+    virtual void strengthChanged() {}
+    virtual void unitsValueChanged() {}
+    virtual void typeValueChanged() {}
+    virtual void modeValueChanged() {}
+    virtual void clampChanged() {}
+    virtual void falloffFromChanged() {}
+    virtual void falloffToChanged() {}
+    virtual void offsetChanged() {}
+    virtual void runIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/text/text_shape_modifier_base.hpp b/include/rive/generated/text/text_shape_modifier_base.hpp
new file mode 100644
index 0000000..f271989
--- /dev/null
+++ b/include/rive/generated/text/text_shape_modifier_base.hpp
@@ -0,0 +1,35 @@
+#ifndef _RIVE_TEXT_SHAPE_MODIFIER_BASE_HPP_
+#define _RIVE_TEXT_SHAPE_MODIFIER_BASE_HPP_
+#include "rive/text/text_modifier.hpp"
+namespace rive
+{
+class TextShapeModifierBase : public TextModifier
+{
+protected:
+    typedef TextModifier Super;
+
+public:
+    static const uint16_t typeKey = 161;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TextShapeModifierBase::typeKey:
+            case TextModifierBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/text/text_style_axis_base.hpp b/include/rive/generated/text/text_style_axis_base.hpp
new file mode 100644
index 0000000..0b3f43b
--- /dev/null
+++ b/include/rive/generated/text/text_style_axis_base.hpp
@@ -0,0 +1,90 @@
+#ifndef _RIVE_TEXT_STYLE_AXIS_BASE_HPP_
+#define _RIVE_TEXT_STYLE_AXIS_BASE_HPP_
+#include "rive/component.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class TextStyleAxisBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 144;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TextStyleAxisBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t tagPropertyKey = 289;
+    static const uint16_t axisValuePropertyKey = 288;
+
+private:
+    uint32_t m_Tag = 0;
+    float m_AxisValue = 0.0f;
+
+public:
+    inline uint32_t tag() const { return m_Tag; }
+    void tag(uint32_t value)
+    {
+        if (m_Tag == value)
+        {
+            return;
+        }
+        m_Tag = value;
+        tagChanged();
+    }
+
+    inline float axisValue() const { return m_AxisValue; }
+    void axisValue(float value)
+    {
+        if (m_AxisValue == value)
+        {
+            return;
+        }
+        m_AxisValue = value;
+        axisValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const TextStyleAxisBase& object)
+    {
+        m_Tag = object.m_Tag;
+        m_AxisValue = object.m_AxisValue;
+        Component::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case tagPropertyKey:
+                m_Tag = CoreUintType::deserialize(reader);
+                return true;
+            case axisValuePropertyKey:
+                m_AxisValue = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return Component::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void tagChanged() {}
+    virtual void axisValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/text/text_style_base.hpp b/include/rive/generated/text/text_style_base.hpp
new file mode 100644
index 0000000..6076d91
--- /dev/null
+++ b/include/rive/generated/text/text_style_base.hpp
@@ -0,0 +1,127 @@
+#ifndef _RIVE_TEXT_STYLE_BASE_HPP_
+#define _RIVE_TEXT_STYLE_BASE_HPP_
+#include "rive/container_component.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class TextStyleBase : public ContainerComponent
+{
+protected:
+    typedef ContainerComponent Super;
+
+public:
+    static const uint16_t typeKey = 137;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TextStyleBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t fontSizePropertyKey = 274;
+    static const uint16_t lineHeightPropertyKey = 370;
+    static const uint16_t letterSpacingPropertyKey = 390;
+    static const uint16_t fontAssetIdPropertyKey = 279;
+
+private:
+    float m_FontSize = 12.0f;
+    float m_LineHeight = -1.0f;
+    float m_LetterSpacing = 0.0f;
+    uint32_t m_FontAssetId = -1;
+
+public:
+    inline float fontSize() const { return m_FontSize; }
+    void fontSize(float value)
+    {
+        if (m_FontSize == value)
+        {
+            return;
+        }
+        m_FontSize = value;
+        fontSizeChanged();
+    }
+
+    inline float lineHeight() const { return m_LineHeight; }
+    void lineHeight(float value)
+    {
+        if (m_LineHeight == value)
+        {
+            return;
+        }
+        m_LineHeight = value;
+        lineHeightChanged();
+    }
+
+    inline float letterSpacing() const { return m_LetterSpacing; }
+    void letterSpacing(float value)
+    {
+        if (m_LetterSpacing == value)
+        {
+            return;
+        }
+        m_LetterSpacing = value;
+        letterSpacingChanged();
+    }
+
+    inline uint32_t fontAssetId() const { return m_FontAssetId; }
+    void fontAssetId(uint32_t value)
+    {
+        if (m_FontAssetId == value)
+        {
+            return;
+        }
+        m_FontAssetId = value;
+        fontAssetIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const TextStyleBase& object)
+    {
+        m_FontSize = object.m_FontSize;
+        m_LineHeight = object.m_LineHeight;
+        m_LetterSpacing = object.m_LetterSpacing;
+        m_FontAssetId = object.m_FontAssetId;
+        ContainerComponent::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case fontSizePropertyKey:
+                m_FontSize = CoreDoubleType::deserialize(reader);
+                return true;
+            case lineHeightPropertyKey:
+                m_LineHeight = CoreDoubleType::deserialize(reader);
+                return true;
+            case letterSpacingPropertyKey:
+                m_LetterSpacing = CoreDoubleType::deserialize(reader);
+                return true;
+            case fontAssetIdPropertyKey:
+                m_FontAssetId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return ContainerComponent::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void fontSizeChanged() {}
+    virtual void lineHeightChanged() {}
+    virtual void letterSpacingChanged() {}
+    virtual void fontAssetIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/text/text_style_feature_base.hpp b/include/rive/generated/text/text_style_feature_base.hpp
new file mode 100644
index 0000000..a5ef69f
--- /dev/null
+++ b/include/rive/generated/text/text_style_feature_base.hpp
@@ -0,0 +1,89 @@
+#ifndef _RIVE_TEXT_STYLE_FEATURE_BASE_HPP_
+#define _RIVE_TEXT_STYLE_FEATURE_BASE_HPP_
+#include "rive/component.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class TextStyleFeatureBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 164;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TextStyleFeatureBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t tagPropertyKey = 356;
+    static const uint16_t featureValuePropertyKey = 357;
+
+private:
+    uint32_t m_Tag = 0;
+    uint32_t m_FeatureValue = 1;
+
+public:
+    inline uint32_t tag() const { return m_Tag; }
+    void tag(uint32_t value)
+    {
+        if (m_Tag == value)
+        {
+            return;
+        }
+        m_Tag = value;
+        tagChanged();
+    }
+
+    inline uint32_t featureValue() const { return m_FeatureValue; }
+    void featureValue(uint32_t value)
+    {
+        if (m_FeatureValue == value)
+        {
+            return;
+        }
+        m_FeatureValue = value;
+        featureValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const TextStyleFeatureBase& object)
+    {
+        m_Tag = object.m_Tag;
+        m_FeatureValue = object.m_FeatureValue;
+        Component::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case tagPropertyKey:
+                m_Tag = CoreUintType::deserialize(reader);
+                return true;
+            case featureValuePropertyKey:
+                m_FeatureValue = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return Component::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void tagChanged() {}
+    virtual void featureValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/text/text_value_run_base.hpp b/include/rive/generated/text/text_value_run_base.hpp
new file mode 100644
index 0000000..79ab4f7
--- /dev/null
+++ b/include/rive/generated/text/text_value_run_base.hpp
@@ -0,0 +1,91 @@
+#ifndef _RIVE_TEXT_VALUE_RUN_BASE_HPP_
+#define _RIVE_TEXT_VALUE_RUN_BASE_HPP_
+#include <string>
+#include "rive/component.hpp"
+#include "rive/core/field_types/core_string_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class TextValueRunBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 135;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TextValueRunBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t styleIdPropertyKey = 272;
+    static const uint16_t textPropertyKey = 268;
+
+private:
+    uint32_t m_StyleId = -1;
+    std::string m_Text = "";
+
+public:
+    inline uint32_t styleId() const { return m_StyleId; }
+    void styleId(uint32_t value)
+    {
+        if (m_StyleId == value)
+        {
+            return;
+        }
+        m_StyleId = value;
+        styleIdChanged();
+    }
+
+    inline const std::string& text() const { return m_Text; }
+    void text(std::string value)
+    {
+        if (m_Text == value)
+        {
+            return;
+        }
+        m_Text = value;
+        textChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const TextValueRunBase& object)
+    {
+        m_StyleId = object.m_StyleId;
+        m_Text = object.m_Text;
+        Component::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case styleIdPropertyKey:
+                m_StyleId = CoreUintType::deserialize(reader);
+                return true;
+            case textPropertyKey:
+                m_Text = CoreStringType::deserialize(reader);
+                return true;
+        }
+        return Component::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void styleIdChanged() {}
+    virtual void textChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/text/text_variation_modifier_base.hpp b/include/rive/generated/text/text_variation_modifier_base.hpp
new file mode 100644
index 0000000..9c72c8b
--- /dev/null
+++ b/include/rive/generated/text/text_variation_modifier_base.hpp
@@ -0,0 +1,92 @@
+#ifndef _RIVE_TEXT_VARIATION_MODIFIER_BASE_HPP_
+#define _RIVE_TEXT_VARIATION_MODIFIER_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/text/text_shape_modifier.hpp"
+namespace rive
+{
+class TextVariationModifierBase : public TextShapeModifier
+{
+protected:
+    typedef TextShapeModifier Super;
+
+public:
+    static const uint16_t typeKey = 162;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TextVariationModifierBase::typeKey:
+            case TextShapeModifierBase::typeKey:
+            case TextModifierBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t axisTagPropertyKey = 320;
+    static const uint16_t axisValuePropertyKey = 321;
+
+private:
+    uint32_t m_AxisTag = 0;
+    float m_AxisValue = 0.0f;
+
+public:
+    inline uint32_t axisTag() const { return m_AxisTag; }
+    void axisTag(uint32_t value)
+    {
+        if (m_AxisTag == value)
+        {
+            return;
+        }
+        m_AxisTag = value;
+        axisTagChanged();
+    }
+
+    inline float axisValue() const { return m_AxisValue; }
+    void axisValue(float value)
+    {
+        if (m_AxisValue == value)
+        {
+            return;
+        }
+        m_AxisValue = value;
+        axisValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const TextVariationModifierBase& object)
+    {
+        m_AxisTag = object.m_AxisTag;
+        m_AxisValue = object.m_AxisValue;
+        TextShapeModifier::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case axisTagPropertyKey:
+                m_AxisTag = CoreUintType::deserialize(reader);
+                return true;
+            case axisValuePropertyKey:
+                m_AxisValue = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return TextShapeModifier::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void axisTagChanged() {}
+    virtual void axisValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/transform_component_base.hpp b/include/rive/generated/transform_component_base.hpp
new file mode 100644
index 0000000..896ab06
--- /dev/null
+++ b/include/rive/generated/transform_component_base.hpp
@@ -0,0 +1,108 @@
+#ifndef _RIVE_TRANSFORM_COMPONENT_BASE_HPP_
+#define _RIVE_TRANSFORM_COMPONENT_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/world_transform_component.hpp"
+namespace rive
+{
+class TransformComponentBase : public WorldTransformComponent
+{
+protected:
+    typedef WorldTransformComponent Super;
+
+public:
+    static const uint16_t typeKey = 38;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case TransformComponentBase::typeKey:
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t rotationPropertyKey = 15;
+    static const uint16_t scaleXPropertyKey = 16;
+    static const uint16_t scaleYPropertyKey = 17;
+
+private:
+    float m_Rotation = 0.0f;
+    float m_ScaleX = 1.0f;
+    float m_ScaleY = 1.0f;
+
+public:
+    inline float rotation() const { return m_Rotation; }
+    void rotation(float value)
+    {
+        if (m_Rotation == value)
+        {
+            return;
+        }
+        m_Rotation = value;
+        rotationChanged();
+    }
+
+    inline float scaleX() const { return m_ScaleX; }
+    void scaleX(float value)
+    {
+        if (m_ScaleX == value)
+        {
+            return;
+        }
+        m_ScaleX = value;
+        scaleXChanged();
+    }
+
+    inline float scaleY() const { return m_ScaleY; }
+    void scaleY(float value)
+    {
+        if (m_ScaleY == value)
+        {
+            return;
+        }
+        m_ScaleY = value;
+        scaleYChanged();
+    }
+
+    void copy(const TransformComponentBase& object)
+    {
+        m_Rotation = object.m_Rotation;
+        m_ScaleX = object.m_ScaleX;
+        m_ScaleY = object.m_ScaleY;
+        WorldTransformComponent::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case rotationPropertyKey:
+                m_Rotation = CoreDoubleType::deserialize(reader);
+                return true;
+            case scaleXPropertyKey:
+                m_ScaleX = CoreDoubleType::deserialize(reader);
+                return true;
+            case scaleYPropertyKey:
+                m_ScaleY = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return WorldTransformComponent::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void rotationChanged() {}
+    virtual void scaleXChanged() {}
+    virtual void scaleYChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/data_enum_base.hpp b/include/rive/generated/viewmodel/data_enum_base.hpp
new file mode 100644
index 0000000..dc5860d
--- /dev/null
+++ b/include/rive/generated/viewmodel/data_enum_base.hpp
@@ -0,0 +1,38 @@
+#ifndef _RIVE_DATA_ENUM_BASE_HPP_
+#define _RIVE_DATA_ENUM_BASE_HPP_
+#include "rive/core.hpp"
+namespace rive
+{
+class DataEnumBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 438;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case DataEnumBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+    void copy(const DataEnumBase& object) {}
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override { return false; }
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/data_enum_value_base.hpp b/include/rive/generated/viewmodel/data_enum_value_base.hpp
new file mode 100644
index 0000000..53c89f9
--- /dev/null
+++ b/include/rive/generated/viewmodel/data_enum_value_base.hpp
@@ -0,0 +1,88 @@
+#ifndef _RIVE_DATA_ENUM_VALUE_BASE_HPP_
+#define _RIVE_DATA_ENUM_VALUE_BASE_HPP_
+#include <string>
+#include "rive/core.hpp"
+#include "rive/core/field_types/core_string_type.hpp"
+namespace rive
+{
+class DataEnumValueBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 445;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case DataEnumValueBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t keyPropertyKey = 578;
+    static const uint16_t valuePropertyKey = 579;
+
+private:
+    std::string m_Key = "";
+    std::string m_Value = "";
+
+public:
+    inline const std::string& key() const { return m_Key; }
+    void key(std::string value)
+    {
+        if (m_Key == value)
+        {
+            return;
+        }
+        m_Key = value;
+        keyChanged();
+    }
+
+    inline const std::string& value() const { return m_Value; }
+    void value(std::string value)
+    {
+        if (m_Value == value)
+        {
+            return;
+        }
+        m_Value = value;
+        valueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const DataEnumValueBase& object)
+    {
+        m_Key = object.m_Key;
+        m_Value = object.m_Value;
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case keyPropertyKey:
+                m_Key = CoreStringType::deserialize(reader);
+                return true;
+            case valuePropertyKey:
+                m_Value = CoreStringType::deserialize(reader);
+                return true;
+        }
+        return false;
+    }
+
+protected:
+    virtual void keyChanged() {}
+    virtual void valueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/viewmodel_base.hpp b/include/rive/generated/viewmodel/viewmodel_base.hpp
new file mode 100644
index 0000000..e9f63b2
--- /dev/null
+++ b/include/rive/generated/viewmodel/viewmodel_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_VIEW_MODEL_BASE_HPP_
+#define _RIVE_VIEW_MODEL_BASE_HPP_
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/viewmodel/viewmodel_component.hpp"
+namespace rive
+{
+class ViewModelBase : public ViewModelComponent
+{
+protected:
+    typedef ViewModelComponent Super;
+
+public:
+    static const uint16_t typeKey = 435;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ViewModelBase::typeKey:
+            case ViewModelComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t defaultInstanceIdPropertyKey = 564;
+
+private:
+    uint32_t m_DefaultInstanceId = -1;
+
+public:
+    inline uint32_t defaultInstanceId() const { return m_DefaultInstanceId; }
+    void defaultInstanceId(uint32_t value)
+    {
+        if (m_DefaultInstanceId == value)
+        {
+            return;
+        }
+        m_DefaultInstanceId = value;
+        defaultInstanceIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ViewModelBase& object)
+    {
+        m_DefaultInstanceId = object.m_DefaultInstanceId;
+        ViewModelComponent::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case defaultInstanceIdPropertyKey:
+                m_DefaultInstanceId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return ViewModelComponent::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void defaultInstanceIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/viewmodel_component_base.hpp b/include/rive/generated/viewmodel/viewmodel_component_base.hpp
new file mode 100644
index 0000000..611ebb3
--- /dev/null
+++ b/include/rive/generated/viewmodel/viewmodel_component_base.hpp
@@ -0,0 +1,67 @@
+#ifndef _RIVE_VIEW_MODEL_COMPONENT_BASE_HPP_
+#define _RIVE_VIEW_MODEL_COMPONENT_BASE_HPP_
+#include <string>
+#include "rive/core.hpp"
+#include "rive/core/field_types/core_string_type.hpp"
+namespace rive
+{
+class ViewModelComponentBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 429;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ViewModelComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t namePropertyKey = 557;
+
+private:
+    std::string m_Name = "";
+
+public:
+    inline const std::string& name() const { return m_Name; }
+    void name(std::string value)
+    {
+        if (m_Name == value)
+        {
+            return;
+        }
+        m_Name = value;
+        nameChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ViewModelComponentBase& object) { m_Name = object.m_Name; }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case namePropertyKey:
+                m_Name = CoreStringType::deserialize(reader);
+                return true;
+        }
+        return false;
+    }
+
+protected:
+    virtual void nameChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/viewmodel_instance_base.hpp b/include/rive/generated/viewmodel/viewmodel_instance_base.hpp
new file mode 100644
index 0000000..21f95aa
--- /dev/null
+++ b/include/rive/generated/viewmodel/viewmodel_instance_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_VIEW_MODEL_INSTANCE_BASE_HPP_
+#define _RIVE_VIEW_MODEL_INSTANCE_BASE_HPP_
+#include "rive/component.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class ViewModelInstanceBase : public Component
+{
+protected:
+    typedef Component Super;
+
+public:
+    static const uint16_t typeKey = 437;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ViewModelInstanceBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t viewModelIdPropertyKey = 566;
+
+private:
+    uint32_t m_ViewModelId = 0;
+
+public:
+    inline uint32_t viewModelId() const { return m_ViewModelId; }
+    void viewModelId(uint32_t value)
+    {
+        if (m_ViewModelId == value)
+        {
+            return;
+        }
+        m_ViewModelId = value;
+        viewModelIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ViewModelInstanceBase& object)
+    {
+        m_ViewModelId = object.m_ViewModelId;
+        Component::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case viewModelIdPropertyKey:
+                m_ViewModelId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return Component::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void viewModelIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/viewmodel_instance_boolean_base.hpp b/include/rive/generated/viewmodel/viewmodel_instance_boolean_base.hpp
new file mode 100644
index 0000000..5841b6c
--- /dev/null
+++ b/include/rive/generated/viewmodel/viewmodel_instance_boolean_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_VIEW_MODEL_INSTANCE_BOOLEAN_BASE_HPP_
+#define _RIVE_VIEW_MODEL_INSTANCE_BOOLEAN_BASE_HPP_
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/viewmodel/viewmodel_instance_value.hpp"
+namespace rive
+{
+class ViewModelInstanceBooleanBase : public ViewModelInstanceValue
+{
+protected:
+    typedef ViewModelInstanceValue Super;
+
+public:
+    static const uint16_t typeKey = 449;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ViewModelInstanceBooleanBase::typeKey:
+            case ViewModelInstanceValueBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t propertyValuePropertyKey = 593;
+
+private:
+    bool m_PropertyValue = false;
+
+public:
+    inline bool propertyValue() const { return m_PropertyValue; }
+    void propertyValue(bool value)
+    {
+        if (m_PropertyValue == value)
+        {
+            return;
+        }
+        m_PropertyValue = value;
+        propertyValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ViewModelInstanceBooleanBase& object)
+    {
+        m_PropertyValue = object.m_PropertyValue;
+        ViewModelInstanceValue::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case propertyValuePropertyKey:
+                m_PropertyValue = CoreBoolType::deserialize(reader);
+                return true;
+        }
+        return ViewModelInstanceValue::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void propertyValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/viewmodel_instance_color_base.hpp b/include/rive/generated/viewmodel/viewmodel_instance_color_base.hpp
new file mode 100644
index 0000000..873f602
--- /dev/null
+++ b/include/rive/generated/viewmodel/viewmodel_instance_color_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_VIEW_MODEL_INSTANCE_COLOR_BASE_HPP_
+#define _RIVE_VIEW_MODEL_INSTANCE_COLOR_BASE_HPP_
+#include "rive/core/field_types/core_color_type.hpp"
+#include "rive/viewmodel/viewmodel_instance_value.hpp"
+namespace rive
+{
+class ViewModelInstanceColorBase : public ViewModelInstanceValue
+{
+protected:
+    typedef ViewModelInstanceValue Super;
+
+public:
+    static const uint16_t typeKey = 426;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ViewModelInstanceColorBase::typeKey:
+            case ViewModelInstanceValueBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t propertyValuePropertyKey = 555;
+
+private:
+    int m_PropertyValue = 0xFF1D1D1D;
+
+public:
+    inline int propertyValue() const { return m_PropertyValue; }
+    void propertyValue(int value)
+    {
+        if (m_PropertyValue == value)
+        {
+            return;
+        }
+        m_PropertyValue = value;
+        propertyValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ViewModelInstanceColorBase& object)
+    {
+        m_PropertyValue = object.m_PropertyValue;
+        ViewModelInstanceValue::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case propertyValuePropertyKey:
+                m_PropertyValue = CoreColorType::deserialize(reader);
+                return true;
+        }
+        return ViewModelInstanceValue::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void propertyValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/viewmodel_instance_enum_base.hpp b/include/rive/generated/viewmodel/viewmodel_instance_enum_base.hpp
new file mode 100644
index 0000000..7d489b3
--- /dev/null
+++ b/include/rive/generated/viewmodel/viewmodel_instance_enum_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_VIEW_MODEL_INSTANCE_ENUM_BASE_HPP_
+#define _RIVE_VIEW_MODEL_INSTANCE_ENUM_BASE_HPP_
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/viewmodel/viewmodel_instance_value.hpp"
+namespace rive
+{
+class ViewModelInstanceEnumBase : public ViewModelInstanceValue
+{
+protected:
+    typedef ViewModelInstanceValue Super;
+
+public:
+    static const uint16_t typeKey = 432;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ViewModelInstanceEnumBase::typeKey:
+            case ViewModelInstanceValueBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t propertyValuePropertyKey = 560;
+
+private:
+    uint32_t m_PropertyValue = 0;
+
+public:
+    inline uint32_t propertyValue() const { return m_PropertyValue; }
+    void propertyValue(uint32_t value)
+    {
+        if (m_PropertyValue == value)
+        {
+            return;
+        }
+        m_PropertyValue = value;
+        propertyValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ViewModelInstanceEnumBase& object)
+    {
+        m_PropertyValue = object.m_PropertyValue;
+        ViewModelInstanceValue::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case propertyValuePropertyKey:
+                m_PropertyValue = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return ViewModelInstanceValue::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void propertyValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/viewmodel_instance_list_base.hpp b/include/rive/generated/viewmodel/viewmodel_instance_list_base.hpp
new file mode 100644
index 0000000..7a30aeb
--- /dev/null
+++ b/include/rive/generated/viewmodel/viewmodel_instance_list_base.hpp
@@ -0,0 +1,36 @@
+#ifndef _RIVE_VIEW_MODEL_INSTANCE_LIST_BASE_HPP_
+#define _RIVE_VIEW_MODEL_INSTANCE_LIST_BASE_HPP_
+#include "rive/viewmodel/viewmodel_instance_value.hpp"
+namespace rive
+{
+class ViewModelInstanceListBase : public ViewModelInstanceValue
+{
+protected:
+    typedef ViewModelInstanceValue Super;
+
+public:
+    static const uint16_t typeKey = 441;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ViewModelInstanceListBase::typeKey:
+            case ViewModelInstanceValueBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/viewmodel_instance_list_item_base.hpp b/include/rive/generated/viewmodel/viewmodel_instance_list_item_base.hpp
new file mode 100644
index 0000000..4fedbf6
--- /dev/null
+++ b/include/rive/generated/viewmodel/viewmodel_instance_list_item_base.hpp
@@ -0,0 +1,124 @@
+#ifndef _RIVE_VIEW_MODEL_INSTANCE_LIST_ITEM_BASE_HPP_
+#define _RIVE_VIEW_MODEL_INSTANCE_LIST_ITEM_BASE_HPP_
+#include "rive/core.hpp"
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class ViewModelInstanceListItemBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 427;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ViewModelInstanceListItemBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t useLinkedArtboardPropertyKey = 547;
+    static const uint16_t viewModelIdPropertyKey = 549;
+    static const uint16_t viewModelInstanceIdPropertyKey = 550;
+    static const uint16_t artboardIdPropertyKey = 551;
+
+private:
+    bool m_UseLinkedArtboard = true;
+    uint32_t m_ViewModelId = -1;
+    uint32_t m_ViewModelInstanceId = -1;
+    uint32_t m_ArtboardId = -1;
+
+public:
+    inline bool useLinkedArtboard() const { return m_UseLinkedArtboard; }
+    void useLinkedArtboard(bool value)
+    {
+        if (m_UseLinkedArtboard == value)
+        {
+            return;
+        }
+        m_UseLinkedArtboard = value;
+        useLinkedArtboardChanged();
+    }
+
+    inline uint32_t viewModelId() const { return m_ViewModelId; }
+    void viewModelId(uint32_t value)
+    {
+        if (m_ViewModelId == value)
+        {
+            return;
+        }
+        m_ViewModelId = value;
+        viewModelIdChanged();
+    }
+
+    inline uint32_t viewModelInstanceId() const { return m_ViewModelInstanceId; }
+    void viewModelInstanceId(uint32_t value)
+    {
+        if (m_ViewModelInstanceId == value)
+        {
+            return;
+        }
+        m_ViewModelInstanceId = value;
+        viewModelInstanceIdChanged();
+    }
+
+    inline uint32_t artboardId() const { return m_ArtboardId; }
+    void artboardId(uint32_t value)
+    {
+        if (m_ArtboardId == value)
+        {
+            return;
+        }
+        m_ArtboardId = value;
+        artboardIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ViewModelInstanceListItemBase& object)
+    {
+        m_UseLinkedArtboard = object.m_UseLinkedArtboard;
+        m_ViewModelId = object.m_ViewModelId;
+        m_ViewModelInstanceId = object.m_ViewModelInstanceId;
+        m_ArtboardId = object.m_ArtboardId;
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case useLinkedArtboardPropertyKey:
+                m_UseLinkedArtboard = CoreBoolType::deserialize(reader);
+                return true;
+            case viewModelIdPropertyKey:
+                m_ViewModelId = CoreUintType::deserialize(reader);
+                return true;
+            case viewModelInstanceIdPropertyKey:
+                m_ViewModelInstanceId = CoreUintType::deserialize(reader);
+                return true;
+            case artboardIdPropertyKey:
+                m_ArtboardId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return false;
+    }
+
+protected:
+    virtual void useLinkedArtboardChanged() {}
+    virtual void viewModelIdChanged() {}
+    virtual void viewModelInstanceIdChanged() {}
+    virtual void artboardIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/viewmodel_instance_number_base.hpp b/include/rive/generated/viewmodel/viewmodel_instance_number_base.hpp
new file mode 100644
index 0000000..6df85d0
--- /dev/null
+++ b/include/rive/generated/viewmodel/viewmodel_instance_number_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_VIEW_MODEL_INSTANCE_NUMBER_BASE_HPP_
+#define _RIVE_VIEW_MODEL_INSTANCE_NUMBER_BASE_HPP_
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/viewmodel/viewmodel_instance_value.hpp"
+namespace rive
+{
+class ViewModelInstanceNumberBase : public ViewModelInstanceValue
+{
+protected:
+    typedef ViewModelInstanceValue Super;
+
+public:
+    static const uint16_t typeKey = 442;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ViewModelInstanceNumberBase::typeKey:
+            case ViewModelInstanceValueBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t propertyValuePropertyKey = 575;
+
+private:
+    float m_PropertyValue = 0.0f;
+
+public:
+    inline float propertyValue() const { return m_PropertyValue; }
+    void propertyValue(float value)
+    {
+        if (m_PropertyValue == value)
+        {
+            return;
+        }
+        m_PropertyValue = value;
+        propertyValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ViewModelInstanceNumberBase& object)
+    {
+        m_PropertyValue = object.m_PropertyValue;
+        ViewModelInstanceValue::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case propertyValuePropertyKey:
+                m_PropertyValue = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return ViewModelInstanceValue::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void propertyValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/viewmodel_instance_string_base.hpp b/include/rive/generated/viewmodel/viewmodel_instance_string_base.hpp
new file mode 100644
index 0000000..538c833
--- /dev/null
+++ b/include/rive/generated/viewmodel/viewmodel_instance_string_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_VIEW_MODEL_INSTANCE_STRING_BASE_HPP_
+#define _RIVE_VIEW_MODEL_INSTANCE_STRING_BASE_HPP_
+#include <string>
+#include "rive/core/field_types/core_string_type.hpp"
+#include "rive/viewmodel/viewmodel_instance_value.hpp"
+namespace rive
+{
+class ViewModelInstanceStringBase : public ViewModelInstanceValue
+{
+protected:
+    typedef ViewModelInstanceValue Super;
+
+public:
+    static const uint16_t typeKey = 433;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ViewModelInstanceStringBase::typeKey:
+            case ViewModelInstanceValueBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t propertyValuePropertyKey = 561;
+
+private:
+    std::string m_PropertyValue = "";
+
+public:
+    inline const std::string& propertyValue() const { return m_PropertyValue; }
+    void propertyValue(std::string value)
+    {
+        if (m_PropertyValue == value)
+        {
+            return;
+        }
+        m_PropertyValue = value;
+        propertyValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ViewModelInstanceStringBase& object)
+    {
+        m_PropertyValue = object.m_PropertyValue;
+        ViewModelInstanceValue::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case propertyValuePropertyKey:
+                m_PropertyValue = CoreStringType::deserialize(reader);
+                return true;
+        }
+        return ViewModelInstanceValue::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void propertyValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/viewmodel_instance_value_base.hpp b/include/rive/generated/viewmodel/viewmodel_instance_value_base.hpp
new file mode 100644
index 0000000..3bb08bd
--- /dev/null
+++ b/include/rive/generated/viewmodel/viewmodel_instance_value_base.hpp
@@ -0,0 +1,68 @@
+#ifndef _RIVE_VIEW_MODEL_INSTANCE_VALUE_BASE_HPP_
+#define _RIVE_VIEW_MODEL_INSTANCE_VALUE_BASE_HPP_
+#include "rive/core.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class ViewModelInstanceValueBase : public Core
+{
+protected:
+    typedef Core Super;
+
+public:
+    static const uint16_t typeKey = 428;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ViewModelInstanceValueBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t viewModelPropertyIdPropertyKey = 554;
+
+private:
+    uint32_t m_ViewModelPropertyId = 0;
+
+public:
+    inline uint32_t viewModelPropertyId() const { return m_ViewModelPropertyId; }
+    void viewModelPropertyId(uint32_t value)
+    {
+        if (m_ViewModelPropertyId == value)
+        {
+            return;
+        }
+        m_ViewModelPropertyId = value;
+        viewModelPropertyIdChanged();
+    }
+
+    void copy(const ViewModelInstanceValueBase& object)
+    {
+        m_ViewModelPropertyId = object.m_ViewModelPropertyId;
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case viewModelPropertyIdPropertyKey:
+                m_ViewModelPropertyId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return false;
+    }
+
+protected:
+    virtual void viewModelPropertyIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/viewmodel_instance_viewmodel_base.hpp b/include/rive/generated/viewmodel/viewmodel_instance_viewmodel_base.hpp
new file mode 100644
index 0000000..395569a
--- /dev/null
+++ b/include/rive/generated/viewmodel/viewmodel_instance_viewmodel_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_VIEW_MODEL_INSTANCE_VIEW_MODEL_BASE_HPP_
+#define _RIVE_VIEW_MODEL_INSTANCE_VIEW_MODEL_BASE_HPP_
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/viewmodel/viewmodel_instance_value.hpp"
+namespace rive
+{
+class ViewModelInstanceViewModelBase : public ViewModelInstanceValue
+{
+protected:
+    typedef ViewModelInstanceValue Super;
+
+public:
+    static const uint16_t typeKey = 444;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ViewModelInstanceViewModelBase::typeKey:
+            case ViewModelInstanceValueBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t propertyValuePropertyKey = 577;
+
+private:
+    uint32_t m_PropertyValue = 0;
+
+public:
+    inline uint32_t propertyValue() const { return m_PropertyValue; }
+    void propertyValue(uint32_t value)
+    {
+        if (m_PropertyValue == value)
+        {
+            return;
+        }
+        m_PropertyValue = value;
+        propertyValueChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ViewModelInstanceViewModelBase& object)
+    {
+        m_PropertyValue = object.m_PropertyValue;
+        ViewModelInstanceValue::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case propertyValuePropertyKey:
+                m_PropertyValue = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return ViewModelInstanceValue::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void propertyValueChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/viewmodel_property_base.hpp b/include/rive/generated/viewmodel/viewmodel_property_base.hpp
new file mode 100644
index 0000000..fd65087
--- /dev/null
+++ b/include/rive/generated/viewmodel/viewmodel_property_base.hpp
@@ -0,0 +1,36 @@
+#ifndef _RIVE_VIEW_MODEL_PROPERTY_BASE_HPP_
+#define _RIVE_VIEW_MODEL_PROPERTY_BASE_HPP_
+#include "rive/viewmodel/viewmodel_component.hpp"
+namespace rive
+{
+class ViewModelPropertyBase : public ViewModelComponent
+{
+protected:
+    typedef ViewModelComponent Super;
+
+public:
+    static const uint16_t typeKey = 430;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ViewModelPropertyBase::typeKey:
+            case ViewModelComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/viewmodel_property_boolean_base.hpp b/include/rive/generated/viewmodel/viewmodel_property_boolean_base.hpp
new file mode 100644
index 0000000..61a8c29
--- /dev/null
+++ b/include/rive/generated/viewmodel/viewmodel_property_boolean_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_VIEW_MODEL_PROPERTY_BOOLEAN_BASE_HPP_
+#define _RIVE_VIEW_MODEL_PROPERTY_BOOLEAN_BASE_HPP_
+#include "rive/viewmodel/viewmodel_property.hpp"
+namespace rive
+{
+class ViewModelPropertyBooleanBase : public ViewModelProperty
+{
+protected:
+    typedef ViewModelProperty Super;
+
+public:
+    static const uint16_t typeKey = 448;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ViewModelPropertyBooleanBase::typeKey:
+            case ViewModelPropertyBase::typeKey:
+            case ViewModelComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/viewmodel_property_color_base.hpp b/include/rive/generated/viewmodel/viewmodel_property_color_base.hpp
new file mode 100644
index 0000000..a24b20f
--- /dev/null
+++ b/include/rive/generated/viewmodel/viewmodel_property_color_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_VIEW_MODEL_PROPERTY_COLOR_BASE_HPP_
+#define _RIVE_VIEW_MODEL_PROPERTY_COLOR_BASE_HPP_
+#include "rive/viewmodel/viewmodel_property.hpp"
+namespace rive
+{
+class ViewModelPropertyColorBase : public ViewModelProperty
+{
+protected:
+    typedef ViewModelProperty Super;
+
+public:
+    static const uint16_t typeKey = 440;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ViewModelPropertyColorBase::typeKey:
+            case ViewModelPropertyBase::typeKey:
+            case ViewModelComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/viewmodel_property_enum_base.hpp b/include/rive/generated/viewmodel/viewmodel_property_enum_base.hpp
new file mode 100644
index 0000000..63bd198
--- /dev/null
+++ b/include/rive/generated/viewmodel/viewmodel_property_enum_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_VIEW_MODEL_PROPERTY_ENUM_BASE_HPP_
+#define _RIVE_VIEW_MODEL_PROPERTY_ENUM_BASE_HPP_
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/viewmodel/viewmodel_property.hpp"
+namespace rive
+{
+class ViewModelPropertyEnumBase : public ViewModelProperty
+{
+protected:
+    typedef ViewModelProperty Super;
+
+public:
+    static const uint16_t typeKey = 439;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ViewModelPropertyEnumBase::typeKey:
+            case ViewModelPropertyBase::typeKey:
+            case ViewModelComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t enumIdPropertyKey = 574;
+
+private:
+    uint32_t m_EnumId = -1;
+
+public:
+    inline uint32_t enumId() const { return m_EnumId; }
+    void enumId(uint32_t value)
+    {
+        if (m_EnumId == value)
+        {
+            return;
+        }
+        m_EnumId = value;
+        enumIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ViewModelPropertyEnumBase& object)
+    {
+        m_EnumId = object.m_EnumId;
+        ViewModelProperty::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case enumIdPropertyKey:
+                m_EnumId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return ViewModelProperty::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void enumIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/viewmodel_property_list_base.hpp b/include/rive/generated/viewmodel/viewmodel_property_list_base.hpp
new file mode 100644
index 0000000..0ced54b
--- /dev/null
+++ b/include/rive/generated/viewmodel/viewmodel_property_list_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_VIEW_MODEL_PROPERTY_LIST_BASE_HPP_
+#define _RIVE_VIEW_MODEL_PROPERTY_LIST_BASE_HPP_
+#include "rive/viewmodel/viewmodel_property.hpp"
+namespace rive
+{
+class ViewModelPropertyListBase : public ViewModelProperty
+{
+protected:
+    typedef ViewModelProperty Super;
+
+public:
+    static const uint16_t typeKey = 434;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ViewModelPropertyListBase::typeKey:
+            case ViewModelPropertyBase::typeKey:
+            case ViewModelComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/viewmodel_property_number_base.hpp b/include/rive/generated/viewmodel/viewmodel_property_number_base.hpp
new file mode 100644
index 0000000..6b4055a
--- /dev/null
+++ b/include/rive/generated/viewmodel/viewmodel_property_number_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_VIEW_MODEL_PROPERTY_NUMBER_BASE_HPP_
+#define _RIVE_VIEW_MODEL_PROPERTY_NUMBER_BASE_HPP_
+#include "rive/viewmodel/viewmodel_property.hpp"
+namespace rive
+{
+class ViewModelPropertyNumberBase : public ViewModelProperty
+{
+protected:
+    typedef ViewModelProperty Super;
+
+public:
+    static const uint16_t typeKey = 431;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ViewModelPropertyNumberBase::typeKey:
+            case ViewModelPropertyBase::typeKey:
+            case ViewModelComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/viewmodel_property_string_base.hpp b/include/rive/generated/viewmodel/viewmodel_property_string_base.hpp
new file mode 100644
index 0000000..49bd3aa
--- /dev/null
+++ b/include/rive/generated/viewmodel/viewmodel_property_string_base.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_VIEW_MODEL_PROPERTY_STRING_BASE_HPP_
+#define _RIVE_VIEW_MODEL_PROPERTY_STRING_BASE_HPP_
+#include "rive/viewmodel/viewmodel_property.hpp"
+namespace rive
+{
+class ViewModelPropertyStringBase : public ViewModelProperty
+{
+protected:
+    typedef ViewModelProperty Super;
+
+public:
+    static const uint16_t typeKey = 443;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ViewModelPropertyStringBase::typeKey:
+            case ViewModelPropertyBase::typeKey:
+            case ViewModelComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    Core* clone() const override;
+
+protected:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/viewmodel/viewmodel_property_viewmodel_base.hpp b/include/rive/generated/viewmodel/viewmodel_property_viewmodel_base.hpp
new file mode 100644
index 0000000..e79e25a
--- /dev/null
+++ b/include/rive/generated/viewmodel/viewmodel_property_viewmodel_base.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_VIEW_MODEL_PROPERTY_VIEW_MODEL_BASE_HPP_
+#define _RIVE_VIEW_MODEL_PROPERTY_VIEW_MODEL_BASE_HPP_
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/viewmodel/viewmodel_property.hpp"
+namespace rive
+{
+class ViewModelPropertyViewModelBase : public ViewModelProperty
+{
+protected:
+    typedef ViewModelProperty Super;
+
+public:
+    static const uint16_t typeKey = 436;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ViewModelPropertyViewModelBase::typeKey:
+            case ViewModelPropertyBase::typeKey:
+            case ViewModelComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t viewModelReferenceIdPropertyKey = 565;
+
+private:
+    uint32_t m_ViewModelReferenceId = 0;
+
+public:
+    inline uint32_t viewModelReferenceId() const { return m_ViewModelReferenceId; }
+    void viewModelReferenceId(uint32_t value)
+    {
+        if (m_ViewModelReferenceId == value)
+        {
+            return;
+        }
+        m_ViewModelReferenceId = value;
+        viewModelReferenceIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ViewModelPropertyViewModelBase& object)
+    {
+        m_ViewModelReferenceId = object.m_ViewModelReferenceId;
+        ViewModelProperty::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case viewModelReferenceIdPropertyKey:
+                m_ViewModelReferenceId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return ViewModelProperty::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void viewModelReferenceIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/world_transform_component_base.hpp b/include/rive/generated/world_transform_component_base.hpp
new file mode 100644
index 0000000..169cb4f
--- /dev/null
+++ b/include/rive/generated/world_transform_component_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_WORLD_TRANSFORM_COMPONENT_BASE_HPP_
+#define _RIVE_WORLD_TRANSFORM_COMPONENT_BASE_HPP_
+#include "rive/container_component.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class WorldTransformComponentBase : public ContainerComponent
+{
+protected:
+    typedef ContainerComponent Super;
+
+public:
+    static const uint16_t typeKey = 91;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case WorldTransformComponentBase::typeKey:
+            case ContainerComponentBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t opacityPropertyKey = 18;
+
+private:
+    float m_Opacity = 1.0f;
+
+public:
+    inline float opacity() const { return m_Opacity; }
+    void opacity(float value)
+    {
+        if (m_Opacity == value)
+        {
+            return;
+        }
+        m_Opacity = value;
+        opacityChanged();
+    }
+
+    void copy(const WorldTransformComponentBase& object)
+    {
+        m_Opacity = object.m_Opacity;
+        ContainerComponent::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case opacityPropertyKey:
+                m_Opacity = CoreDoubleType::deserialize(reader);
+                return true;
+        }
+        return ContainerComponent::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void opacityChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/gesture_click_phase.hpp b/include/rive/gesture_click_phase.hpp
new file mode 100644
index 0000000..2daf21a
--- /dev/null
+++ b/include/rive/gesture_click_phase.hpp
@@ -0,0 +1,12 @@
+#ifndef _RIVE_GESTURE_CLICK_PHASE_HPP_
+#define _RIVE_GESTURE_CLICK_PHASE_HPP_
+namespace rive
+{
+enum class GestureClickPhase : int
+{
+    out = 0,
+    down = 1,
+    clicked = 2,
+};
+}
+#endif
\ No newline at end of file
diff --git a/include/rive/hit_info.hpp b/include/rive/hit_info.hpp
new file mode 100644
index 0000000..efbd03a
--- /dev/null
+++ b/include/rive/hit_info.hpp
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_HITINFO_HPP_
+#define _RIVE_HITINFO_HPP_
+
+#include "rive/math/aabb.hpp"
+#include <vector>
+
+namespace rive
+{
+
+class NestedArtboard;
+
+struct HitInfo
+{
+    IAABB area;                          // input
+    std::vector<NestedArtboard*> mounts; // output
+};
+
+} // namespace rive
+#endif
diff --git a/include/rive/hit_result.hpp b/include/rive/hit_result.hpp
new file mode 100644
index 0000000..ec0c7fd
--- /dev/null
+++ b/include/rive/hit_result.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_HIT_RESULT_HPP_
+#define _RIVE_HIT_RESULT_HPP_
+
+namespace rive
+{
+enum class HitResult : uint8_t
+{
+    none,
+    hit,
+    hitOpaque,
+};
+} // namespace rive
+#endif
diff --git a/include/rive/hittest_command_path.hpp b/include/rive/hittest_command_path.hpp
new file mode 100644
index 0000000..ea2b6d8
--- /dev/null
+++ b/include/rive/hittest_command_path.hpp
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_HITTEST_COMMAND_PATH_HPP_
+#define _RIVE_HITTEST_COMMAND_PATH_HPP_
+
+#include "rive/command_path.hpp"
+#include "rive/math/hit_test.hpp"
+
+namespace rive
+{
+class HitTester;
+
+class HitTestCommandPath : public CommandPath
+{
+    HitTester m_Tester;
+    Mat2D m_Xform;
+    IAABB m_Area;
+    FillRule m_FillRule = FillRule::nonZero;
+
+public:
+    HitTestCommandPath(const IAABB& area);
+
+    // can call this between calls to move/line/etc.
+    void setXform(const Mat2D& xform) { m_Xform = xform; }
+
+    bool wasHit();
+
+    // These 4 are not a good for the hit-tester
+    void rewind() override;
+    void fillRule(FillRule value) override;
+    void addPath(CommandPath* path, const Mat2D& transform) override;
+    RenderPath* renderPath() override;
+
+    void moveTo(float x, float y) override;
+    void lineTo(float x, float y) override;
+    void cubicTo(float ox, float oy, float ix, float iy, float x, float y) override;
+    void close() override;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/importers/artboard_importer.hpp b/include/rive/importers/artboard_importer.hpp
new file mode 100644
index 0000000..102bfd0
--- /dev/null
+++ b/include/rive/importers/artboard_importer.hpp
@@ -0,0 +1,32 @@
+#ifndef _RIVE_ARTBOARD_IMPORTER_HPP_
+#define _RIVE_ARTBOARD_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+
+namespace rive
+{
+class Core;
+class Artboard;
+class LinearAnimation;
+class StateMachine;
+class TextValueRun;
+class Event;
+class DataBind;
+class ArtboardImporter : public ImportStackObject
+{
+private:
+    Artboard* m_Artboard;
+
+public:
+    ArtboardImporter(Artboard* artboard);
+    void addComponent(Core* object);
+    void addAnimation(LinearAnimation* animation);
+    void addStateMachine(StateMachine* stateMachine);
+    void addDataBind(DataBind* dataBind);
+    StatusCode resolve() override;
+    const Artboard* artboard() const { return m_Artboard; }
+
+    bool readNullObject() override;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/importers/backboard_importer.hpp b/include/rive/importers/backboard_importer.hpp
new file mode 100644
index 0000000..cb2f971
--- /dev/null
+++ b/include/rive/importers/backboard_importer.hpp
@@ -0,0 +1,46 @@
+#ifndef _RIVE_BACKBOARD_IMPORTER_HPP_
+#define _RIVE_BACKBOARD_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+#include <unordered_map>
+#include <vector>
+
+namespace rive
+{
+class Artboard;
+class NestedArtboard;
+class Backboard;
+class FileAsset;
+class FileAssetReferencer;
+class DataConverter;
+class DataBind;
+class DataConverterGroupItem;
+class BackboardImporter : public ImportStackObject
+{
+private:
+    Backboard* m_Backboard;
+    std::unordered_map<int, Artboard*> m_ArtboardLookup;
+    std::vector<NestedArtboard*> m_NestedArtboards;
+    std::vector<FileAsset*> m_FileAssets;
+    std::vector<FileAssetReferencer*> m_FileAssetReferencers;
+    std::vector<DataConverter*> m_DataConverters;
+    std::vector<DataBind*> m_DataConverterReferencers;
+    std::vector<DataConverterGroupItem*> m_DataConverterGroupItemReferencers;
+    int m_NextArtboardId;
+
+public:
+    BackboardImporter(Backboard* backboard);
+    void addArtboard(Artboard* artboard);
+    void addMissingArtboard();
+    void addNestedArtboard(NestedArtboard* artboard);
+    void addFileAsset(FileAsset* asset);
+    void addFileAssetReferencer(FileAssetReferencer* referencer);
+    void addDataConverterReferencer(DataBind* referencer);
+    void addDataConverter(DataConverter* converter);
+    void addDataConverterGroupItemReferencer(DataConverterGroupItem* referencer);
+
+    StatusCode resolve() override;
+    const Backboard* backboard() const { return m_Backboard; }
+};
+} // namespace rive
+#endif
diff --git a/include/rive/importers/bindable_property_importer.hpp b/include/rive/importers/bindable_property_importer.hpp
new file mode 100644
index 0000000..002ebe7
--- /dev/null
+++ b/include/rive/importers/bindable_property_importer.hpp
@@ -0,0 +1,20 @@
+#ifndef _RIVE_BINDABLE_PROPERTY_IMPORTER_HPP_
+#define _RIVE_BINDABLE_PROPERTY_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+
+namespace rive
+{
+class Core;
+class BindableProperty;
+class BindablePropertyImporter : public ImportStackObject
+{
+private:
+    BindableProperty* m_bindableProperty;
+
+public:
+    BindablePropertyImporter(BindableProperty* bindableProperty);
+    BindableProperty* bindableProperty() { return m_bindableProperty; }
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/importers/data_converter_group_importer.hpp b/include/rive/importers/data_converter_group_importer.hpp
new file mode 100644
index 0000000..61ba8ab
--- /dev/null
+++ b/include/rive/importers/data_converter_group_importer.hpp
@@ -0,0 +1,20 @@
+#ifndef _RIVE_DATA_CONVERTER_GROUP_IMPORTER_HPP_
+#define _RIVE_DATA_CONVERTER_GROUP_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+
+namespace rive
+{
+class Core;
+class DataConverterGroup;
+class DataConverterGroupImporter : public ImportStackObject
+{
+private:
+    DataConverterGroup* m_dataConverterGroup;
+
+public:
+    DataConverterGroupImporter(DataConverterGroup* group);
+    DataConverterGroup* group() { return m_dataConverterGroup; }
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/importers/enum_importer.hpp b/include/rive/importers/enum_importer.hpp
new file mode 100644
index 0000000..a79b1b6
--- /dev/null
+++ b/include/rive/importers/enum_importer.hpp
@@ -0,0 +1,23 @@
+#ifndef _RIVE_ENUM_IMPORTER_HPP_
+#define _RIVE_ENUM_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+
+namespace rive
+{
+class Core;
+class DataEnum;
+class DataEnumValue;
+class EnumImporter : public ImportStackObject
+{
+private:
+    DataEnum* m_DataEnum;
+
+public:
+    EnumImporter(DataEnum* dataEnum);
+    void addValue(DataEnumValue* object);
+    StatusCode resolve() override;
+    const DataEnum* dataEnum() const { return m_DataEnum; }
+};
+} // namespace rive
+#endif
diff --git a/include/rive/importers/file_asset_importer.hpp b/include/rive/importers/file_asset_importer.hpp
new file mode 100644
index 0000000..c32cff3
--- /dev/null
+++ b/include/rive/importers/file_asset_importer.hpp
@@ -0,0 +1,30 @@
+#ifndef _RIVE_FILE_ASSET_IMPORTER_HPP_
+#define _RIVE_FILE_ASSET_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+#include <unordered_map>
+#include <vector>
+
+namespace rive
+{
+class FileAsset;
+class FileAssetContents;
+class FileAssetLoader;
+class Factory;
+
+class FileAssetImporter : public ImportStackObject
+{
+private:
+    FileAsset* m_FileAsset;
+    FileAssetLoader* m_FileAssetLoader;
+    Factory* m_Factory;
+    // we will delete this when we go out of scope
+    std::unique_ptr<FileAssetContents> m_Content;
+
+public:
+    FileAssetImporter(FileAsset*, FileAssetLoader*, Factory*);
+    void onFileAssetContents(std::unique_ptr<FileAssetContents> contents);
+    StatusCode resolve() override;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/importers/import_stack.hpp b/include/rive/importers/import_stack.hpp
new file mode 100644
index 0000000..5e0436f
--- /dev/null
+++ b/include/rive/importers/import_stack.hpp
@@ -0,0 +1,108 @@
+#ifndef _RIVE_IMPORT_STACK_HPP_
+#define _RIVE_IMPORT_STACK_HPP_
+#include "rive/status_code.hpp"
+#include <algorithm>
+#include <unordered_map>
+#include <vector>
+#include <algorithm>
+#include <memory>
+
+namespace rive
+{
+class ImportStackObject
+{
+public:
+    virtual ~ImportStackObject() {}
+    virtual StatusCode resolve() { return StatusCode::Ok; }
+    virtual bool readNullObject() { return false; }
+};
+
+class ImportStack
+{
+public:
+    template <typename T = ImportStackObject> T* latest(uint16_t coreType)
+    {
+        auto itr = m_latests.find(coreType);
+        if (itr == m_latests.end())
+        {
+            return nullptr;
+        }
+        return static_cast<T*>(itr->second.get());
+    }
+
+    StatusCode makeLatest(uint16_t coreType, std::unique_ptr<ImportStackObject> object)
+    {
+        // Clean up the old object in the stack.
+        auto itr = m_latests.find(coreType);
+        if (itr != m_latests.end())
+        {
+            auto stackObject = itr->second.get();
+
+            // Remove it from latests.
+            auto lastAddedItr = std::find(m_lastAdded.begin(), m_lastAdded.end(), stackObject);
+            if (lastAddedItr != m_lastAdded.end())
+            {
+                m_lastAdded.erase(lastAddedItr);
+            }
+
+            StatusCode code = stackObject->resolve();
+            if (code != StatusCode::Ok)
+            {
+                m_latests.erase(coreType);
+                return code;
+            }
+        }
+
+        // Set the new one.
+        if (object == nullptr)
+        {
+            m_latests.erase(coreType);
+        }
+        else
+        {
+            m_lastAdded.push_back(object.get());
+            m_latests[coreType] = std::move(object);
+        }
+        return StatusCode::Ok;
+    }
+
+    StatusCode resolve()
+    {
+        StatusCode returnCode = StatusCode::Ok;
+
+        // Reverse iterate the last added import stack objects and resolve them.
+        // If any don't resolve, capture the return code and stop resolving.
+        for (auto itr = m_lastAdded.rbegin(); itr != m_lastAdded.rend(); itr++)
+        {
+            StatusCode code = (*itr)->resolve();
+            if (code != StatusCode::Ok)
+            {
+                returnCode = code;
+                break;
+            }
+        }
+
+        m_latests.clear();
+        m_lastAdded.clear();
+
+        return returnCode;
+    }
+
+    bool readNullObject()
+    {
+        for (auto itr = m_lastAdded.rbegin(); itr != m_lastAdded.rend(); itr++)
+        {
+            if ((*itr)->readNullObject())
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+private:
+    std::unordered_map<uint16_t, std::unique_ptr<ImportStackObject>> m_latests;
+    std::vector<ImportStackObject*> m_lastAdded;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/importers/keyed_object_importer.hpp b/include/rive/importers/keyed_object_importer.hpp
new file mode 100644
index 0000000..fd7abf1
--- /dev/null
+++ b/include/rive/importers/keyed_object_importer.hpp
@@ -0,0 +1,21 @@
+#ifndef _RIVE_KEYED_OBJECT_IMPORTER_HPP_
+#define _RIVE_KEYED_OBJECT_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+
+namespace rive
+{
+class Core;
+class KeyedObject;
+class KeyedProperty;
+class KeyedObjectImporter : public ImportStackObject
+{
+private:
+    KeyedObject* m_KeyedObject;
+
+public:
+    KeyedObjectImporter(KeyedObject* keyedObject);
+    void addKeyedProperty(std::unique_ptr<KeyedProperty>);
+};
+} // namespace rive
+#endif
diff --git a/include/rive/importers/keyed_property_importer.hpp b/include/rive/importers/keyed_property_importer.hpp
new file mode 100644
index 0000000..46096a9
--- /dev/null
+++ b/include/rive/importers/keyed_property_importer.hpp
@@ -0,0 +1,24 @@
+#ifndef _RIVE_KEYED_PROPERTY_IMPORTER_HPP_
+#define _RIVE_KEYED_PROPERTY_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+
+namespace rive
+{
+class Core;
+class KeyFrame;
+class KeyedProperty;
+class LinearAnimation;
+class KeyedPropertyImporter : public ImportStackObject
+{
+private:
+    LinearAnimation* m_Animation;
+    KeyedProperty* m_KeyedProperty;
+
+public:
+    KeyedPropertyImporter(LinearAnimation* animation, KeyedProperty* keyedProperty);
+    void addKeyFrame(std::unique_ptr<KeyFrame>);
+    bool readNullObject() override;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/importers/layer_state_importer.hpp b/include/rive/importers/layer_state_importer.hpp
new file mode 100644
index 0000000..d11d39a
--- /dev/null
+++ b/include/rive/importers/layer_state_importer.hpp
@@ -0,0 +1,24 @@
+#ifndef _RIVE_LAYER_STATE_IMPORTER_HPP_
+#define _RIVE_LAYER_STATE_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+
+namespace rive
+{
+class LayerState;
+class StateTransition;
+class BlendAnimation;
+
+class LayerStateImporter : public ImportStackObject
+{
+private:
+    LayerState* m_State;
+
+public:
+    LayerStateImporter(LayerState* state);
+    void addTransition(StateTransition* transition);
+    bool addBlendAnimation(BlendAnimation* animation);
+    StatusCode resolve() override;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/importers/linear_animation_importer.hpp b/include/rive/importers/linear_animation_importer.hpp
new file mode 100644
index 0000000..64a27bc
--- /dev/null
+++ b/include/rive/importers/linear_animation_importer.hpp
@@ -0,0 +1,22 @@
+#ifndef _RIVE_LINEAR_ANIMATION_IMPORTER_HPP_
+#define _RIVE_LINEAR_ANIMATION_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+
+namespace rive
+{
+class Core;
+class LinearAnimation;
+class KeyedObject;
+class LinearAnimationImporter : public ImportStackObject
+{
+private:
+    LinearAnimation* m_Animation;
+
+public:
+    LinearAnimation* animation() const { return m_Animation; }
+    LinearAnimationImporter(LinearAnimation* animation);
+    void addKeyedObject(std::unique_ptr<KeyedObject>);
+};
+} // namespace rive
+#endif
diff --git a/include/rive/importers/state_machine_importer.hpp b/include/rive/importers/state_machine_importer.hpp
new file mode 100644
index 0000000..dc08236
--- /dev/null
+++ b/include/rive/importers/state_machine_importer.hpp
@@ -0,0 +1,31 @@
+#ifndef _RIVE_STATE_MACHINE_IMPORTER_HPP_
+#define _RIVE_STATE_MACHINE_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+
+namespace rive
+{
+class StateMachineInput;
+class StateMachineLayer;
+class StateMachineListener;
+class StateMachine;
+class DataBind;
+class StateMachineImporter : public ImportStackObject
+{
+private:
+    StateMachine* m_StateMachine;
+
+public:
+    StateMachineImporter(StateMachine* machine);
+    const StateMachine* stateMachine() const { return m_StateMachine; }
+
+    void addLayer(std::unique_ptr<StateMachineLayer>);
+    void addInput(std::unique_ptr<StateMachineInput>);
+    void addListener(std::unique_ptr<StateMachineListener>);
+    void addDataBind(std::unique_ptr<DataBind>);
+
+    StatusCode resolve() override;
+    bool readNullObject() override;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/importers/state_machine_layer_component_importer.hpp b/include/rive/importers/state_machine_layer_component_importer.hpp
new file mode 100644
index 0000000..b9132a2
--- /dev/null
+++ b/include/rive/importers/state_machine_layer_component_importer.hpp
@@ -0,0 +1,22 @@
+#ifndef _RIVE_STATE_MACHINE_LAYER_COMPONENT_IMPORTER_HPP_
+#define _RIVE_STATE_MACHINE_LAYER_COMPONENT_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+
+namespace rive
+{
+class StateMachineLayerComponent;
+class StateMachineFireEvent;
+
+class StateMachineLayerComponentImporter : public ImportStackObject
+{
+public:
+    StateMachineLayerComponentImporter(StateMachineLayerComponent* component);
+
+    void addFireEvent(StateMachineFireEvent* fireEvent);
+
+private:
+    StateMachineLayerComponent* m_stateMachineLayerComponent;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/importers/state_machine_layer_importer.hpp b/include/rive/importers/state_machine_layer_importer.hpp
new file mode 100644
index 0000000..37f4de8
--- /dev/null
+++ b/include/rive/importers/state_machine_layer_importer.hpp
@@ -0,0 +1,25 @@
+#ifndef _RIVE_STATE_MACHINE_LAYER_IMPORTER_HPP_
+#define _RIVE_STATE_MACHINE_LAYER_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+
+namespace rive
+{
+class StateMachineLayer;
+class LayerState;
+class Artboard;
+
+class StateMachineLayerImporter : public ImportStackObject
+{
+private:
+    StateMachineLayer* m_Layer;
+    const Artboard* m_Artboard;
+
+public:
+    StateMachineLayerImporter(StateMachineLayer* layer, const Artboard* artboard);
+    void addState(LayerState* state);
+    StatusCode resolve() override;
+    bool readNullObject() override;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/importers/state_machine_listener_importer.hpp b/include/rive/importers/state_machine_listener_importer.hpp
new file mode 100644
index 0000000..4d5660a
--- /dev/null
+++ b/include/rive/importers/state_machine_listener_importer.hpp
@@ -0,0 +1,23 @@
+#ifndef _RIVE_STATE_MACHINE_LISTENER_IMPORTER_HPP_
+#define _RIVE_STATE_MACHINE_LISTENER_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+
+namespace rive
+{
+class StateMachineListener;
+class StateMachine;
+class ListenerAction;
+class StateMachineListenerImporter : public ImportStackObject
+{
+private:
+    StateMachineListener* m_StateMachineListener;
+
+public:
+    StateMachineListenerImporter(StateMachineListener* listener);
+    const StateMachineListener* stateMachineListener() const { return m_StateMachineListener; }
+    void addAction(std::unique_ptr<ListenerAction>);
+    StatusCode resolve() override;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/importers/state_transition_importer.hpp b/include/rive/importers/state_transition_importer.hpp
new file mode 100644
index 0000000..f683cc2
--- /dev/null
+++ b/include/rive/importers/state_transition_importer.hpp
@@ -0,0 +1,22 @@
+#ifndef _RIVE_TRANSITION_IMPORTER_HPP_
+#define _RIVE_TRANSITION_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+
+namespace rive
+{
+class StateTransition;
+class TransitionCondition;
+
+class StateTransitionImporter : public ImportStackObject
+{
+private:
+    StateTransition* m_Transition;
+
+public:
+    StateTransitionImporter(StateTransition* transition);
+    void addCondition(TransitionCondition* condition);
+    StatusCode resolve() override;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/importers/transition_viewmodel_condition_importer.hpp b/include/rive/importers/transition_viewmodel_condition_importer.hpp
new file mode 100644
index 0000000..7cf92bd
--- /dev/null
+++ b/include/rive/importers/transition_viewmodel_condition_importer.hpp
@@ -0,0 +1,23 @@
+#ifndef _RIVE_TRANSITION_VIEWMODEL_CONDITION_IMPORTER_HPP_
+#define _RIVE_TRANSITION_VIEWMODEL_CONDITION_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+
+namespace rive
+{
+class TransitionViewModelCondition;
+class TransitionComparator;
+class DataBind;
+
+class TransitionViewModelConditionImporter : public ImportStackObject
+{
+private:
+    TransitionViewModelCondition* m_TransitionViewModelCondition;
+
+public:
+    TransitionViewModelConditionImporter(
+        TransitionViewModelCondition* transitionViewModelCondition);
+    void setComparator(TransitionComparator* comparator);
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/importers/viewmodel_importer.hpp b/include/rive/importers/viewmodel_importer.hpp
new file mode 100644
index 0000000..8298c26
--- /dev/null
+++ b/include/rive/importers/viewmodel_importer.hpp
@@ -0,0 +1,24 @@
+#ifndef _RIVE_VIEWMODEL_IMPORTER_HPP_
+#define _RIVE_VIEWMODEL_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+
+namespace rive
+{
+class ViewModel;
+class ViewModelProperty;
+class ViewModelInstance;
+
+class ViewModelImporter : public ImportStackObject
+{
+private:
+    ViewModel* m_ViewModel;
+
+public:
+    ViewModelImporter(ViewModel* viewModel);
+    void addProperty(ViewModelProperty* property);
+    void addInstance(ViewModelInstance* value);
+    StatusCode resolve() override;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/importers/viewmodel_instance_importer.hpp b/include/rive/importers/viewmodel_instance_importer.hpp
new file mode 100644
index 0000000..d70c858
--- /dev/null
+++ b/include/rive/importers/viewmodel_instance_importer.hpp
@@ -0,0 +1,22 @@
+#ifndef _RIVE_VIEWMODEL_INSTANCE_IMPORTER_HPP_
+#define _RIVE_VIEWMODEL_INSTANCE_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+
+namespace rive
+{
+class ViewModelInstance;
+class ViewModelInstanceValue;
+
+class ViewModelInstanceImporter : public ImportStackObject
+{
+private:
+    ViewModelInstance* m_ViewModelInstance;
+
+public:
+    ViewModelInstanceImporter(ViewModelInstance* viewModelInstance);
+    void addValue(ViewModelInstanceValue* value);
+    StatusCode resolve() override;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/importers/viewmodel_instance_list_importer.hpp b/include/rive/importers/viewmodel_instance_list_importer.hpp
new file mode 100644
index 0000000..3833b2e
--- /dev/null
+++ b/include/rive/importers/viewmodel_instance_list_importer.hpp
@@ -0,0 +1,22 @@
+#ifndef _RIVE_VIEWMODEL_INSTANCE_LIST_IMPORTER_HPP_
+#define _RIVE_VIEWMODEL_INSTANCE_LIST_IMPORTER_HPP_
+
+#include "rive/importers/import_stack.hpp"
+
+namespace rive
+{
+class Core;
+class ViewModelInstanceList;
+class ViewModelInstanceListItem;
+class ViewModelInstanceListImporter : public ImportStackObject
+{
+private:
+    ViewModelInstanceList* m_ViewModelInstanceList;
+
+public:
+    ViewModelInstanceListImporter(ViewModelInstanceList* viewModelInstanceList);
+    void addItem(ViewModelInstanceListItem* listItem);
+    StatusCode resolve() override;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/joystick.hpp b/include/rive/joystick.hpp
new file mode 100644
index 0000000..b781890
--- /dev/null
+++ b/include/rive/joystick.hpp
@@ -0,0 +1,40 @@
+#ifndef _RIVE_JOYSTICK_HPP_
+#define _RIVE_JOYSTICK_HPP_
+#include "rive/generated/joystick_base.hpp"
+#include "rive/joystick_flags.hpp"
+#include "rive/math/mat2d.hpp"
+#include <stdio.h>
+
+namespace rive
+{
+class Artboard;
+class LinearAnimation;
+class TransformComponent;
+class Joystick : public JoystickBase
+{
+public:
+    void update(ComponentDirt value) override;
+    void apply(Artboard* artboard) const;
+    StatusCode onAddedClean(CoreContext* context) override;
+    StatusCode onAddedDirty(CoreContext* context) override;
+
+    bool isJoystickFlagged(JoystickFlags flag) const
+    {
+        return ((JoystickFlags)joystickFlags()) & flag;
+    }
+
+    bool canApplyBeforeUpdate() const { return m_handleSource == nullptr; }
+
+protected:
+    void buildDependencies() override;
+
+private:
+    Mat2D m_worldTransform;
+    Mat2D m_inverseWorldTransform;
+    LinearAnimation* m_xAnimation = nullptr;
+    LinearAnimation* m_yAnimation = nullptr;
+    TransformComponent* m_handleSource = nullptr;
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/joystick_flags.hpp b/include/rive/joystick_flags.hpp
new file mode 100644
index 0000000..f4b2534
--- /dev/null
+++ b/include/rive/joystick_flags.hpp
@@ -0,0 +1,21 @@
+#ifndef _RIVE_JOYSTICK_FLAGS_HPP_
+#define _RIVE_JOYSTICK_FLAGS_HPP_
+
+#include "rive/enum_bitset.hpp"
+
+namespace rive
+{
+enum class JoystickFlags : unsigned char
+{
+    /// Whether to invert the application of the x axis.
+    invertX = 1 << 0,
+
+    /// Whether to invert the application of the y axis.
+    invertY = 1 << 1,
+
+    /// Whether this Joystick works in world space.
+    worldSpace = 1 << 2
+};
+RIVE_MAKE_ENUM_BITSET(JoystickFlags)
+} // namespace rive
+#endif
diff --git a/include/rive/layout.hpp b/include/rive/layout.hpp
new file mode 100644
index 0000000..afd21ab
--- /dev/null
+++ b/include/rive/layout.hpp
@@ -0,0 +1,40 @@
+#ifndef _RIVE_LAYOUT_HPP_
+#define _RIVE_LAYOUT_HPP_
+namespace rive
+{
+enum class Fit : unsigned char
+{
+    fill,
+    contain,
+    cover,
+    fitWidth,
+    fitHeight,
+    none,
+    scaleDown
+};
+
+class Alignment
+{
+public:
+    Alignment(float x, float y) : m_x(x), m_y(y) {}
+    Alignment() : m_x(0.0f), m_y(0.0f) {}
+
+    float x() const { return m_x; }
+    float y() const { return m_y; }
+
+    static const Alignment topLeft;
+    static const Alignment topCenter;
+    static const Alignment topRight;
+    static const Alignment centerLeft;
+    static const Alignment center;
+    static const Alignment centerRight;
+    static const Alignment bottomLeft;
+    static const Alignment bottomCenter;
+    static const Alignment bottomRight;
+
+private:
+    float m_x, m_y;
+};
+
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/layout/axis.hpp b/include/rive/layout/axis.hpp
new file mode 100644
index 0000000..a5eef6e
--- /dev/null
+++ b/include/rive/layout/axis.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_AXIS_HPP_
+#define _RIVE_AXIS_HPP_
+#include "rive/generated/layout/axis_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class Axis : public AxisBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/layout/axis_x.hpp b/include/rive/layout/axis_x.hpp
new file mode 100644
index 0000000..3cd9a37
--- /dev/null
+++ b/include/rive/layout/axis_x.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_AXIS_X_HPP_
+#define _RIVE_AXIS_X_HPP_
+#include "rive/generated/layout/axis_x_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class AxisX : public AxisXBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/layout/axis_y.hpp b/include/rive/layout/axis_y.hpp
new file mode 100644
index 0000000..3f38d83
--- /dev/null
+++ b/include/rive/layout/axis_y.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_AXIS_Y_HPP_
+#define _RIVE_AXIS_Y_HPP_
+#include "rive/generated/layout/axis_y_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class AxisY : public AxisYBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/layout/layout_component_style.hpp b/include/rive/layout/layout_component_style.hpp
new file mode 100644
index 0000000..cab31c9
--- /dev/null
+++ b/include/rive/layout/layout_component_style.hpp
@@ -0,0 +1,185 @@
+#ifndef _RIVE_LAYOUT_COMPONENT_STYLE_HPP_
+#define _RIVE_LAYOUT_COMPONENT_STYLE_HPP_
+#include "rive/generated/layout/layout_component_style_base.hpp"
+#ifdef WITH_RIVE_LAYOUT
+#include "yoga/Yoga.h"
+#endif
+#include <stdio.h>
+namespace rive
+{
+enum class LayoutAnimationStyle : uint8_t
+{
+    none,
+    inherit,
+    custom
+};
+
+enum class LayoutStyleInterpolation : uint8_t
+{
+    hold,
+    linear,
+    cubic,
+    elastic
+};
+
+enum class LayoutAlignmentType : uint8_t
+{
+    topLeft,
+    topCenter,
+    topRight,
+    centerLeft,
+    center,
+    centerRight,
+    bottomLeft,
+    bottomCenter,
+    bottomRight,
+    spaceBetweenStart,
+    spaceBetweenCenter,
+    spaceBetweenEnd
+};
+
+enum class LayoutScaleType : uint8_t
+{
+    fixed,
+    fill,
+    hug
+};
+
+class KeyFrameInterpolator;
+class LayoutComponentStyle : public LayoutComponentStyleBase
+{
+private:
+#ifdef WITH_RIVE_LAYOUT
+    KeyFrameInterpolator* m_interpolator;
+#endif
+
+public:
+    LayoutComponentStyle() {}
+
+#ifdef WITH_RIVE_LAYOUT
+    StatusCode onAddedDirty(CoreContext* context) override;
+    KeyFrameInterpolator* interpolator();
+    LayoutStyleInterpolation interpolation();
+    LayoutAnimationStyle animationStyle();
+    YGDisplay display();
+    YGPositionType positionType();
+    LayoutAlignmentType alignmentType();
+    LayoutScaleType widthScaleType();
+    LayoutScaleType heightScaleType();
+
+    YGFlexDirection flexDirection();
+    YGDirection direction();
+    YGWrap flexWrap();
+    YGOverflow overflow();
+
+    YGAlign alignItems();
+    YGAlign alignSelf();
+    YGAlign alignContent();
+    YGJustify justifyContent();
+    bool intrinsicallySized();
+    YGUnit widthUnits();
+    YGUnit heightUnits();
+
+    YGUnit borderLeftUnits();
+    YGUnit borderRightUnits();
+    YGUnit borderTopUnits();
+    YGUnit borderBottomUnits();
+    YGUnit marginLeftUnits();
+    YGUnit marginRightUnits();
+    YGUnit marginTopUnits();
+    YGUnit marginBottomUnits();
+    YGUnit paddingLeftUnits();
+    YGUnit paddingRightUnits();
+    YGUnit paddingTopUnits();
+    YGUnit paddingBottomUnits();
+    YGUnit positionLeftUnits();
+    YGUnit positionRightUnits();
+    YGUnit positionTopUnits();
+    YGUnit positionBottomUnits();
+
+    YGUnit gapHorizontalUnits();
+    YGUnit gapVerticalUnits();
+    YGUnit maxWidthUnits();
+    YGUnit maxHeightUnits();
+    YGUnit minWidthUnits();
+    YGUnit minHeightUnits();
+#endif
+
+    void markLayoutNodeDirty();
+    void markLayoutStyleDirty();
+
+    void layoutAlignmentTypeChanged() override;
+    void layoutWidthScaleTypeChanged() override;
+    void layoutHeightScaleTypeChanged() override;
+    void displayValueChanged() override;
+    void positionTypeValueChanged() override;
+    void overflowValueChanged() override;
+    void intrinsicallySizedValueChanged() override;
+    void flexDirectionValueChanged() override;
+    void directionValueChanged() override;
+    void alignContentValueChanged() override;
+    void alignItemsValueChanged() override;
+    void alignSelfValueChanged() override;
+    void justifyContentValueChanged() override;
+    void flexWrapValueChanged() override;
+    void flexChanged() override;
+    void flexGrowChanged() override;
+    void flexShrinkChanged() override;
+    void flexBasisChanged() override;
+    void aspectRatioChanged() override;
+    void gapHorizontalChanged() override;
+    void gapVerticalChanged() override;
+    void maxWidthChanged() override;
+    void maxHeightChanged() override;
+    void minWidthChanged() override;
+    void minHeightChanged() override;
+    void borderLeftChanged() override;
+    void borderRightChanged() override;
+    void borderTopChanged() override;
+    void borderBottomChanged() override;
+    void marginLeftChanged() override;
+    void marginRightChanged() override;
+    void marginTopChanged() override;
+    void marginBottomChanged() override;
+    void paddingLeftChanged() override;
+    void paddingRightChanged() override;
+    void paddingTopChanged() override;
+    void paddingBottomChanged() override;
+    void positionLeftChanged() override;
+    void positionRightChanged() override;
+    void positionTopChanged() override;
+    void positionBottomChanged() override;
+
+    void widthUnitsValueChanged() override;
+    void heightUnitsValueChanged() override;
+    void gapHorizontalUnitsValueChanged() override;
+    void gapVerticalUnitsValueChanged() override;
+    void maxWidthUnitsValueChanged() override;
+    void maxHeightUnitsValueChanged() override;
+    void minWidthUnitsValueChanged() override;
+    void minHeightUnitsValueChanged() override;
+    void borderLeftUnitsValueChanged() override;
+    void borderRightUnitsValueChanged() override;
+    void borderTopUnitsValueChanged() override;
+    void borderBottomUnitsValueChanged() override;
+    void marginLeftUnitsValueChanged() override;
+    void marginRightUnitsValueChanged() override;
+    void marginTopUnitsValueChanged() override;
+    void marginBottomUnitsValueChanged() override;
+    void paddingLeftUnitsValueChanged() override;
+    void paddingRightUnitsValueChanged() override;
+    void paddingTopUnitsValueChanged() override;
+    void paddingBottomUnitsValueChanged() override;
+    void positionLeftUnitsValueChanged() override;
+    void positionRightUnitsValueChanged() override;
+    void positionTopUnitsValueChanged() override;
+    void positionBottomUnitsValueChanged() override;
+
+    void cornerRadiusTLChanged() override;
+    void cornerRadiusTRChanged() override;
+    void cornerRadiusBLChanged() override;
+    void cornerRadiusBRChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/layout/layout_measure_mode.hpp b/include/rive/layout/layout_measure_mode.hpp
new file mode 100644
index 0000000..50f6eb8
--- /dev/null
+++ b/include/rive/layout/layout_measure_mode.hpp
@@ -0,0 +1,12 @@
+#ifndef _RIVE_LAYOUT_MEASURE_MODE_HPP_
+#define _RIVE_LAYOUT_MEASURE_MODE_HPP_
+namespace rive
+{
+enum class LayoutMeasureMode : uint8_t
+{
+    undefined = 0,
+    exactly = 1,
+    atMost = 2
+};
+}
+#endif
\ No newline at end of file
diff --git a/include/rive/layout/n_slicer.hpp b/include/rive/layout/n_slicer.hpp
new file mode 100644
index 0000000..7f13a61
--- /dev/null
+++ b/include/rive/layout/n_slicer.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_N_SLICER_HPP_
+#define _RIVE_N_SLICER_HPP_
+#include "rive/generated/layout/n_slicer_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class NSlicer : public NSlicerBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/layout/n_slicer_tile_mode.hpp b/include/rive/layout/n_slicer_tile_mode.hpp
new file mode 100644
index 0000000..06e7cab
--- /dev/null
+++ b/include/rive/layout/n_slicer_tile_mode.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_N_SLICER_TILE_MODE_HPP_
+#define _RIVE_N_SLICER_TILE_MODE_HPP_
+#include "rive/generated/layout/n_slicer_tile_mode_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class NSlicerTileMode : public NSlicerTileModeBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/layout_component.hpp b/include/rive/layout_component.hpp
new file mode 100644
index 0000000..18d8999
--- /dev/null
+++ b/include/rive/layout_component.hpp
@@ -0,0 +1,169 @@
+#ifndef _RIVE_LAYOUT_COMPONENT_HPP_
+#define _RIVE_LAYOUT_COMPONENT_HPP_
+#include "rive/drawable.hpp"
+#include "rive/generated/layout_component_base.hpp"
+#include "rive/layout/layout_component_style.hpp"
+#include "rive/layout/layout_measure_mode.hpp"
+#include "rive/math/raw_path.hpp"
+#include "rive/shapes/rectangle.hpp"
+#include "rive/shapes/shape_paint_container.hpp"
+#ifdef WITH_RIVE_LAYOUT
+#include "yoga/YGNode.h"
+#include "yoga/YGStyle.h"
+#include "yoga/Yoga.h"
+#endif
+
+namespace rive
+{
+
+class AABB;
+class KeyFrameInterpolator;
+
+struct LayoutData
+{
+#ifdef WITH_RIVE_LAYOUT
+    YGNode node;
+    YGStyle style;
+#endif
+};
+
+struct LayoutAnimationData
+{
+    float elapsedSeconds = 0;
+    AABB fromBounds = AABB();
+    AABB toBounds = AABB();
+};
+
+class LayoutComponent : public LayoutComponentBase, public ProxyDrawing, public ShapePaintContainer
+{
+protected:
+    LayoutComponentStyle* m_style = nullptr;
+    std::unique_ptr<LayoutData> m_layoutData;
+
+    float m_layoutSizeWidth = 0;
+    float m_layoutSizeHeight = 0;
+    float m_layoutLocationX = 0;
+    float m_layoutLocationY = 0;
+
+    LayoutAnimationData m_animationData;
+    KeyFrameInterpolator* m_inheritedInterpolator;
+    LayoutStyleInterpolation m_inheritedInterpolation = LayoutStyleInterpolation::hold;
+    float m_inheritedInterpolationTime = 0;
+    Rectangle* m_backgroundRect = new Rectangle();
+    rcp<RenderPath> m_backgroundPath;
+    rcp<RenderPath> m_clipPath;
+    DrawableProxy m_proxy;
+
+    Artboard* getArtboard() override { return artboard(); }
+
+    LayoutComponent* layoutParent()
+    {
+        auto p = parent();
+        while (p != nullptr)
+        {
+            if (p->is<LayoutComponent>())
+            {
+                return p->as<LayoutComponent>();
+            }
+            p = p->parent();
+        }
+        return nullptr;
+    }
+
+private:
+    float m_widthOverride = NAN;
+    int m_widthUnitValueOverride = -1;
+    float m_heightOverride = NAN;
+    int m_heightUnitValueOverride = -1;
+    bool m_parentIsRow = true;
+
+#ifdef WITH_RIVE_LAYOUT
+protected:
+    YGNode& layoutNode() { return m_layoutData->node; }
+    YGStyle& layoutStyle() { return m_layoutData->style; }
+    void syncLayoutChildren();
+    void propagateSizeToChildren(ContainerComponent* component);
+    bool applyInterpolation(double elapsedSeconds);
+
+protected:
+    void calculateLayout();
+#endif
+
+public:
+    LayoutComponentStyle* style() { return m_style; }
+    void style(LayoutComponentStyle* style) { m_style = style; }
+
+    void draw(Renderer* renderer) override;
+    void drawProxy(Renderer* renderer) override;
+    Core* hitTest(HitInfo*, const Mat2D&) override;
+    DrawableProxy* proxy() { return &m_proxy; };
+    virtual void updateRenderPath();
+    void update(ComponentDirt value) override;
+    void onDirty(ComponentDirt value) override;
+    AABB layoutBounds()
+    {
+        return AABB::fromLTWH(m_layoutLocationX,
+                              m_layoutLocationY,
+                              m_layoutSizeWidth,
+                              m_layoutSizeHeight);
+    }
+    AABB localBounds() const override
+    {
+        return AABB::fromLTWH(0.0f, 0.0f, m_layoutSizeWidth, m_layoutSizeHeight);
+    }
+
+    // We provide a way for nested artboards (or other objects) to override this layout's
+    // width/height and unit values.
+    void widthOverride(float width, int unitValue = 1, bool isRow = true);
+    void heightOverride(float height, int unitValue = 1, bool isRow = true);
+    virtual bool canHaveOverrides() { return false; }
+    bool mainAxisIsRow();
+    bool mainAxisIsColumn();
+
+#ifdef WITH_RIVE_LAYOUT
+    LayoutComponent() : m_layoutData(std::unique_ptr<LayoutData>(new LayoutData())), m_proxy(this)
+    {
+        layoutNode().getConfig()->setPointScaleFactor(0);
+    }
+    ~LayoutComponent() { delete m_backgroundRect; }
+    void syncStyle();
+    virtual void propagateSize();
+    void updateLayoutBounds();
+    StatusCode onAddedDirty(CoreContext* context) override;
+    StatusCode onAddedClean(CoreContext* context) override;
+
+    bool advance(double elapsedSeconds);
+    bool animates();
+    LayoutAnimationStyle animationStyle();
+    KeyFrameInterpolator* interpolator();
+    LayoutStyleInterpolation interpolation();
+    float interpolationTime();
+
+    void cascadeAnimationStyle(LayoutStyleInterpolation inheritedInterpolation,
+                               KeyFrameInterpolator* inheritedInterpolator,
+                               float inheritedInterpolationTime);
+    void setInheritedInterpolation(LayoutStyleInterpolation inheritedInterpolation,
+                                   KeyFrameInterpolator* inheritedInterpolator,
+                                   float inheritedInterpolationTime);
+    void clearInheritedInterpolation();
+#else
+    LayoutComponent() : m_layoutData(std::unique_ptr<LayoutData>(new LayoutData())), m_proxy(this)
+    {}
+#endif
+    void buildDependencies() override;
+
+    void markLayoutNodeDirty();
+    void markLayoutStyleDirty();
+    void clipChanged() override;
+    void widthChanged() override;
+    void heightChanged() override;
+    void styleIdChanged() override;
+
+    Vec2D measureLayout(float width,
+                        LayoutMeasureMode widthMode,
+                        float height,
+                        LayoutMeasureMode heightMode) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/listener_type.hpp b/include/rive/listener_type.hpp
new file mode 100644
index 0000000..7fab809
--- /dev/null
+++ b/include/rive/listener_type.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_LISTENER_TYPE_HPP_
+#define _RIVE_LISTENER_TYPE_HPP_
+namespace rive
+{
+enum class ListenerType : int
+{
+    enter = 0,
+    exit = 1,
+    down = 2,
+    up = 3,
+    move = 4,
+    event = 5,
+    click = 6,
+};
+}
+#endif
\ No newline at end of file
diff --git a/include/rive/math/aabb.hpp b/include/rive/math/aabb.hpp
new file mode 100644
index 0000000..6dda2ab
--- /dev/null
+++ b/include/rive/math/aabb.hpp
@@ -0,0 +1,129 @@
+#ifndef _RIVE_AABB_HPP_
+#define _RIVE_AABB_HPP_
+
+#include "rive/span.hpp"
+#include "rive/math/vec2d.hpp"
+#include <limits>
+
+namespace rive
+{
+struct IAABB
+{
+    int32_t left, top, right, bottom;
+
+    constexpr int width() const { return right - left; }
+    constexpr int height() const { return bottom - top; }
+    constexpr bool empty() const { return left >= right || top >= bottom; }
+
+    IAABB inset(int dx, int dy) const { return {left + dx, top + dy, right - dx, bottom - dy}; }
+    IAABB offset(int dx, int dy) const { return {left + dx, top + dy, right + dx, bottom + dy}; }
+    IAABB join(IAABB b) const
+    {
+        return {std::min(left, b.left),
+                std::min(top, b.top),
+                std::max(right, b.right),
+                std::max(bottom, b.bottom)};
+    }
+    IAABB intersect(IAABB b) const
+    {
+        return {std::max(left, b.left),
+                std::max(top, b.top),
+                std::min(right, b.right),
+                std::min(bottom, b.bottom)};
+    }
+
+    bool operator==(const IAABB& o) const
+    {
+        return left == o.left && top == o.top && right == o.right && bottom == o.bottom;
+    }
+    bool operator!=(const IAABB& o) const { return !(*this == o); }
+};
+
+class AABB
+{
+public:
+    float minX, minY, maxX, maxY;
+
+    AABB() : minX(0), minY(0), maxX(0), maxY(0) {}
+    AABB(const Vec2D& min, const Vec2D& max) : minX(min.x), minY(min.y), maxX(max.x), maxY(max.y) {}
+    static AABB fromLTWH(float x, float y, float width, float height)
+    {
+        return {x, y, x + width, y + height};
+    }
+
+    AABB(float minX, float minY, float maxX, float maxY) :
+        minX(minX), minY(minY), maxX(maxX), maxY(maxY)
+    {}
+
+    AABB(const IAABB& o) : AABB((float)o.left, (float)o.top, (float)o.right, (float)o.bottom) {}
+
+    AABB(Span<Vec2D>); // computes the union of all points, or 0,0,0,0
+
+    bool operator==(const AABB& o) const
+    {
+        return minX == o.minX && minY == o.minY && maxX == o.maxX && maxY == o.maxY;
+    }
+    bool operator!=(const AABB& o) const { return !(*this == o); }
+
+    float left() const { return minX; }
+    float top() const { return minY; }
+    float right() const { return maxX; }
+    float bottom() const { return maxY; }
+
+    float width() const { return maxX - minX; }
+    float height() const { return maxY - minY; }
+    Vec2D size() const { return {width(), height()}; }
+    Vec2D center() const { return {(minX + maxX) * 0.5f, (minY + maxY) * 0.5f}; }
+
+    bool isEmptyOrNaN() const
+    {
+        // Use "inverse" logic so we return true if either of the comparisons fail due to a NaN.
+        return !(width() > 0 && height() > 0);
+    }
+
+    AABB inset(float dx, float dy) const
+    {
+        AABB r = {minX + dx, minY + dy, maxX - dx, maxY - dy};
+        assert(r.width() >= 0);
+        assert(r.height() >= 0);
+        return r;
+    }
+    AABB offset(float dx, float dy) const { return {minX + dx, minY + dy, maxX + dx, maxY + dy}; }
+
+    IAABB round() const;
+    IAABB roundOut() const; // Rounds out to integer bounds that fully contain the rectangle.
+
+    ///
+    /// Initialize an AABB to values that represent an invalid/collapsed
+    /// AABB that can then expand to points that are added to it.
+    ///
+    inline static AABB forExpansion()
+    {
+        return AABB(std::numeric_limits<float>::max(),
+                    std::numeric_limits<float>::max(),
+                    -std::numeric_limits<float>::max(),
+                    -std::numeric_limits<float>::max());
+    }
+
+    ///
+    /// Grow the AABB to fit the point.
+    ///
+    static void expandTo(AABB& out, const Vec2D& point);
+    static void expandTo(AABB& out, float x, float y);
+
+    /// Join two AABBs.
+    static void join(AABB& out, const AABB& a, const AABB& b);
+
+    void expand(const AABB& other) { join(*this, *this, other); }
+
+    Vec2D factorFrom(Vec2D point) const
+    {
+        return Vec2D(width() == 0.0f ? 0.0f : (point.x - left()) * 2.0f / width() - 1.0f,
+                     (height() == 0.0f ? 0.0f : point.y - top()) * 2.0f / height() - 1.0f);
+    }
+
+    bool contains(Vec2D position) const;
+};
+
+} // namespace rive
+#endif
diff --git a/include/rive/math/bit_field_loc.hpp b/include/rive/math/bit_field_loc.hpp
new file mode 100644
index 0000000..7c0c983
--- /dev/null
+++ b/include/rive/math/bit_field_loc.hpp
@@ -0,0 +1,28 @@
+#ifndef _RIVE_BIT_FIELD_LOC_HPP_
+#define _RIVE_BIT_FIELD_LOC_HPP_
+
+#include <cmath>
+#include <stdio.h>
+#include <cstdint>
+#include <tuple>
+#include <vector>
+
+namespace rive
+{
+
+class BitFieldLoc
+{
+public:
+    BitFieldLoc(uint32_t start, uint32_t end);
+
+    uint32_t read(uint32_t bits);
+    uint32_t write(uint32_t bits, uint32_t value);
+
+private:
+    uint32_t m_start;
+    uint32_t m_count;
+    uint32_t m_mask;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/math/circle_constant.hpp b/include/rive/math/circle_constant.hpp
new file mode 100644
index 0000000..e2e9a4b
--- /dev/null
+++ b/include/rive/math/circle_constant.hpp
@@ -0,0 +1,12 @@
+#ifndef _RIVE_CIRCLE_CONSTANT_HPP_
+#define _RIVE_CIRCLE_CONSTANT_HPP_
+
+#include "rive/rive_types.hpp"
+
+namespace rive
+{
+constexpr float circleConstant = 0.552284749831f;
+constexpr float icircleConstant = 1.0f - circleConstant;
+} // namespace rive
+
+#endif
diff --git a/include/rive/math/contour_measure.hpp b/include/rive/math/contour_measure.hpp
new file mode 100644
index 0000000..93eea52
--- /dev/null
+++ b/include/rive/math/contour_measure.hpp
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_CONTOUR_MEASURE_HPP_
+#define _RIVE_CONTOUR_MEASURE_HPP_
+
+#include "rive/math/raw_path.hpp"
+#include "rive/math/vec2d.hpp"
+#include "rive/refcnt.hpp"
+#include <utility>
+
+namespace rive
+{
+
+class ContourMeasure : public RefCnt<ContourMeasure>
+{
+public:
+    static constexpr unsigned kMaxDot30 = (1 << 30) - 1;
+    static constexpr float kInvScaleD30 = 1.0f / (float)kMaxDot30;
+
+    // Deliberately making this pack well (12 bytes)
+    struct Segment
+    {
+        float m_distance;       // total distance up to this point
+        uint32_t m_ptIndex;     // index of the first point for this line/quad/cubic
+        unsigned m_tValue : 30; // Dot30 t value for the end of this segment
+        unsigned m_type : 2;    // [private enum]
+
+        float getT() const { return m_tValue * kInvScaleD30; }
+
+        bool operator<(const Segment& other) const { return m_distance < other.m_distance; }
+
+        void extract(RawPath* dst, float fromT, float toT, const Vec2D pts[], bool moveTo) const;
+        void extract(RawPath* dst, const Vec2D pts[]) const;
+    };
+
+private:
+    size_t findSegment(float distance) const;
+
+    std::vector<Segment> m_segments;
+    std::vector<Vec2D> m_points;
+    const float m_length;
+    const bool m_isClosed;
+
+    ContourMeasure(std::vector<Segment>&&, std::vector<Vec2D>&&, float length, bool isClosed);
+
+    friend class ContourMeasureIter;
+
+public:
+    float length() const { return m_length; }
+    bool isClosed() const { return m_isClosed; }
+
+    struct PosTan
+    {
+        Vec2D pos, tan;
+    };
+    PosTan getPosTan(float distance) const;
+
+    void getSegment(float startDistance, float endDistance, RawPath* dst, bool startWithMove) const;
+
+    Vec2D warp(Vec2D src) const
+    {
+        const auto result = this->getPosTan(src.x);
+        return {
+            result.pos.x - result.tan.y * src.y,
+            result.pos.y + result.tan.x * src.y,
+        };
+    }
+
+    void dump() const;
+};
+
+class ContourMeasureIter
+{
+    RawPath m_optionalCopy;
+    RawPath::Iter m_iter;
+    RawPath::Iter m_end;
+    const Vec2D* m_srcPoints;
+    float m_invTolerance;
+
+    float addQuadSegs(ContourMeasure::Segment*,
+                      const Vec2D[],
+                      uint32_t segmentCount,
+                      uint32_t ptIndex,
+                      float distance) const;
+    float addCubicSegs(ContourMeasure::Segment*,
+                       const Vec2D[],
+                       uint32_t segmentCount,
+                       uint32_t ptIndex,
+                       float distance) const;
+    rcp<ContourMeasure> tryNext();
+
+public:
+    // Tolerance is the max deviation of the curve from its approximating line
+    // segments. A smaller tolerance means more line segments, but a better
+    // approximation for the curves actual length.
+    static constexpr float kDefaultTolerance = 0.5f;
+
+    ContourMeasureIter(const RawPath* path, float tol = kDefaultTolerance)
+    {
+        this->rewind(path, tol);
+    }
+
+    void rewind(const RawPath*, float = kDefaultTolerance);
+
+    // Returns a measure object for each contour in the path
+    //   (contours with zero-length are skipped over)
+    // and then returns nullptr when its finished.
+    //
+    //  ContourMeasureIter iter(path);
+    //  while ((auto meas = iter.next())) {
+    //      ... meas can be used, and passed to other objects
+    //  }
+    //
+    // Each measure object is stand-alone, and can outlive the ContourMeasureIter
+    // that created it. It contains no back pointers to the Iter or to the path.
+    //
+    rcp<ContourMeasure> next();
+
+    // Temporary storage used during tryNext(), for counting up how many segments a contour will be
+    // divided into.
+    std::vector<uint32_t> m_segmentCounts;
+};
+
+} // namespace rive
+
+#endif
diff --git a/include/rive/math/cubic_utilities.hpp b/include/rive/math/cubic_utilities.hpp
new file mode 100644
index 0000000..3450300
--- /dev/null
+++ b/include/rive/math/cubic_utilities.hpp
@@ -0,0 +1,58 @@
+#ifndef _RIVE_CUBIC_UTILITIES_HPP_
+#define _RIVE_CUBIC_UTILITIES_HPP_
+
+#include "rive/math/vec2d.hpp"
+#include <algorithm>
+
+namespace rive
+{
+///
+/// Utility functions for recursively subdividing a cubic.
+///
+class CubicUtilities
+{
+public:
+    static void computeHull(const Vec2D& from,
+                            const Vec2D& fromOut,
+                            const Vec2D& toIn,
+                            const Vec2D& to,
+                            float t,
+                            Vec2D* hull)
+    {
+        hull[0] = Vec2D::lerp(from, fromOut, t);
+        hull[1] = Vec2D::lerp(fromOut, toIn, t);
+        hull[2] = Vec2D::lerp(toIn, to, t);
+
+        hull[3] = Vec2D::lerp(hull[0], hull[1], t);
+        hull[4] = Vec2D::lerp(hull[1], hull[2], t);
+
+        hull[5] = Vec2D::lerp(hull[3], hull[4], t);
+    }
+
+    static bool tooFar(const Vec2D& a, const Vec2D& b, float threshold)
+    {
+        return std::max(std::abs(a.x - b.x), std::abs(a.y - b.y)) > threshold;
+    }
+
+    static bool shouldSplitCubic(const Vec2D& from,
+                                 const Vec2D& fromOut,
+                                 const Vec2D& toIn,
+                                 const Vec2D& to,
+                                 float threshold)
+    {
+
+        Vec2D oneThird = Vec2D::lerp(from, to, 1.0f / 3.0f);
+        Vec2D twoThird = Vec2D::lerp(from, to, 2.0f / 3.0f);
+        return tooFar(fromOut, oneThird, threshold) || tooFar(toIn, twoThird, threshold);
+    }
+
+    static float cubicAt(float t, float a, float b, float c, float d)
+    {
+        float ti = 1.0f - t;
+        float value =
+            ti * ti * ti * a + 3.0f * ti * ti * t * b + 3.0f * ti * t * t * c + t * t * t * d;
+        return value;
+    }
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/math/hit_test.hpp b/include/rive/math/hit_test.hpp
new file mode 100644
index 0000000..d2baeb4
--- /dev/null
+++ b/include/rive/math/hit_test.hpp
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_HITTEST_HPP_
+#define _RIVE_HITTEST_HPP_
+
+#include "rive/span.hpp"
+#include "rive/math/aabb.hpp"
+#include "rive/math/path_types.hpp"
+#include "rive/math/vec2d.hpp"
+
+#include <cstdint>
+#include <vector>
+
+namespace rive
+{
+
+class HitTester
+{
+    std::vector<int> m_DW; // width * height delta-windings
+    Vec2D m_First, m_Prev;
+    Vec2D m_offset;
+    float m_height;
+    int m_IWidth, m_IHeight;
+    bool m_ExpectsMove;
+
+    void recurse_cubic(Vec2D b, Vec2D c, Vec2D d, int count);
+
+public:
+    HitTester() {}
+    HitTester(const IAABB& area) { reset(area); }
+
+    void reset();
+    void reset(const IAABB& area);
+
+    void move(Vec2D);
+    void line(Vec2D);
+    void quad(Vec2D, Vec2D);
+    void cubic(Vec2D, Vec2D, Vec2D);
+    void close();
+
+    void addRect(const AABB&, const Mat2D&, PathDirection = PathDirection::ccw);
+
+    bool test(FillRule = rive::FillRule::nonZero);
+
+    static bool testMesh(Vec2D point, Span<Vec2D> verts, Span<uint16_t> indices);
+    static bool testMesh(const IAABB&, Span<Vec2D> verts, Span<uint16_t> indices);
+};
+
+} // namespace rive
+#endif
diff --git a/include/rive/math/mat2d.hpp b/include/rive/math/mat2d.hpp
new file mode 100644
index 0000000..dd1414b
--- /dev/null
+++ b/include/rive/math/mat2d.hpp
@@ -0,0 +1,111 @@
+#ifndef _RIVE_MAT2D_HPP_
+#define _RIVE_MAT2D_HPP_
+
+#include "rive/math/aabb.hpp"
+#include "rive/math/vec2d.hpp"
+#include <array>
+#include <cstddef>
+
+namespace rive
+{
+class TransformComponents;
+class Mat2D
+{
+public:
+    constexpr Mat2D() : m_buffer{{1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f}} {}
+    constexpr Mat2D(float x1, float y1, float x2, float y2, float tx, float ty) :
+        m_buffer{{x1, y1, x2, y2, tx, ty}}
+    {}
+
+    inline const float* values() const { return &m_buffer[0]; }
+
+    float& operator[](std::size_t idx) { return m_buffer[idx]; }
+    const float& operator[](std::size_t idx) const { return m_buffer[idx]; }
+
+    static Mat2D fromRotation(float rad);
+    static Mat2D fromScale(float sx, float sy) { return {sx, 0, 0, sy, 0, 0}; }
+    static Mat2D fromTranslate(float tx, float ty) { return {1, 0, 0, 1, tx, ty}; }
+    static Mat2D fromTranslation(Vec2D translation)
+    {
+        return {1, 0, 0, 1, translation.x, translation.y};
+    }
+    static Mat2D fromScaleAndTranslation(float sx, float sy, float tx, float ty)
+    {
+        return {sx, 0, 0, sy, tx, ty};
+    }
+
+    void scaleByValues(float sx, float sy);
+
+    Mat2D& operator*=(const Mat2D& rhs)
+    {
+        *this = Mat2D::multiply(*this, rhs);
+        return *this;
+    }
+
+    // Sets dst[i] = M * pts[i] for i in 0..n-1.
+    void mapPoints(Vec2D dst[], const Vec2D pts[], size_t n) const;
+
+    // Computes a bounding box that would tightly contain the given points if they were to all be
+    // transformed by this matrix, ignoring NaN values.
+    // Returns {0, 0, 0, 0} if the given points are empty or all NaN.
+    AABB mapBoundingBox(const Vec2D pts[], size_t n) const;
+    AABB mapBoundingBox(const AABB&) const;
+
+    // If returns true, result holds the inverse.
+    // If returns false, result is unchnaged.
+    bool invert(Mat2D* result) const;
+
+    Mat2D invertOrIdentity() const
+    {
+        Mat2D inverse;          // initialized to identity
+        (void)invert(&inverse); // inverse is unchanged if invert() fails
+        return inverse;
+    }
+
+    TransformComponents decompose() const;
+    static Mat2D compose(const TransformComponents&);
+    float findMaxScale() const;
+    Mat2D scale(Vec2D) const;
+
+    static Mat2D multiply(const Mat2D& a, const Mat2D& b);
+
+    float xx() const { return m_buffer[0]; }
+    float xy() const { return m_buffer[1]; }
+    float yx() const { return m_buffer[2]; }
+    float yy() const { return m_buffer[3]; }
+    float tx() const { return m_buffer[4]; }
+    float ty() const { return m_buffer[5]; }
+
+    Vec2D translation() const { return {m_buffer[4], m_buffer[5]}; }
+
+    void xx(float value) { m_buffer[0] = value; }
+    void xy(float value) { m_buffer[1] = value; }
+    void yx(float value) { m_buffer[2] = value; }
+    void yy(float value) { m_buffer[3] = value; }
+    void tx(float value) { m_buffer[4] = value; }
+    void ty(float value) { m_buffer[5] = value; }
+
+private:
+    std::array<float, 6> m_buffer;
+};
+
+inline Vec2D operator*(const Mat2D& m, Vec2D v)
+{
+    return {
+        m[0] * v.x + m[2] * v.y + m[4],
+        m[1] * v.x + m[3] * v.y + m[5],
+    };
+}
+
+inline Mat2D operator*(const Mat2D& a, const Mat2D& b) { return Mat2D::multiply(a, b); }
+
+inline bool operator==(const Mat2D& a, const Mat2D& b)
+{
+    return a[0] == b[0] && a[1] == b[1] && a[2] == b[2] && a[3] == b[3] && a[4] == b[4] &&
+           a[5] == b[5];
+}
+
+inline bool operator!=(const Mat2D& a, const Mat2D& b) { return !(a == b); }
+
+} // namespace rive
+#endif
diff --git a/include/rive/math/math_types.hpp b/include/rive/math/math_types.hpp
new file mode 100644
index 0000000..b34adc3
--- /dev/null
+++ b/include/rive/math/math_types.hpp
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_MATH_TYPES_DEFINED_
+#define _RIVE_MATH_TYPES_DEFINED_
+
+#include "rive/rive_types.hpp"
+#include <cmath>
+#include <limits>
+#include <string.h>
+
+namespace rive
+{
+
+namespace math
+{
+constexpr float PI = 3.14159265f;
+constexpr float SQRT2 = 1.41421356f;
+constexpr float EPSILON = 1.f / (1 << 12); // Common threshold for detecting values near zero.
+
+RIVE_MAYBE_UNUSED inline bool nearly_zero(float a, float tolerance = EPSILON)
+{
+    assert(tolerance >= 0);
+    return fabsf(a) <= tolerance;
+}
+
+RIVE_MAYBE_UNUSED inline bool nearly_equal(float a, float b, float tolerance = EPSILON)
+{
+    return nearly_zero(b - a, tolerance);
+}
+
+// Performs a floating point division with conformant IEEE 754 behavior for NaN and Inf.
+//
+//   Returns +/-Inf if b == 0.
+//   Returns 0 if b == +/-Inf.
+//   Returns NaN if a and b are both zero.
+//   Returns NaN if a and b are both infinite.
+//   Returns NaN a or b is NaN.
+//
+// Reference:
+// https://stackoverflow.com/questions/42926763/the-behaviour-of-floating-point-division-by-zero
+RIVE_MAYBE_UNUSED
+#if defined(__clang__) || defined(__GNUC__)
+__attribute__((no_sanitize("float-divide-by-zero"), always_inline))
+#endif
+inline static float
+ieee_float_divide(float a, float b)
+{
+    static_assert(std::numeric_limits<float>::is_iec559,
+                  "conformant IEEE 754 behavior for NaN and Inf is required");
+    return a / b;
+}
+
+// Reinterprets the underlying bits of src as the given type.
+template <typename Dst, typename Src> Dst bit_cast(const Src& src)
+{
+    static_assert(sizeof(Dst) == sizeof(Src), "sizes of both types must match");
+    Dst dst;
+    RIVE_INLINE_MEMCPY(&dst, &src, sizeof(Dst));
+    return dst;
+}
+
+// Lossless cast function that asserts on overflow
+template <typename T, typename U> T lossless_numeric_cast(U u)
+{
+    T t = static_cast<T>(u);
+    assert(static_cast<U>(t) == u);
+    return t;
+}
+
+// Attempt to generate a "clz" assembly instruction.
+RIVE_ALWAYS_INLINE static int clz32(uint32_t x)
+{
+    assert(x != 0);
+#if __has_builtin(__builtin_clz)
+    return __builtin_clz(x);
+#else
+    uint64_t doubleBits = bit_cast<uint64_t>(static_cast<double>(x));
+    return 1054 - (doubleBits >> 52);
+#endif
+}
+
+RIVE_ALWAYS_INLINE static int clz64(uint64_t x)
+{
+    assert(x != 0);
+#if __has_builtin(__builtin_clzll)
+    return __builtin_clzll(x);
+#else
+    uint32_t hi32 = x >> 32;
+    return hi32 != 0 ? clz32(hi32) : 32 + clz32(x & 0xffffffff);
+#endif
+}
+
+// Returns the 1-based index of the most significat bit in x.
+//
+//   0    -> 0
+//   1    -> 1
+//   2..3 -> 2
+//   4..7 -> 3
+//   ...
+//
+RIVE_ALWAYS_INLINE static uint32_t msb(uint32_t x) { return x != 0 ? 32 - clz32(x) : 0; }
+
+// Attempt to generate a "rotl" (rotate-left) assembly instruction.
+RIVE_ALWAYS_INLINE static uint32_t rotateleft32(uint32_t x, int y)
+{
+#if __has_builtin(__builtin_rotateleft32)
+    return __builtin_rotateleft32(x, y);
+#else
+    return (x << y) | (x >> (32 - y));
+#endif
+}
+
+// Returns x rounded up to the next multiple of N.
+// If x is already a multiple of N, returns x.
+template <size_t N, typename T> RIVE_ALWAYS_INLINE constexpr T round_up_to_multiple_of(T x)
+{
+    static_assert(N != 0 && (N & (N - 1)) == 0,
+                  "math::round_up_to_multiple_of<> only supports powers of 2.");
+    return (x + (N - 1)) & ~static_cast<T>(N - 1);
+}
+
+// Behaves better with NaN than std::clamp(). (Matching simd::clamp().)
+//
+//   Returns lo if x == NaN (but std::clamp() returns NaN).
+//   Returns hi if hi <= lo.
+//   Ignores hi and/or lo if they are NaN.
+//
+RIVE_ALWAYS_INLINE static float clamp(float x, float lo, float hi)
+{
+    return fminf(fmaxf(lo, x), hi);
+}
+} // namespace math
+
+template <typename T> T lerp(const T& a, const T& b, float t) { return a + (b - a) * t; }
+} // namespace rive
+
+#endif
diff --git a/include/rive/math/path_types.hpp b/include/rive/math/path_types.hpp
new file mode 100644
index 0000000..010a19d
--- /dev/null
+++ b/include/rive/math/path_types.hpp
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_PATH_TYPES_HPP_
+#define _RIVE_PATH_TYPES_HPP_
+
+#include "rive/rive_types.hpp"
+
+namespace rive
+{
+
+enum class FillRule
+{
+    nonZero,
+    evenOdd,
+};
+
+enum class PathDirection
+{
+    cw,
+    ccw,
+    // aliases
+    clockwise = cw,
+    counterclockwise = ccw,
+};
+
+enum class PathVerb : uint8_t
+{
+    // These deliberately match Skia's values
+    move = 0,
+    line = 1,
+    quad = 2,
+    // conic
+    cubic = 4,
+    close = 5,
+};
+
+int path_verb_to_point_count(PathVerb);
+
+} // namespace rive
+#endif
diff --git a/include/rive/math/raw_path.hpp b/include/rive/math/raw_path.hpp
new file mode 100644
index 0000000..df50245
--- /dev/null
+++ b/include/rive/math/raw_path.hpp
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_RAW_PATH_HPP_
+#define _RIVE_RAW_PATH_HPP_
+
+#include "rive/span.hpp"
+#include "rive/math/aabb.hpp"
+#include "rive/math/mat2d.hpp"
+#include "rive/math/path_types.hpp"
+#include "rive/math/vec2d.hpp"
+
+#include <cmath>
+#include <stdio.h>
+#include <cstdint>
+#include <tuple>
+#include <vector>
+
+namespace rive
+{
+
+class CommandPath;
+
+class RawPath
+{
+public:
+    bool operator==(const RawPath& o) const;
+    bool operator!=(const RawPath& o) const { return !(*this == o); }
+
+    bool empty() const { return m_Points.empty(); }
+    AABB bounds() const;
+    size_t countMoveTos() const;
+
+    void move(Vec2D);
+    void line(Vec2D);
+    void quad(Vec2D, Vec2D);
+    void cubic(Vec2D, Vec2D, Vec2D);
+    void close();
+
+    void swap(RawPath&);
+
+    // Makes the path empty and frees any memory allocated by the drawing
+    // (line, curve, move, close) calls.
+    void reset();
+
+    // Makes the path empty but keeps the memory for the drawing calls reserved.
+    void rewind();
+
+    RawPath transform(const Mat2D&) const;
+    void transformInPlace(const Mat2D&);
+
+    Span<const Vec2D> points() const { return m_Points; }
+    Span<Vec2D> points() { return m_Points; }
+
+    Span<const PathVerb> verbs() const { return m_Verbs; }
+    Span<PathVerb> verbs() { return m_Verbs; }
+
+    Span<const uint8_t> verbsU8() const
+    {
+        const uint8_t* ptr = (const uint8_t*)m_Verbs.data();
+        return Span<const uint8_t>(ptr, m_Verbs.size());
+    }
+
+    // Syntactic sugar for x,y -vs- vec2d
+
+    void moveTo(float x, float y) { move({x, y}); }
+    void lineTo(float x, float y) { line({x, y}); }
+    void quadTo(float x, float y, float x1, float y1) { quad({x, y}, {x1, y1}); }
+    void cubicTo(float x, float y, float x1, float y1, float x2, float y2)
+    {
+        cubic({x, y}, {x1, y1}, {x2, y2});
+    }
+
+    // Helpers for adding new contours
+
+    void addRect(const AABB&, PathDirection = PathDirection::cw);
+    void addOval(const AABB&, PathDirection = PathDirection::cw);
+    void addPoly(Span<const Vec2D>, bool isClosed);
+
+    // Simple STL-style iterator. To traverse using range-for:
+    //
+    //   for (auto [verb, pts] : rawPath) { ... }
+    //
+    class Iter
+    {
+    public:
+        Iter() = default;
+        Iter(const PathVerb* verbs, const Vec2D* pts) : m_verbs(verbs), m_pts(pts) {}
+
+        bool operator!=(const Iter& that) const
+        {
+            assert(m_verbs != that.m_verbs || m_pts == that.m_pts);
+            return m_verbs != that.m_verbs;
+        }
+        bool operator==(const Iter& that) const
+        {
+            assert(m_verbs != that.m_verbs || m_pts == that.m_pts);
+            return m_verbs == that.m_verbs;
+        }
+
+        // Generic accessors. The points pointer is adjusted to point to p0 for each specific verb.
+        PathVerb verb() const { return *m_verbs; }
+        const Vec2D* pts() const { return m_pts + PtsBacksetForVerb(verb()); }
+        std::tuple<PathVerb, const Vec2D*> operator*() const
+        {
+            PathVerb verb = *m_verbs;
+            return {verb, m_pts + PtsBacksetForVerb(verb)};
+        }
+
+        // Specific point accessors for callers who already know the verb. (These may be a tiny bit
+        // faster in some cases since the iterator doesn't have to check the verb.)
+        Vec2D movePt() const
+        {
+            assert(verb() == PathVerb::move);
+            return m_pts[0];
+        }
+        const Vec2D* linePts() const
+        {
+            assert(verb() == PathVerb::line);
+            return m_pts - 1;
+        }
+        const Vec2D* quadPts() const
+        {
+            assert(verb() == PathVerb::quad);
+            return m_pts - 1;
+        }
+        const Vec2D* cubicPts() const
+        {
+            assert(verb() == PathVerb::cubic);
+            return m_pts - 1;
+        }
+        Vec2D ptBeforeClose() const
+        {
+            assert(verb() == PathVerb::close);
+            return m_pts[-1];
+        }
+        // P0 for a close can be accessed via rawPtsPtr()[-1]. Note than p1 for a close is not in
+        // the array at this location.
+
+        // Internal pointers held by the iterator. See PtsBacksetForVerb() for how pts() relates to
+        // the data for specific verbs.
+        const PathVerb* rawVerbsPtr() const { return m_verbs; }
+        const Vec2D* rawPtsPtr() const { return m_pts; }
+
+        Iter& operator++() // "++iter"
+        {
+            m_pts += PtsAdvanceAfterVerb(*m_verbs++);
+            return *this;
+        }
+
+        // How much should we advance pts after encountering this verb?
+        inline static int PtsAdvanceAfterVerb(PathVerb verb)
+        {
+            switch (verb)
+            {
+                case PathVerb::move:
+                    return 1;
+                case PathVerb::line:
+                    return 1;
+                case PathVerb::quad:
+                    return 2;
+                case PathVerb::cubic:
+                    return 3;
+                case PathVerb::close:
+                    return 0;
+            }
+            RIVE_UNREACHABLE();
+        }
+
+        // Where is p0 relative to our m_pts pointer? We find the start point of segments by
+        // peeking backwards from the current point, which works as long as there is always a
+        // PathVerb::move before any geometry. (injectImplicitMoveToIfNeeded() guarantees this
+        // to be the case.)
+        inline static int PtsBacksetForVerb(PathVerb verb)
+        {
+            switch (verb)
+            {
+                case PathVerb::move:
+                    return 0;
+                case PathVerb::line:
+                    return -1;
+                case PathVerb::quad:
+                    return -1;
+                case PathVerb::cubic:
+                    return -1;
+                case PathVerb::close:
+                    return -1;
+            }
+            RIVE_UNREACHABLE();
+        }
+
+    private:
+        const PathVerb* m_verbs;
+        const Vec2D* m_pts;
+    };
+    Iter begin() const { return {m_Verbs.data(), m_Points.data()}; }
+    Iter end() const
+    {
+        return {m_Verbs.data() + m_Verbs.size(), m_Points.data() + m_Points.size()};
+    }
+
+    template <typename Handler> RawPath morph(Handler proc) const
+    {
+        RawPath dst;
+        // todo: dst.reserve(src.ptCount, src.verbCount);
+        for (auto iter : *this)
+        {
+            PathVerb verb = std::get<0>(iter);
+            const Vec2D* pts = std::get<1>(iter);
+            switch (verb)
+            {
+                case PathVerb::move:
+                    dst.move(proc(pts[0]));
+                    break;
+                case PathVerb::line:
+                    dst.line(proc(pts[1]));
+                    break;
+                case PathVerb::quad:
+                    dst.quad(proc(pts[1]), proc(pts[2]));
+                    break;
+                case PathVerb::cubic:
+                    dst.cubic(proc(pts[1]), proc(pts[2]), proc(pts[3]));
+                    break;
+                case PathVerb::close:
+                    dst.close();
+                    break;
+            }
+        }
+        return dst;
+    }
+
+    // Adds the given RawPath to the end of this path, with an optional transform.
+    // Returns an iterator at the beginning of the newly added geometry.
+    Iter addPath(const RawPath&, const Mat2D* = nullptr);
+
+    void pruneEmptySegments(Iter start);
+    void pruneEmptySegments() { pruneEmptySegments(begin()); }
+
+    // Utility for pouring a RawPath into a CommandPath
+    void addTo(CommandPath*) const;
+
+    // If there is not currently an open contour, this method opens a new contour at the current pen
+    // location, or [0,0] if the path is empty. Otherwise it does nothing.
+    void injectImplicitMoveIfNeeded();
+
+private:
+    std::vector<Vec2D> m_Points;
+    std::vector<PathVerb> m_Verbs;
+    size_t m_lastMoveIdx;
+    // True of the path is nonempty and the most recent verb is not "close".
+    bool m_contourIsOpen = false;
+};
+
+} // namespace rive
+
+#endif
diff --git a/include/rive/math/raw_path_utils.hpp b/include/rive/math/raw_path_utils.hpp
new file mode 100644
index 0000000..2eb7208
--- /dev/null
+++ b/include/rive/math/raw_path_utils.hpp
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_RAW_PATH_UTILS_HPP_
+#define _RIVE_RAW_PATH_UTILS_HPP_
+
+#include "rive/math/vec2d.hpp"
+
+namespace rive
+{
+static inline Vec2D two(Vec2D v) { return v + v; }
+
+// Caches the setup to evaluate a quadratic bezier. Useful if you
+// want to evaluate the save curve at multiple t values.
+// clang-format off
+    struct EvalQuad {
+        const Vec2D a, b, c; // at^2 + bt + c
+
+        // pts are the 3 quadratic bezier control points
+        EvalQuad(const Vec2D pts[3]) :
+            a(pts[0] - two(pts[1]) + pts[2]),
+            b(two(pts[1] - pts[0])),
+            c(pts[0]) {}
+
+        Vec2D operator()(float t) const { return (a * t + b) * t + c; }
+    };
+// clang-format on
+
+// Caches the setup to evaluate a cubic bezier. Useful if you
+// want to evaluate the save curve at multiple t values.
+struct EvalCubic
+{
+    const Vec2D a, b, c, d; // at^3 + bt^2 + ct + d
+
+    // pts are the 4 cubic bezier control points
+    EvalCubic(const Vec2D pts[4]) :
+        a(pts[3] + 3 * (pts[1] - pts[2]) - pts[0]),
+        b(3 * (pts[2] - two(pts[1]) + pts[0])),
+        c(3 * (pts[1] - pts[0])),
+        d(pts[0])
+    {}
+
+    Vec2D operator()(float t) const { return ((a * t + b) * t + c) * t + d; }
+};
+
+// Extract a subcurve from the curve (given start and end t-values)
+
+extern void quad_subdivide(const Vec2D src[3], float t, Vec2D dst[5]);
+extern void cubic_subdivide(const Vec2D src[4], float t, Vec2D dst[7]);
+
+extern void line_extract(const Vec2D src[2], float startT, float endT, Vec2D dst[2]);
+extern void quad_extract(const Vec2D src[3], float startT, float endT, Vec2D dst[3]);
+extern void cubic_extract(const Vec2D src[4], float startT, float endT, Vec2D dst[4]);
+
+} // namespace rive
+
+#endif
diff --git a/include/rive/math/simd.hpp b/include/rive/math/simd.hpp
new file mode 100644
index 0000000..23d3674
--- /dev/null
+++ b/include/rive/math/simd.hpp
@@ -0,0 +1,677 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+// An SSE / NEON / WASM_SIMD library based on clang vector types.
+//
+// This header makes use of the clang vector builtins specified in https://reviews.llvm.org/D111529.
+// This effort in clang is still a work in progress, so getting maximum performance from this header
+// requires an extremely recent version of clang.
+//
+// To explore the codegen from this header, paste it into https://godbolt.org/, select a recent
+// clang compiler, and add an -O3 flag.
+
+#ifndef _RIVE_SIMD_HPP_
+#define _RIVE_SIMD_HPP_
+
+// #define RIVE_SIMD_PERF_WARNINGS
+
+#include <algorithm>
+#include <cassert>
+#include <limits>
+#include <math.h>
+#include <stdint.h>
+#include <tuple>
+#include <type_traits>
+
+#ifdef __SSE__
+#include <immintrin.h>
+#endif
+
+#if defined(__ARM_NEON__) || defined(__aarch64__)
+#include <arm_neon.h>
+#endif
+
+#if defined(__GNUC__) || defined(__clang__)
+#ifndef __has_builtin
+#define __has_builtin(x) 0
+#endif
+#define SIMD_ALWAYS_INLINE inline __attribute__((always_inline))
+#else
+#define __has_builtin(x) 0
+#define SIMD_ALWAYS_INLINE inline
+#endif
+
+#if __has_builtin(__builtin_memcpy)
+#define SIMD_INLINE_MEMCPY __builtin_memcpy
+#else
+#define SIMD_INLINE_MEMCPY memcpy
+#endif
+
+// SIMD math can expect conformant IEEE 754 behavior for NaN and Inf.
+static_assert(std::numeric_limits<float>::is_iec559,
+              "Conformant IEEE 754 behavior for NaN and Inf is required.");
+
+#if defined(__clang__)
+
+namespace rive
+{
+namespace simd
+{
+// gvec can be native vectors inside the compiler.
+// (The GLSL spec uses "gvec" to denote a vector of unspecified type.)
+template <typename T, int N>
+using gvec = T __attribute__((ext_vector_type(N))) __attribute__((aligned(sizeof(T) * N)));
+
+// Vector booleans are masks of integer type, where true is -1 and false is 0. Vector booleans masks
+// are generated using the builtin boolean operators: ==, !=, <=, >=, <, >
+template <typename T> struct extract_element_type;
+template <typename T, int N> struct extract_element_type<gvec<T, N>>
+{
+    using type = T;
+};
+template <typename T> struct boolean_mask_type
+{
+    using type = typename extract_element_type<decltype(gvec<T, 4>() == gvec<T, 4>())>::type;
+};
+} // namespace simd
+} // namespace rive
+
+#define SIMD_NATIVE_GVEC 1
+
+#else
+
+// gvec needs to be polyfilled with templates.
+#ifdef RIVE_SIMD_PERF_WARNINGS
+#pragma message("performance: ext_vector_type not supported. Consider using clang.")
+#endif
+#include "simd_gvec_polyfill.hpp"
+
+#define SIMD_NATIVE_GVEC 0
+
+#endif
+
+namespace rive
+{
+namespace simd
+{
+////// Boolean logic //////
+//
+// Vector booleans are masks of integer type, where true is -1 and false is 0. Vector booleans masks
+// can be generated using the builtin boolean operators: ==, !=, <=, >=, <, >
+//
+
+// Returns true if all elements in x are equal to 0.
+template <typename T, int N> SIMD_ALWAYS_INLINE bool any(gvec<T, N> x)
+{
+#if __has_builtin(__builtin_reduce_or) && SIMD_NATIVE_GVEC
+    return __builtin_reduce_or(x);
+#else
+#ifdef RIVE_SIMD_PERF_WARNINGS
+#pragma message("performance: __builtin_reduce_or() not supported. Consider updating clang.")
+#endif
+    // This particular logic structure gets decent codegen in clang.
+    for (int i = 0; i < N; ++i)
+    {
+        if (x[i])
+            return true;
+    }
+    return false;
+#endif
+}
+
+// Returns true if all elements in x are equal to ~0.
+template <typename T, int N> SIMD_ALWAYS_INLINE bool all(gvec<T, N> x)
+{
+#if __has_builtin(__builtin_reduce_and) && SIMD_NATIVE_GVEC
+    return __builtin_reduce_and(x);
+#else
+#ifdef RIVE_SIMD_PERF_WARNINGS
+#pragma message("performance: __builtin_reduce_and() not supported. Consider updating clang.")
+#endif
+    // In vector, true is represented by -1 exactly, so we use ~x for "not".
+    return !any(~x);
+#endif
+}
+
+template <typename T,
+          int N,
+          typename std::enable_if<std::is_floating_point<T>::value>::type* = nullptr>
+SIMD_ALWAYS_INLINE gvec<typename boolean_mask_type<T>::type, N> isnan(gvec<T, N> x)
+{
+    return ~(x == x);
+}
+
+template <typename T, int N, typename std::enable_if<std::is_integral<T>::value>::type* = nullptr>
+constexpr gvec<typename boolean_mask_type<T>::type, N> isnan(gvec<T, N>)
+{
+    return {}; // Integer types are never NaN.
+}
+
+////// Math //////
+
+// Elementwise ternary expression: "_if ? _then : _else" for each component.
+template <typename T, int N>
+SIMD_ALWAYS_INLINE gvec<T, N> if_then_else(gvec<typename boolean_mask_type<T>::type, N> _if,
+                                           gvec<T, N> _then,
+                                           gvec<T, N> _else)
+{
+#if defined(__clang_major__) && __clang_major__ >= 13
+    // The '?:' operator supports a vector condition beginning in clang 13.
+    return _if ? _then : _else;
+#else
+#ifdef RIVE_SIMD_PERF_WARNINGS
+#pragma message("performance: vectorized '?:' operator not supported. Consider updating clang.")
+#endif
+    gvec<T, N> ret{};
+    for (int i = 0; i < N; ++i)
+        ret[i] = _if[i] ? _then[i] : _else[i];
+    return ret;
+#endif
+}
+
+// Similar to std::min(), with a noteworthy difference:
+// If a[i] or b[i] is NaN and the other is not, returns whichever is _not_ NaN.
+template <typename T, int N> SIMD_ALWAYS_INLINE gvec<T, N> min(gvec<T, N> a, gvec<T, N> b)
+{
+#if __has_builtin(__builtin_elementwise_min) && SIMD_NATIVE_GVEC
+    return __builtin_elementwise_min(a, b);
+#else
+#ifdef RIVE_SIMD_PERF_WARNINGS
+#pragma message("performance: __builtin_elementwise_min() not supported. Consider updating clang.")
+#endif
+    // Generate the same behavior for NaN as the SIMD builtins. (isnan() is a no-op for int types.)
+    return if_then_else(b < a || isnan(a), b, a);
+#endif
+}
+
+// Similar to std::max(), with a noteworthy difference:
+// If a[i] or b[i] is NaN and the other is not, returns whichever is _not_ NaN.
+template <typename T, int N> SIMD_ALWAYS_INLINE gvec<T, N> max(gvec<T, N> a, gvec<T, N> b)
+{
+#if __has_builtin(__builtin_elementwise_max) && SIMD_NATIVE_GVEC
+    return __builtin_elementwise_max(a, b);
+#else
+#ifdef RIVE_SIMD_PERF_WARNINGS
+#pragma message("performance: __builtin_elementwise_max() not supported. Consider updating clang.")
+#endif
+    // Generate the same behavior for NaN as the SIMD builtins. (isnan() is a no-op for int types.)
+    return if_then_else(a < b || isnan(a), b, a);
+#endif
+}
+
+// Unlike std::clamp(), simd::clamp() always returns a value between lo and hi.
+//
+//   Returns lo if x == NaN, but std::clamp() returns NaN.
+//   Returns hi if hi <= lo.
+//   Ignores hi and/or lo if they are NaN.
+//
+template <typename T, int N>
+SIMD_ALWAYS_INLINE gvec<T, N> clamp(gvec<T, N> x, gvec<T, N> lo, gvec<T, N> hi)
+{
+    return min(max(lo, x), hi);
+}
+
+// Returns the absolute value of x per element, with one exception:
+// If x[i] is an integer type and equal to the minimum representable value, returns x[i].
+template <typename T, int N> SIMD_ALWAYS_INLINE gvec<T, N> abs(gvec<T, N> x)
+{
+#if __has_builtin(__builtin_elementwise_abs) && SIMD_NATIVE_GVEC
+    return __builtin_elementwise_abs(x);
+#else
+#ifdef RIVE_SIMD_PERF_WARNINGS
+#pragma message("performance: __builtin_elementwise_abs() not supported. Consider updating clang.")
+#endif
+    return if_then_else(x < (T)0, -x, x); // Negate on the "true" side so we never negate NaN.
+#endif
+}
+
+template <typename T, int N>
+SIMD_ALWAYS_INLINE typename std::enable_if<std::is_integral<T>::value, T>::type reduce_add(
+    gvec<T, N> x)
+{
+#if __has_builtin(__builtin_reduce_add) && SIMD_NATIVE_GVEC
+    return __builtin_reduce_add(x);
+#else
+#ifdef RIVE_SIMD_PERF_WARNINGS
+#pragma message("performance: __builtin_reduce_and() not supported. Consider updating clang.")
+#endif
+    T s = x[0];
+    for (int i = 1; i < N; ++i)
+        s += x[i];
+    return s;
+#endif
+}
+
+template <typename T, int N>
+SIMD_ALWAYS_INLINE typename std::enable_if<!std::is_integral<T>::value, T>::type reduce_add(
+    gvec<T, N> x)
+{
+#ifdef RIVE_SIMD_PERF_WARNINGS
+#pragma message("performance: __builtin_reduce_and() not supported. Consider updating clang.")
+#endif
+    T s = x[0];
+    for (int i = 1; i < N; ++i)
+        s += x[i];
+    return s;
+}
+
+template <typename T, int N> SIMD_ALWAYS_INLINE T reduce_min(gvec<T, N> x)
+{
+#if __has_builtin(__builtin_reduce_and) && SIMD_NATIVE_GVEC
+    return __builtin_reduce_min(x);
+#else
+#ifdef RIVE_SIMD_PERF_WARNINGS
+#pragma message("performance: __builtin_reduce_and() not supported. Consider updating clang.")
+#endif
+    T reduced = x[0];
+    for (int i = 1; i < N; ++i)
+        reduced = std::min(reduced, x[i]);
+    return reduced;
+#endif
+}
+
+template <typename T, int N> SIMD_ALWAYS_INLINE T reduce_max(gvec<T, N> x)
+{
+#if __has_builtin(__builtin_reduce_and) && SIMD_NATIVE_GVEC
+    return __builtin_reduce_max(x);
+#else
+#ifdef RIVE_SIMD_PERF_WARNINGS
+#pragma message("performance: __builtin_reduce_and() not supported. Consider updating clang.")
+#endif
+    T reduced = x[0];
+    for (int i = 1; i < N; ++i)
+        reduced = std::max(reduced, x[i]);
+    return reduced;
+#endif
+}
+
+template <typename T, int N> SIMD_ALWAYS_INLINE T reduce_and(gvec<T, N> x)
+{
+#if __has_builtin(__builtin_reduce_and) && SIMD_NATIVE_GVEC
+    return __builtin_reduce_and(x);
+#else
+#ifdef RIVE_SIMD_PERF_WARNINGS
+#pragma message("performance: __builtin_reduce_and() not supported. Consider updating clang.")
+#endif
+    T reduced = x[0];
+    for (int i = 1; i < N; ++i)
+        reduced &= x[i];
+    return reduced;
+#endif
+}
+
+template <typename T, int N> SIMD_ALWAYS_INLINE T reduce_or(gvec<T, N> x)
+{
+#if __has_builtin(__builtin_reduce_and) && SIMD_NATIVE_GVEC
+    return __builtin_reduce_or(x);
+#else
+#ifdef RIVE_SIMD_PERF_WARNINGS
+#pragma message("performance: __builtin_reduce_and() not supported. Consider updating clang.")
+#endif
+    T reduced = x[0];
+    for (int i = 1; i < N; ++i)
+        reduced |= x[i];
+    return reduced;
+#endif
+}
+
+////// Floating Point Functions //////
+
+template <int N> SIMD_ALWAYS_INLINE gvec<float, N> floor(gvec<float, N> x)
+{
+#if __has_builtin(__builtin_elementwise_floor) && SIMD_NATIVE_GVEC
+    return __builtin_elementwise_floor(x);
+#else
+#ifdef RIVE_SIMD_PERF_WARNINGS
+#pragma message(                                                                                   \
+    "performance: __builtin_elementwise_floor() not supported. Consider updating clang.")
+#endif
+    for (int i = 0; i < N; ++i)
+        x[i] = floorf(x[i]);
+    return x;
+#endif
+}
+
+template <int N> SIMD_ALWAYS_INLINE gvec<float, N> ceil(gvec<float, N> x)
+{
+#if __has_builtin(__builtin_elementwise_ceil) && SIMD_NATIVE_GVEC
+    return __builtin_elementwise_ceil(x);
+#else
+#ifdef RIVE_SIMD_PERF_WARNINGS
+#pragma message("performance: __builtin_elementwise_ceil() not supported. Consider updating clang.")
+#endif
+    for (int i = 0; i < N; ++i)
+        x[i] = ceilf(x[i]);
+    return x;
+#endif
+}
+
+// IEEE compliant sqrt.
+template <int N> SIMD_ALWAYS_INLINE gvec<float, N> sqrt(gvec<float, N> x)
+{
+    // There isn't an elementwise builtin for sqrt. We define architecture-specific specializations
+    // of this function later.
+    for (int i = 0; i < N; ++i)
+        x[i] = sqrtf(x[i]);
+    return x;
+}
+
+#ifdef __SSE__
+template <> SIMD_ALWAYS_INLINE gvec<float, 4> sqrt(gvec<float, 4> x)
+{
+    __m128 _x;
+    SIMD_INLINE_MEMCPY(&_x, &x, sizeof(float) * 4);
+    _x = _mm_sqrt_ps(_x);
+    SIMD_INLINE_MEMCPY(&x, &_x, sizeof(float) * 4);
+    return x;
+}
+
+template <> SIMD_ALWAYS_INLINE gvec<float, 2> sqrt(gvec<float, 2> x)
+{
+    __m128 _x;
+    SIMD_INLINE_MEMCPY(&_x, &x, sizeof(float) * 2);
+    _x = _mm_sqrt_ps(_x);
+    SIMD_INLINE_MEMCPY(&x, &_x, sizeof(float) * 2);
+    return x;
+}
+#endif
+
+#ifdef __aarch64__
+template <> SIMD_ALWAYS_INLINE gvec<float, 4> sqrt(gvec<float, 4> x)
+{
+    float32x4_t _x;
+    SIMD_INLINE_MEMCPY(&_x, &x, sizeof(float) * 4);
+    _x = vsqrtq_f32(_x);
+    SIMD_INLINE_MEMCPY(&x, &_x, sizeof(float) * 4);
+    return x;
+}
+
+template <> SIMD_ALWAYS_INLINE gvec<float, 2> sqrt(gvec<float, 2> x)
+{
+    float32x2_t _x;
+    SIMD_INLINE_MEMCPY(&_x, &x, sizeof(float) * 2);
+    _x = vsqrt_f32(_x);
+    SIMD_INLINE_MEMCPY(&x, &_x, sizeof(float) * 2);
+    return x;
+}
+#endif
+
+// This will only be present when building with Emscripten and "-msimd128".
+#if __has_builtin(__builtin_wasm_sqrt_f32x4) && SIMD_NATIVE_GVEC
+template <> SIMD_ALWAYS_INLINE gvec<float, 4> sqrt(gvec<float, 4> x)
+{
+    return __builtin_wasm_sqrt_f32x4(x);
+}
+
+template <> SIMD_ALWAYS_INLINE gvec<float, 2> sqrt(gvec<float, 2> x)
+{
+    gvec<float, 4> _x{x.x, x.y};
+    _x = __builtin_wasm_sqrt_f32x4(_x);
+    return _x.xy;
+}
+#endif
+
+// Approximates acos(x) within 0.96 degrees, using the rational polynomial:
+//
+//     acos(x) ~= (bx^3 + ax) / (dx^4 + cx^2 + 1) + pi/2
+//
+// See: https://stackoverflow.com/a/36387954
+#define SIMD_FAST_ACOS_MAX_ERROR 0.0167552f // .96 degrees
+template <int N> SIMD_ALWAYS_INLINE gvec<float, N> fast_acos(gvec<float, N> x)
+{
+    constexpr static float a = -0.939115566365855f;
+    constexpr static float b = 0.9217841528914573f;
+    constexpr static float c = -1.2845906244690837f;
+    constexpr static float d = 0.295624144969963174f;
+    constexpr static float pi_over_2 = 1.5707963267948966f;
+    auto xx = x * x;
+    auto numer = b * xx + a;
+    auto denom = xx * (d * xx + c) + 1.f;
+    return x * (numer / denom) + pi_over_2;
+}
+
+////// Type conversion //////
+
+template <typename U, typename T, int N> SIMD_ALWAYS_INLINE gvec<U, N> cast(gvec<T, N> x)
+{
+#if __has_builtin(__builtin_convertvector) && SIMD_NATIVE_GVEC
+    return __builtin_convertvector(x, gvec<U, N>);
+#else
+    gvec<U, N> y{};
+    for (int i = 0; i < N; ++i)
+        y[i] = static_cast<U>(x[i]);
+    return y;
+#endif
+}
+
+////// Loading and storing //////
+
+template <typename T, int N> SIMD_ALWAYS_INLINE gvec<T, N> load(const void* ptr)
+{
+    gvec<T, N> ret;
+    SIMD_INLINE_MEMCPY(&ret, ptr, sizeof(T) * N);
+    return ret;
+}
+SIMD_ALWAYS_INLINE gvec<float, 2> load2f(const void* ptr) { return load<float, 2>(ptr); }
+SIMD_ALWAYS_INLINE gvec<float, 4> load4f(const void* ptr) { return load<float, 4>(ptr); }
+SIMD_ALWAYS_INLINE gvec<int32_t, 2> load2i(const void* ptr) { return load<int32_t, 2>(ptr); }
+SIMD_ALWAYS_INLINE gvec<int32_t, 4> load4i(const void* ptr) { return load<int32_t, 4>(ptr); }
+SIMD_ALWAYS_INLINE gvec<uint32_t, 2> load2ui(const void* ptr) { return load<uint32_t, 2>(ptr); }
+SIMD_ALWAYS_INLINE gvec<uint32_t, 4> load4ui(const void* ptr) { return load<uint32_t, 4>(ptr); }
+
+template <typename T, int N> SIMD_ALWAYS_INLINE void store(void* dst, gvec<T, N> vec)
+{
+    SIMD_INLINE_MEMCPY(dst, &vec, sizeof(T) * N);
+}
+
+////// Column-major (transposed) loads //////
+
+#if defined(__ARM_NEON__) || defined(__aarch64__)
+SIMD_ALWAYS_INLINE std::tuple<gvec<float, 4>, gvec<float, 4>, gvec<float, 4>, gvec<float, 4>>
+load4x4f(const float* matrix)
+{
+    float32x4x4_t m = vld4q_f32(matrix);
+    gvec<float, 4> c0, c1, c2, c3;
+    SIMD_INLINE_MEMCPY(&c0, &m.val[0], sizeof(c0));
+    SIMD_INLINE_MEMCPY(&c1, &m.val[1], sizeof(c1));
+    SIMD_INLINE_MEMCPY(&c2, &m.val[2], sizeof(c2));
+    SIMD_INLINE_MEMCPY(&c3, &m.val[3], sizeof(c3));
+    return {c0, c1, c2, c3};
+}
+#elif defined(__SSE__)
+SIMD_ALWAYS_INLINE std::tuple<gvec<float, 4>, gvec<float, 4>, gvec<float, 4>, gvec<float, 4>>
+load4x4f(const float* m)
+{
+    __m128 r0, r1, r2, r3;
+    SIMD_INLINE_MEMCPY(&r0, m + 4 * 0, sizeof(r0));
+    SIMD_INLINE_MEMCPY(&r1, m + 4 * 1, sizeof(r1));
+    SIMD_INLINE_MEMCPY(&r2, m + 4 * 2, sizeof(r2));
+    SIMD_INLINE_MEMCPY(&r3, m + 4 * 3, sizeof(r3));
+    _MM_TRANSPOSE4_PS(r0, r1, r2, r3);
+    gvec<float, 4> c0, c1, c2, c3;
+    SIMD_INLINE_MEMCPY(&c0, &r0, sizeof(c0));
+    SIMD_INLINE_MEMCPY(&c1, &r1, sizeof(c1));
+    SIMD_INLINE_MEMCPY(&c2, &r2, sizeof(c2));
+    SIMD_INLINE_MEMCPY(&c3, &r3, sizeof(c3));
+    return {c0, c1, c2, c3};
+}
+#else
+SIMD_ALWAYS_INLINE std::tuple<gvec<float, 4>, gvec<float, 4>, gvec<float, 4>, gvec<float, 4>>
+load4x4f(const float* m)
+{
+    gvec<float, 4> c0 = {m[0], m[4], m[8], m[12]};
+    gvec<float, 4> c1 = {m[1], m[5], m[9], m[13]};
+    gvec<float, 4> c2 = {m[2], m[6], m[10], m[14]};
+    gvec<float, 4> c3 = {m[3], m[7], m[11], m[15]};
+    return {c0, c1, c2, c3};
+}
+#endif
+
+template <typename T, int M, int N>
+SIMD_ALWAYS_INLINE gvec<T, M + N> join(gvec<T, M> a, gvec<T, N> b)
+{
+    T data[M + N];
+    SIMD_INLINE_MEMCPY(data, &a, sizeof(T) * M);
+    SIMD_INLINE_MEMCPY(data + M, &b, sizeof(T) * N);
+    return load<T, M + N>(data);
+}
+
+template <typename T, int M, int N, int O>
+SIMD_ALWAYS_INLINE gvec<T, M + N + O> join(gvec<T, M> a, gvec<T, N> b, gvec<T, O> c)
+{
+    T data[M + N + O];
+    SIMD_INLINE_MEMCPY(data, &a, sizeof(T) * M);
+    SIMD_INLINE_MEMCPY(data + M, &b, sizeof(T) * N);
+    SIMD_INLINE_MEMCPY(data + M + N, &c, sizeof(T) * O);
+    return load<T, M + N + O>(data);
+}
+
+template <typename T, int M, int N, int O, int P>
+SIMD_ALWAYS_INLINE gvec<T, M + N + O + P> join(gvec<T, M> a,
+                                               gvec<T, N> b,
+                                               gvec<T, O> c,
+                                               gvec<T, P> d)
+{
+    T data[M + N + O + P];
+    SIMD_INLINE_MEMCPY(data, &a, sizeof(T) * M);
+    SIMD_INLINE_MEMCPY(data + M, &b, sizeof(T) * N);
+    SIMD_INLINE_MEMCPY(data + M + N, &c, sizeof(T) * O);
+    SIMD_INLINE_MEMCPY(data + M + N + O, &d, sizeof(T) * P);
+    return load<T, M + N + O + P>(data);
+}
+
+template <typename T> SIMD_ALWAYS_INLINE gvec<T, 4> zip(gvec<T, 2> a, gvec<T, 2> b)
+{
+#if __has_builtin(__builtin_shufflevector) && SIMD_NATIVE_GVEC
+    return __builtin_shufflevector(a, b, 0, 2, 1, 3);
+#else
+    return gvec<T, 4>{a.x, b.x, a.y, b.y};
+#endif
+}
+
+template <typename T> SIMD_ALWAYS_INLINE gvec<T, 8> zip(gvec<T, 4> a, gvec<T, 4> b)
+{
+#if __has_builtin(__builtin_shufflevector) && SIMD_NATIVE_GVEC
+    return __builtin_shufflevector(a, b, 0, 4, 1, 5, 2, 6, 3, 7);
+#else
+    return gvec<T, 8>{a.x, b.x, a.y, b.y, a.z, b.z, a.w, b.w};
+#endif
+}
+
+template <typename T> SIMD_ALWAYS_INLINE gvec<T, 16> zip(gvec<T, 8> a, gvec<T, 8> b)
+{
+#if __has_builtin(__builtin_shufflevector) && SIMD_NATIVE_GVEC
+    return __builtin_shufflevector(a, b, 0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15);
+#else
+    return gvec<T, 16>{a[0],
+                       b[0],
+                       a[1],
+                       b[1],
+                       a[2],
+                       b[2],
+                       a[3],
+                       b[3],
+                       a[4],
+                       b[4],
+                       a[5],
+                       b[5],
+                       a[6],
+                       b[6],
+                       a[7],
+                       b[7]};
+#endif
+}
+
+template <typename T, int N>
+SIMD_ALWAYS_INLINE typename std::enable_if<N != 2 && N != 4 && N != 8, gvec<T, N * 2>>::type zip(
+    gvec<T, N> a,
+    gvec<T, N> b)
+{
+    gvec<T, N * 2> ret{};
+    for (int i = 0; i < N; ++i)
+    {
+        ret[i * 2] = a[i];
+        ret[i * 2 + 1] = b[i];
+    }
+    return ret;
+}
+
+////// Basic linear algebra //////
+
+template <typename T, int N> SIMD_ALWAYS_INLINE T dot(gvec<T, N> a, gvec<T, N> b)
+{
+    return reduce_add(a * b);
+}
+
+SIMD_ALWAYS_INLINE float cross(gvec<float, 2> a, gvec<float, 2> b)
+{
+    auto c = a * b.yx;
+    return c.x - c.y;
+}
+
+// Linearly interpolates between a and b.
+//
+// NOTE: mix(a, b, 1) !== b (!!)
+//
+// The floating point numerics are not precise in the case where t === 1. But overall, this
+// structure seems to get better precision for things like chopping cubics on exact cusp points than
+// "a*(1 - t) + b*t" (which would return exactly b when t == 1).
+template <int N>
+SIMD_ALWAYS_INLINE gvec<float, N> mix(gvec<float, N> a, gvec<float, N> b, gvec<float, N> t)
+{
+    assert(simd::all(0.f <= t && t < 1.f));
+    return (b - a) * t + a;
+}
+
+// Linearly interpolates between a and b, returning precisely 'a' if t==0 and precisely 'b' if t==1.
+template <int N>
+SIMD_ALWAYS_INLINE gvec<float, N> precise_mix(gvec<float, N> a, gvec<float, N> b, gvec<float, N> t)
+{
+    return a * (1.f - t) + b * t;
+}
+} // namespace simd
+} // namespace rive
+
+namespace rive
+{
+template <int N> using vec = simd::gvec<float, N>;
+using float2 = vec<2>;
+using float4 = vec<4>;
+
+template <int N> using ivec = simd::gvec<int32_t, N>;
+using int2 = ivec<2>;
+using int4 = ivec<4>;
+
+template <int N> using uvec = simd::gvec<uint32_t, N>;
+using uint2 = uvec<2>;
+using uint4 = uvec<4>;
+
+using int8x8 = simd::gvec<int8_t, 8>;
+using int8x16 = simd::gvec<int8_t, 16>;
+using int8x32 = simd::gvec<int8_t, 32>;
+
+using uint8x8 = simd::gvec<uint8_t, 8>;
+using uint8x16 = simd::gvec<uint8_t, 16>;
+using uint8x32 = simd::gvec<uint8_t, 32>;
+
+using int16x4 = simd::gvec<int16_t, 4>;
+using int16x8 = simd::gvec<int16_t, 8>;
+using int16x16 = simd::gvec<int16_t, 16>;
+
+using uint16x4 = simd::gvec<uint16_t, 4>;
+using uint16x8 = simd::gvec<uint16_t, 8>;
+using uint16x16 = simd::gvec<uint16_t, 16>;
+
+using int64x2 = simd::gvec<int64_t, 2>;
+using int64x4 = simd::gvec<int64_t, 4>;
+
+using uint64x2 = simd::gvec<uint64_t, 2>;
+using uint64x4 = simd::gvec<uint64_t, 4>;
+
+} // namespace rive
+
+#undef SIMD_INLINE_MEMCPY
+#undef SIMD_ALWAYS_INLINE
+
+#endif
diff --git a/include/rive/math/simd_gvec_polyfill.hpp b/include/rive/math/simd_gvec_polyfill.hpp
new file mode 100644
index 0000000..e9eb964
--- /dev/null
+++ b/include/rive/math/simd_gvec_polyfill.hpp
@@ -0,0 +1,370 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+// This header provides a fallback gvec<> implementation for when we don't have gcc/clang vector
+// extensions. Swizzles are implemented as unions, which is questionably undefined due to the
+// "active member" C++ restriction on unions, however, since the members all have the same
+// underlying type, this is a gray area. See:
+//
+// https://stackoverflow.com/questions/11373203/accessing-inactive-union-member-and-undefined-behavior
+//
+// This works in Visual Studio, which is the main reason for having this header.
+
+#ifndef _RIVE_SIMD_GVEC_POLYFILL_HPP_
+#define _RIVE_SIMD_GVEC_POLYFILL_HPP_
+
+#include <algorithm>
+#include <initializer_list>
+#include <stdint.h>
+#include <cstring>
+
+namespace rive
+{
+namespace simd
+{
+using Swizzle = uint32_t;
+constexpr static Swizzle PackSwizzle2(uint32_t sourceVectorLength, uint32_t i0, uint32_t i1)
+{
+    return (i1 << 5) | (i0 << 3) | sourceVectorLength;
+}
+constexpr static Swizzle PackSwizzle4(uint32_t sourceVectorLength,
+                                      uint32_t i0,
+                                      uint32_t i1,
+                                      uint32_t i2,
+                                      uint32_t i3)
+{
+    return (i3 << 9) | (i2 << 7) | PackSwizzle2(sourceVectorLength, i0, i1);
+}
+constexpr static uint32_t UnpackSwizzleSourceVectorLength(Swizzle swizzle) { return swizzle & 7; }
+constexpr static uint32_t UnpackSwizzleIdx(Swizzle swizzle, uint32_t i)
+{
+    return (swizzle >> (i * 2 + 3)) & 3;
+}
+
+template <typename T, int N, Swizzle Z = 0> struct gvec
+{
+    T operator[](size_t i) const { return data[UnpackSwizzleIdx(Z, i)]; }
+    T& operator[](size_t i) { return data[UnpackSwizzleIdx(Z, i)]; }
+    operator gvec<T, N>() const
+    {
+        gvec<T, N> ret;
+        for (int i = 0; i < N; ++i)
+            ret[i] = (*this)[i];
+        return ret;
+    }
+    T data[UnpackSwizzleSourceVectorLength(Z)];
+};
+
+template <typename T, int N> struct gvec_data
+{
+    T data[N];
+};
+
+template <typename T> struct gvec_data<T, 1>
+{
+    union
+    {
+        T data[1];
+        T x;
+    };
+};
+
+template <typename T> struct gvec_data<T, 2>
+{
+    union
+    {
+        T data[2];
+        struct
+        {
+            T x, y;
+        };
+        gvec<T, 2, PackSwizzle2(2, 1, 0)> yx;
+        gvec<T, 4, PackSwizzle4(2, 0, 1, 0, 1)> xyxy;
+        gvec<T, 4, PackSwizzle4(2, 1, 0, 1, 0)> yxyx;
+    };
+};
+
+template <typename T> struct gvec_data<T, 3>
+{
+    union
+    {
+        T data[2];
+        struct
+        {
+            T x, y, z;
+        };
+    };
+};
+
+template <typename T> struct gvec_data<T, 4>
+{
+    union
+    {
+        T data[4];
+        gvec<T, 3> xyz;
+        struct
+        {
+            gvec<T, 2> xy, zw;
+        };
+        struct
+        {
+            T x;
+            union
+            {
+                gvec<T, 3> yzw;
+                gvec<T, 2> yz;
+                struct
+                {
+                    T y, z, w;
+                };
+            };
+        };
+        // **WARNING**!! Only add swizzles that include ALL components of the vector. Since these
+        // types are POD, it's not possible to overwrite their default operator=, and their default
+        // operator= is just a memcpy. So: "float.xz = float4.xz" would also assign y and w.
+        gvec<T, 4, PackSwizzle4(4, 1, 0, 3, 2)> yxwz;
+        gvec<T, 4, PackSwizzle4(4, 2, 3, 0, 1)> zwxy;
+        gvec<T, 4, PackSwizzle4(4, 2, 1, 0, 3)> zyxw;
+        gvec<T, 4, PackSwizzle4(4, 0, 3, 2, 1)> xwzy;
+    };
+};
+
+template <typename T, int N> struct gvec<T, N, 0> : public gvec_data<T, N>
+{
+    gvec() = default;
+    gvec(T val)
+    {
+        for (int i = 0; i < N; ++i)
+            gvec_data<T, N>::data[i] = val;
+    }
+    gvec(std::initializer_list<T> vals)
+    {
+        memset(gvec_data<T, N>::data, 0, sizeof(gvec_data<T, N>::data));
+        std::copy(vals.begin(),
+                  vals.begin() + std::min<size_t>(vals.size(), N),
+                  gvec_data<T, N>::data);
+    }
+    T operator[](size_t i) const { return gvec_data<T, N>::data[i]; }
+    T& operator[](size_t i) { return gvec_data<T, N>::data[i]; }
+};
+
+static_assert(sizeof(gvec<float, 1>) == 4, "gvec<1> is expected to be tightly packed");
+static_assert(sizeof(gvec<float, 2>) == 8, "gvec<2> is expected to be tightly packed");
+static_assert(sizeof(gvec<float, 4>) == 16, "gvec<4> is expected to be tightly packed");
+
+// Vector booleans are masks of integer type, where true is -1 and false is 0. Vector booleans masks
+// are generated using the builtin boolean operators: ==, !=, <=, >=, <, >
+template <size_t> struct boolean_mask_type_by_size;
+template <> struct boolean_mask_type_by_size<1>
+{
+    using type = int8_t;
+};
+template <> struct boolean_mask_type_by_size<2>
+{
+    using type = int16_t;
+};
+template <> struct boolean_mask_type_by_size<4>
+{
+    using type = int32_t;
+};
+template <> struct boolean_mask_type_by_size<8>
+{
+    using type = int64_t;
+};
+template <typename T> struct boolean_mask_type
+{
+    using type = typename boolean_mask_type_by_size<sizeof(T)>::type;
+};
+
+#define DECL_UNARY_OP(_OP_)                                                                        \
+    template <typename T, int N, Swizzle Z> gvec<T, N> operator _OP_(gvec<T, N, Z> x)              \
+    {                                                                                              \
+        gvec<T, N> ret;                                                                            \
+        for (int i = 0; i < N; ++i)                                                                \
+            ret[i] = _OP_ x[i];                                                                    \
+        return ret;                                                                                \
+    }
+
+DECL_UNARY_OP(+)
+DECL_UNARY_OP(-)
+DECL_UNARY_OP(~)
+
+#undef DECL_UNARY_OP
+
+#define DECL_ARITHMETIC_OP(_OP_)                                                                   \
+    template <typename T, typename U, int N, Swizzle Z0, Swizzle Z1>                               \
+    gvec<T, N, Z0>& operator _OP_##=(gvec<T, N, Z0>& a, gvec<U, N, Z1> b)                          \
+    {                                                                                              \
+        for (int i = 0; i < N; ++i)                                                                \
+            a[i] _OP_## = b[i];                                                                    \
+        return a;                                                                                  \
+    }                                                                                              \
+    template <typename T, typename U, int N, Swizzle Z>                                            \
+    gvec<T, N, Z>& operator _OP_##=(gvec<T, N, Z>& a, U b)                                         \
+    {                                                                                              \
+        for (int i = 0; i < N; ++i)                                                                \
+            a[i] _OP_## = b;                                                                       \
+        return a;                                                                                  \
+    }                                                                                              \
+    template <typename T, typename U, int N, Swizzle Z0, Swizzle Z1>                               \
+    gvec<T, N> operator _OP_(gvec<T, N, Z0> a, gvec<U, N, Z1> b)                                   \
+    {                                                                                              \
+        gvec<T, N> ret;                                                                            \
+        for (int i = 0; i < N; ++i)                                                                \
+            ret[i] = a[i] _OP_ b[i];                                                               \
+        return ret;                                                                                \
+    }                                                                                              \
+    template <typename T, typename U, int N, Swizzle Z>                                            \
+    gvec<T, N> operator _OP_(gvec<T, N, Z> a, U b)                                                 \
+    {                                                                                              \
+        gvec<T, N> ret;                                                                            \
+        for (int i = 0; i < N; ++i)                                                                \
+            ret[i] = a[i] _OP_ b;                                                                  \
+        return ret;                                                                                \
+    }                                                                                              \
+    template <typename T, typename U, int N, Swizzle Z>                                            \
+    gvec<U, N> operator _OP_(T a, gvec<U, N, Z> b)                                                 \
+    {                                                                                              \
+        gvec<T, N> ret;                                                                            \
+        for (int i = 0; i < N; ++i)                                                                \
+            ret[i] = a _OP_ b[i];                                                                  \
+        return ret;                                                                                \
+    }
+
+DECL_ARITHMETIC_OP(+);
+DECL_ARITHMETIC_OP(-);
+DECL_ARITHMETIC_OP(*);
+DECL_ARITHMETIC_OP(/);
+DECL_ARITHMETIC_OP(%);
+DECL_ARITHMETIC_OP(|);
+DECL_ARITHMETIC_OP(&);
+DECL_ARITHMETIC_OP(^);
+DECL_ARITHMETIC_OP(<<);
+DECL_ARITHMETIC_OP(>>);
+
+#undef DECL_ARITHMETIC_OP
+
+#define DECL_BOOLEAN_OP(_OP_)                                                                      \
+    template <typename T, int N, Swizzle Z0, Swizzle Z1>                                           \
+    gvec<typename boolean_mask_type<T>::type, N> operator _OP_(gvec<T, N, Z0> a, gvec<T, N, Z1> b) \
+    {                                                                                              \
+        gvec<typename boolean_mask_type<T>::type, N> ret;                                          \
+        for (int i = 0; i < N; ++i)                                                                \
+            ret[i] = a[i] _OP_ b[i] ? ~0 : 0;                                                      \
+        return ret;                                                                                \
+    }                                                                                              \
+    template <typename T, typename U, int N, Swizzle Z>                                            \
+    gvec<typename boolean_mask_type<T>::type, N> operator _OP_(gvec<T, N, Z> a, U b)               \
+    {                                                                                              \
+        gvec<typename boolean_mask_type<T>::type, N> ret;                                          \
+        for (int i = 0; i < N; ++i)                                                                \
+            ret[i] = a[i] _OP_ b ? ~0 : 0;                                                         \
+        return ret;                                                                                \
+    }                                                                                              \
+    template <typename T, typename U, int N, Swizzle Z>                                            \
+    gvec<typename boolean_mask_type<T>::type, N> operator _OP_(U a, gvec<T, N, Z> b)               \
+    {                                                                                              \
+        gvec<typename boolean_mask_type<T>::type, N> ret;                                          \
+        for (int i = 0; i < N; ++i)                                                                \
+            ret[i] = a _OP_ b[i] ? ~0 : 0;                                                         \
+        return ret;                                                                                \
+    }
+
+DECL_BOOLEAN_OP(==)
+DECL_BOOLEAN_OP(!=)
+DECL_BOOLEAN_OP(<)
+DECL_BOOLEAN_OP(<=)
+DECL_BOOLEAN_OP(>)
+DECL_BOOLEAN_OP(>=)
+DECL_BOOLEAN_OP(&&)
+DECL_BOOLEAN_OP(||)
+
+#undef DECL_BOOLEAN_OP
+
+#define ENABLE_SWIZZLE1(F)                                                                         \
+    template <typename T, int N, Swizzle Z0> gvec<T, N> F(gvec<T, N, Z0> x)                        \
+    {                                                                                              \
+        return F((gvec<T, N>)x);                                                                   \
+    }
+#define ENABLE_SWIZZLE_REDUCE(F)                                                                   \
+    template <typename T, int N, Swizzle Z0> T F(gvec<T, N, Z0> x) { return F((gvec<T, N>)x); }
+#define ENABLE_SWIZZLE1F(F)                                                                        \
+    template <int N, Swizzle Z0> gvec<float, N> F(gvec<float, N, Z0> x)                            \
+    {                                                                                              \
+        return F((gvec<float, N>)x);                                                               \
+    }
+#define ENABLE_SWIZZLE1B(F)                                                                        \
+    template <typename T, int N, Swizzle Z0> bool F(gvec<T, N, Z0> x) { return F((gvec<T, N>)x); }
+#define ENABLE_SWIZZLEUT(F)                                                                        \
+    template <typename T, typename U, int N, Swizzle Z0> gvec<U, N> F(gvec<T, N, Z0> x)            \
+    {                                                                                              \
+        return F((gvec<T, N>)x);                                                                   \
+    }
+#define ENABLE_SWIZZLE2(F)                                                                         \
+    template <typename T, int N, Swizzle Z0, Swizzle Z1>                                           \
+    gvec<T, N> F(gvec<T, N, Z0> a, gvec<T, N, Z1> b)                                               \
+    {                                                                                              \
+        return F((gvec<T, N>)a, (gvec<T, N>)b);                                                    \
+    }
+#define ENABLE_SWIZZLE3(F)                                                                         \
+    template <typename T, int N, Swizzle Z0, Swizzle Z1, Swizzle Z2>                               \
+    gvec<T, N> F(gvec<T, N, Z0> a, gvec<T, N, Z1> b, gvec<T, N, Z2> c)                             \
+    {                                                                                              \
+        return F((gvec<T, N>)a, (gvec<T, N>)b, (gvec<T, N>)c);                                     \
+    }
+#define ENABLE_SWIZZLE3F(F)                                                                        \
+    template <int N, Swizzle Z0, Swizzle Z1, Swizzle Z2>                                           \
+    gvec<float, N> F(gvec<float, N, Z0> a, gvec<float, N, Z1> b, gvec<float, N, Z2> c)             \
+    {                                                                                              \
+        return F((gvec<float, N>)a, (gvec<float, N>)b, (gvec<float, N>)c);                         \
+    }
+#define ENABLE_SWIZZLE3IT(F)                                                                       \
+    template <typename T, int N, Swizzle Z0, Swizzle Z1, Swizzle Z2>                               \
+    gvec<T, N> F(gvec<typename boolean_mask_type<T>::type, N, Z0> a,                               \
+                 gvec<T, N, Z1> b,                                                                 \
+                 gvec<T, N, Z2> c)                                                                 \
+    {                                                                                              \
+        return F((gvec<int32_t, N>)a, (gvec<T, N>)b, (gvec<T, N>)c);                               \
+    }
+
+ENABLE_SWIZZLE1(abs)
+ENABLE_SWIZZLE_REDUCE(reduce_add)
+ENABLE_SWIZZLE_REDUCE(reduce_min)
+ENABLE_SWIZZLE_REDUCE(reduce_max)
+ENABLE_SWIZZLE_REDUCE(reduce_and)
+ENABLE_SWIZZLE_REDUCE(reduce_or)
+ENABLE_SWIZZLE1F(floor)
+ENABLE_SWIZZLE1F(ceil)
+ENABLE_SWIZZLE1F(sqrt)
+ENABLE_SWIZZLE1F(fast_acos)
+ENABLE_SWIZZLE1B(any)
+ENABLE_SWIZZLE1B(all)
+ENABLE_SWIZZLE2(min)
+ENABLE_SWIZZLE2(max)
+ENABLE_SWIZZLE3(clamp)
+ENABLE_SWIZZLE3F(mix)
+ENABLE_SWIZZLE3F(precise_mix)
+ENABLE_SWIZZLE3IT(if_then_else)
+template <typename T, int N, Swizzle Z> void store(void* dst, gvec<T, N, Z> vec)
+{
+    store(dst, (gvec<T, N>)vec);
+}
+template <typename U, typename T, int N, Swizzle Z> gvec<U, N> cast(gvec<T, N, Z> x)
+{
+    return cast<U>((gvec<T, N>)x);
+}
+
+#undef ENABLE_SWIZZLE1
+#undef ENABLE_SWIZZLE_REDUCE
+#undef ENABLE_SWIZZLE1F
+#undef ENABLE_SWIZZLE1B
+#undef ENABLE_SWIZZLEUT
+#undef ENABLE_SWIZZLE2
+#undef ENABLE_SWIZZLE3
+#undef ENABLE_SWIZZLE3F
+#undef ENABLE_SWIZZLE3IT
+} // namespace simd
+} // namespace rive
+
+#endif
diff --git a/include/rive/math/transform_components.hpp b/include/rive/math/transform_components.hpp
new file mode 100644
index 0000000..d3726da
--- /dev/null
+++ b/include/rive/math/transform_components.hpp
@@ -0,0 +1,59 @@
+#ifndef _RIVE_TRANSFORMCOMPONENTS_HPP_
+#define _RIVE_TRANSFORMCOMPONENTS_HPP_
+
+#include "rive/math/vec2d.hpp"
+
+namespace rive
+{
+class TransformComponents
+{
+private:
+    float m_X;
+    float m_Y;
+    float m_ScaleX;
+    float m_ScaleY;
+    float m_Rotation;
+    float m_Skew;
+
+public:
+    TransformComponents() :
+        m_X(0.0f), m_Y(0.0f), m_ScaleX(1.0f), m_ScaleY(1.0f), m_Rotation(0.0f), m_Skew(0.0f)
+    {}
+    TransformComponents(const TransformComponents& copy) :
+        m_X(copy.m_X),
+        m_Y(copy.m_Y),
+        m_ScaleX(copy.m_ScaleX),
+        m_ScaleY(copy.m_ScaleY),
+        m_Rotation(copy.m_Rotation),
+        m_Skew(copy.m_Skew)
+    {}
+
+    float x() const { return m_X; }
+    void x(float value) { m_X = value; }
+    float y() const { return m_Y; }
+    void y(float value) { m_Y = value; }
+    float scaleX() const { return m_ScaleX; }
+    void scaleX(float value) { m_ScaleX = value; }
+    float scaleY() const { return m_ScaleY; }
+    void scaleY(float value) { m_ScaleY = value; }
+    float rotation() const { return m_Rotation; }
+    void rotation(float value) { m_Rotation = value; }
+    float skew() const { return m_Skew; }
+    void skew(float value) { m_Skew = value; }
+
+    Vec2D translation() const { return {m_X, m_Y}; }
+    Vec2D scale() const { return {m_ScaleX, m_ScaleY}; }
+
+    TransformComponents& operator=(const TransformComponents& a)
+    {
+        m_X = a.m_X;
+        m_Y = a.m_Y;
+        m_ScaleX = a.m_ScaleX;
+        m_ScaleY = a.m_ScaleY;
+        m_Rotation = a.m_Rotation;
+        m_Skew = a.m_Skew;
+        return *this;
+    }
+};
+} // namespace rive
+#endif
diff --git a/include/rive/math/vec2d.hpp b/include/rive/math/vec2d.hpp
new file mode 100644
index 0000000..1a431e8
--- /dev/null
+++ b/include/rive/math/vec2d.hpp
@@ -0,0 +1,93 @@
+#ifndef _RIVE_VEC2D_HPP_
+#define _RIVE_VEC2D_HPP_
+
+#include "rive/rive_types.hpp"
+
+namespace rive
+{
+class Mat2D;
+class Vec2D
+{
+public:
+    float x, y;
+
+    Vec2D() = default;
+    constexpr Vec2D(float x, float y) : x(x), y(y) {}
+
+    float lengthSquared() const { return x * x + y * y; }
+    float length() const;
+    Vec2D normalized() const;
+
+    // Normalize this Vec, and return its previous length
+    float normalizeLength()
+    {
+        const float len = this->length();
+        if (len > 0)
+        {
+            x /= len;
+            y /= len;
+        }
+        return len;
+    }
+
+    Vec2D operator-() const { return {-x, -y}; }
+
+    void operator*=(float s)
+    {
+        x *= s;
+        y *= s;
+    }
+
+    void operator/=(float s)
+    {
+        x /= s;
+        y /= s;
+    }
+
+    friend inline Vec2D operator-(const Vec2D& a, const Vec2D& b) { return {a.x - b.x, a.y - b.y}; }
+
+    static inline Vec2D lerp(Vec2D a, Vec2D b, float f);
+
+    static Vec2D transformDir(const Vec2D& a, const Mat2D& m);
+    static Vec2D transformMat2D(const Vec2D& a, const Mat2D& m);
+
+    static float dot(Vec2D a, Vec2D b) { return a.x * b.x + a.y * b.y; }
+    static float cross(Vec2D a, Vec2D b) { return a.x * b.y - a.y * b.x; }
+    static Vec2D scaleAndAdd(Vec2D a, Vec2D b, float scale)
+    {
+        return {
+            a.x + b.x * scale,
+            a.y + b.y * scale,
+        };
+    }
+    static float distance(const Vec2D& a, const Vec2D& b) { return (a - b).length(); }
+    static float distanceSquared(const Vec2D& a, const Vec2D& b) { return (a - b).lengthSquared(); }
+
+    Vec2D& operator+=(Vec2D v)
+    {
+        x += v.x;
+        y += v.y;
+        return *this;
+    }
+    Vec2D& operator-=(Vec2D v)
+    {
+        x -= v.x;
+        y -= v.y;
+        return *this;
+    }
+};
+static_assert(std::is_pod<Vec2D>::value, "Vec2D must be plain-old-data");
+
+inline Vec2D operator*(const Vec2D& v, float s) { return {v.x * s, v.y * s}; }
+inline Vec2D operator*(float s, const Vec2D& v) { return {v.x * s, v.y * s}; }
+inline Vec2D operator/(const Vec2D& v, float s) { return {v.x / s, v.y / s}; }
+
+inline Vec2D operator+(const Vec2D& a, const Vec2D& b) { return {a.x + b.x, a.y + b.y}; }
+
+inline bool operator==(const Vec2D& a, const Vec2D& b) { return a.x == b.x && a.y == b.y; }
+inline bool operator!=(const Vec2D& a, const Vec2D& b) { return a.x != b.x || a.y != b.y; }
+
+Vec2D Vec2D::lerp(Vec2D a, Vec2D b, float t) { return a + (b - a) * t; }
+
+} // namespace rive
+#endif
diff --git a/include/rive/math/wangs_formula.hpp b/include/rive/math/wangs_formula.hpp
new file mode 100644
index 0000000..aa1f3c4
--- /dev/null
+++ b/include/rive/math/wangs_formula.hpp
@@ -0,0 +1,283 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ *
+ * Initial import from skia:src/gpu/tessellate/WangsFormula.h
+ *
+ * Copyright 2023 Rive
+ */
+
+#pragma once
+
+#include "rive/math/simd.hpp"
+#include "rive/math/vec2d.hpp"
+#include "rive/math/mat2d.hpp"
+#include <math.h>
+
+#define AI RIVE_MAYBE_UNUSED RIVE_ALWAYS_INLINE
+
+// Wang's formula gives the minimum number of evenly spaced (in the parametric sense) line segments
+// that a bezier curve must be chopped into in order to guarantee all lines stay within a distance
+// of "1/precision" pixels from the true curve. Its definition for a bezier curve of degree "n" is
+// as follows:
+//
+//     maxLength = max([length(p[i+2] - 2p[i+1] + p[i]) for (0 <= i <= n-2)])
+//     numParametricSegments = sqrt(maxLength * precision * n*(n - 1)/8)
+//
+// (Goldman, Ron. (2003). 5.6.3 Wang's Formula. "Pyramid Algorithms: A Dynamic Programming Approach
+// to Curves and Surfaces for Geometric Modeling". Morgan Kaufmann Publishers.)
+namespace rive
+{
+namespace wangs_formula
+{
+// Returns the value by which to multiply length in Wang's formula. (See above.)
+template <int Degree> constexpr float length_term(float precision)
+{
+    return (Degree * (Degree - 1) / 8.f) * precision;
+}
+template <int Degree> constexpr float length_term_pow2(float precision)
+{
+    return ((Degree * Degree) * ((Degree - 1) * (Degree - 1)) / 64.f) * (precision * precision);
+}
+
+AI static float root4(float x) { return sqrtf(sqrtf(x)); }
+
+// Returns the log2 of the provided value, were that value to be rounded up to the next power of 2.
+// Returns 0 if value <= 0:
+// Never returns a negative number, even if value is NaN.
+//
+//     sk_float_nextlog2((-inf..1]) -> 0
+//     sk_float_nextlog2((1..2]) -> 1
+//     sk_float_nextlog2((2..4]) -> 2
+//     sk_float_nextlog2((4..8]) -> 3
+//     ...
+AI static int sk_float_nextlog2(float x)
+{
+    uint32_t bits;
+    RIVE_INLINE_MEMCPY(&bits, &x, 4);
+    bits += (1u << 23) - 1u; // Increment the exponent for non-powers-of-2.
+    int exp = ((int32_t)bits >> 23) - 127;
+    return exp & ~(exp >> 31); // Return 0 for negative or denormalized floats, and exponents < 0.
+}
+
+// Returns nextlog2(sqrt(x)):
+//
+//   log2(sqrt(x)) == log2(x^(1/2)) == log2(x)/2 == log2(x)/log2(4) == log4(x)
+//
+AI static int nextlog4(float x) { return (sk_float_nextlog2(x) + 1) >> 1; }
+
+// Returns nextlog2(sqrt(sqrt(x))):
+//
+//   log2(sqrt(sqrt(x))) == log2(x^(1/4)) == log2(x)/4 == log2(x)/log2(16) == log16(x)
+//
+AI static int nextlog16(float x) { return (sk_float_nextlog2(x) + 3) >> 2; }
+
+// Represents the upper-left 2x2 matrix of an affine transform for applying to vectors:
+//
+//     VectorXform(p1 - p0) == M * float3(p1, 1) - M * float3(p0, 1)
+//
+class alignas(32) VectorXform
+{
+public:
+    AI VectorXform() : m_scale(1), m_skew(0) {}
+    AI explicit VectorXform(const Mat2D& m) { *this = m; }
+
+    AI VectorXform& operator=(const Mat2D& m)
+    {
+        m_scale = float2{m[0], m[3]}.xyxy;
+        m_skew = simd::load2f(&m[1]).yxyx;
+        return *this;
+    }
+
+    AI float2 operator()(float2 vector) const
+    {
+        return m_scale.xy * vector + m_skew.xy * vector.yx;
+    }
+    AI float4 operator()(float4 vectors) const { return m_scale * vectors + m_skew * vectors.yxwz; }
+
+private:
+    float4 m_scale;
+    float4 m_skew;
+};
+
+// Returns Wang's formula, raised to the 4th power, specialized for a quadratic curve.
+AI static float quadratic_pow4(float2 p0,
+                               float2 p1,
+                               float2 p2,
+                               float precision,
+                               const VectorXform& vectorXform = VectorXform())
+{
+    float2 v = -2.f * p1 + p0 + p2;
+    v = vectorXform(v);
+    float2 vv = v * v;
+    return (vv[0] + vv[1]) * length_term_pow2<2>(precision);
+}
+AI static float quadratic_pow4(const Vec2D pts[],
+                               float precision,
+                               const VectorXform& vectorXform = VectorXform())
+{
+    return quadratic_pow4(simd::load2f(&pts[0].x),
+                          simd::load2f(&pts[1].x),
+                          simd::load2f(&pts[2].x),
+                          precision,
+                          vectorXform);
+}
+
+// Returns Wang's formula specialized for a quadratic curve.
+AI static float quadratic(const Vec2D pts[],
+                          float precision,
+                          const VectorXform& vectorXform = VectorXform())
+{
+    return root4(quadratic_pow4(pts, precision, vectorXform));
+}
+
+// Returns the log2 value of Wang's formula specialized for a quadratic curve, rounded up to the
+// next int.
+AI static int quadratic_log2(const Vec2D pts[],
+                             float precision,
+                             const VectorXform& vectorXform = VectorXform())
+{
+    // nextlog16(x) == ceil(log2(sqrt(sqrt(x))))
+    return nextlog16(quadratic_pow4(pts, precision, vectorXform));
+}
+
+// Returns Wang's formula, raised to the 4th power, specialized for a cubic curve.
+AI static float cubic_pow4(const Vec2D pts[],
+                           float precision,
+                           const VectorXform& vectorXform = VectorXform())
+{
+    float4 p01 = simd::load4f(pts);
+    float4 p12 = simd::load4f(pts + 1);
+    float4 p23 = simd::load4f(pts + 2);
+    float4 v = -2.f * p12 + p01 + p23;
+    v = vectorXform(v);
+    float4 vv = v * v;
+    return std::max(vv[0] + vv[1], vv[2] + vv[3]) * length_term_pow2<3>(precision);
+}
+
+// Returns Wang's formula specialized for a cubic curve.
+AI static float cubic(const Vec2D pts[],
+                      float precision,
+                      const VectorXform& vectorXform = VectorXform())
+{
+    return root4(cubic_pow4(pts, precision, vectorXform));
+}
+
+// Returns the log2 value of Wang's formula specialized for a cubic curve, rounded up to the next
+// int.
+AI static int cubic_log2(const Vec2D pts[],
+                         float precision,
+                         const VectorXform& vectorXform = VectorXform())
+{
+    // nextlog16(x) == ceil(log2(sqrt(sqrt(x))))
+    return nextlog16(cubic_pow4(pts, precision, vectorXform));
+}
+
+// Returns the maximum number of line segments a cubic with the given device-space bounding box size
+// would ever need to be divided into, raised to the 4th power. This is simply a special case of the
+// cubic formula where we maximize its value by placing control points on specific corners of the
+// bounding box.
+AI static float worst_case_cubic_pow4(float devWidth, float devHeight, float precision)
+{
+    float kk = length_term_pow2<3>(precision);
+    return 4 * kk * (devWidth * devWidth + devHeight * devHeight);
+}
+
+// Returns the maximum number of line segments a cubic with the given device-space bounding box size
+// would ever need to be divided into.
+AI static float worst_case_cubic(float devWidth, float devHeight, float precision)
+{
+    return root4(worst_case_cubic_pow4(devWidth, devHeight, precision));
+}
+
+// Returns the maximum log2 number of line segments a cubic with the given device-space bounding box
+// size would ever need to be divided into.
+AI static int worst_case_cubic_log2(float devWidth, float devHeight, float precision)
+{
+    // nextlog16(x) == ceil(log2(sqrt(sqrt(x))))
+    return nextlog16(worst_case_cubic_pow4(devWidth, devHeight, precision));
+}
+
+// Returns Wang's formula specialized for a conic curve, raised to the second power.
+// Input points should be in projected space.
+//
+// This is not actually due to Wang, but is an analogue from (Theorem 3, corollary 1):
+//   J. Zheng, T. Sederberg. "Estimating Tessellation Parameter Intervals for
+//   Rational Curves and Surfaces." ACM Transactions on Graphics 19(1). 2000.
+AI static float conic_pow2(float precision,
+                           float2 p0,
+                           float2 p1,
+                           float2 p2,
+                           float w,
+                           const VectorXform& vectorXform = VectorXform())
+{
+    p0 = vectorXform(p0);
+    p1 = vectorXform(p1);
+    p2 = vectorXform(p2);
+
+    // Compute center of bounding box in projected space
+    const float2 C = 0.5f * (simd::min(simd::min(p0, p1), p2) + simd::max(simd::max(p0, p1), p2));
+
+    // Translate by -C. This improves translation-invariance of the formula,
+    // see Sec. 3.3 of cited paper
+    p0 -= C;
+    p1 -= C;
+    p2 -= C;
+
+    // Compute max length
+    const float max_len =
+        sqrtf(std::max(simd::dot(p0, p0), std::max(simd::dot(p1, p1), simd::dot(p2, p2))));
+
+    // Compute forward differences
+    const float2 dp = -2.f * w * p1 + p0 + p2;
+    const float dw = fabsf(-2.f * w + 2);
+
+    // Compute numerator and denominator for parametric step size of linearization. Here, the
+    // epsilon referenced from the cited paper is 1/precision.
+    const float rp_minus_1 = std::max(0.f, max_len * precision - 1);
+    const float numer = sqrtf(simd::dot(dp, dp)) * precision + rp_minus_1 * dw;
+    const float denom = 4 * std::min(w, 1.f);
+
+    // Number of segments = sqrt(numer / denom).
+    // This assumes parametric interval of curve being linearized is [t0,t1] = [0, 1].
+    // If not, the number of segments is (tmax - tmin) / sqrt(denom / numer).
+    return numer / denom;
+}
+AI static float conic_pow2(float precision,
+                           const Vec2D pts[],
+                           float w,
+                           const VectorXform& vectorXform = VectorXform())
+{
+    return conic_pow2(precision,
+                      simd::load2f(&pts[0].x),
+                      simd::load2f(&pts[1].x),
+                      simd::load2f(&pts[2].x),
+                      w,
+                      vectorXform);
+}
+
+// Returns the value of Wang's formula specialized for a conic curve.
+AI static float conic(float tolerance,
+                      const Vec2D pts[],
+                      float w,
+                      const VectorXform& vectorXform = VectorXform())
+{
+    return sqrtf(conic_pow2(tolerance, pts, w, vectorXform));
+}
+
+// Returns the log2 value of Wang's formula specialized for a conic curve, rounded up to the next
+// int.
+AI static int conic_log2(float tolerance,
+                         const Vec2D pts[],
+                         float w,
+                         const VectorXform& vectorXform = VectorXform())
+{
+    // nextlog4(x) == ceil(log2(sqrt(x)))
+    return nextlog4(conic_pow2(tolerance, pts, w, vectorXform));
+}
+} // namespace wangs_formula
+} // namespace rive
+
+#undef AI
diff --git a/include/rive/nested_animation.hpp b/include/rive/nested_animation.hpp
new file mode 100644
index 0000000..ac14f36
--- /dev/null
+++ b/include/rive/nested_animation.hpp
@@ -0,0 +1,68 @@
+#ifndef _RIVE_NESTED_ANIMATION_HPP_
+#define _RIVE_NESTED_ANIMATION_HPP_
+#include "rive/event.hpp"
+#include "rive/event_report.hpp"
+#include "rive/generated/nested_animation_base.hpp"
+#include "rive/nested_artboard.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ArtboardInstance;
+
+class NestedEventListener
+{
+public:
+    virtual ~NestedEventListener() {}
+    virtual void notify(const std::vector<EventReport>& events, NestedArtboard* context) = 0;
+};
+
+class NestedEventNotifier
+{
+public:
+    ~NestedEventNotifier()
+    {
+        m_nestedArtboard = nullptr;
+        m_nestedEventListeners.clear();
+    }
+    void addNestedEventListener(NestedEventListener* listener)
+    {
+        m_nestedEventListeners.push_back(listener);
+    }
+    std::vector<NestedEventListener*> nestedEventListeners() { return m_nestedEventListeners; }
+
+    void setNestedArtboard(NestedArtboard* artboard) { m_nestedArtboard = artboard; }
+    NestedArtboard* nestedArtboard() { return m_nestedArtboard; }
+
+    void notifyListeners(const std::vector<Event*>& events)
+    {
+        std::vector<EventReport> eventReports;
+        for (auto event : events)
+        {
+            eventReports.push_back(EventReport(event, 0));
+        }
+        for (auto listener : m_nestedEventListeners)
+        {
+            listener->notify(eventReports, m_nestedArtboard);
+        }
+    }
+
+private:
+    NestedArtboard* m_nestedArtboard = nullptr;
+    std::vector<NestedEventListener*> m_nestedEventListeners;
+};
+
+class NestedAnimation : public NestedAnimationBase
+{
+public:
+    StatusCode onAddedDirty(CoreContext* context) override;
+
+    // Advance animations and apply them to the artboard.
+    virtual bool advance(float elapsedSeconds) = 0;
+
+    // Initialize the animation (make instances as necessary) from the
+    // source artboard.
+    virtual void initializeAnimation(ArtboardInstance*) = 0;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/nested_artboard.hpp b/include/rive/nested_artboard.hpp
new file mode 100644
index 0000000..6b97708
--- /dev/null
+++ b/include/rive/nested_artboard.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_NESTED_ARTBOARD_HPP_
+#define _RIVE_NESTED_ARTBOARD_HPP_
+
+#include "rive/generated/nested_artboard_base.hpp"
+#include "rive/data_bind/data_context.hpp"
+#include "rive/viewmodel/viewmodel_instance_value.hpp"
+#include "rive/hit_info.hpp"
+#include "rive/span.hpp"
+#include <stdio.h>
+
+namespace rive
+{
+
+class ArtboardInstance;
+class NestedAnimation;
+class NestedInput;
+class NestedStateMachine;
+class StateMachineInstance;
+class NestedArtboard : public NestedArtboardBase
+{
+protected:
+    Artboard* m_Artboard = nullptr;               // might point to m_Instance, and might not
+    std::unique_ptr<ArtboardInstance> m_Instance; // may be null
+    std::vector<NestedAnimation*> m_NestedAnimations;
+
+protected:
+    std::vector<uint32_t> m_DataBindPathIdsBuffer;
+
+public:
+    NestedArtboard();
+    ~NestedArtboard() override;
+    StatusCode onAddedClean(CoreContext* context) override;
+    void draw(Renderer* renderer) override;
+    Core* hitTest(HitInfo*, const Mat2D&) override;
+    void addNestedAnimation(NestedAnimation* nestedAnimation);
+
+    void nest(Artboard* artboard);
+    ArtboardInstance* artboardInstance() { return m_Instance.get(); }
+
+    StatusCode import(ImportStack& importStack) override;
+    Core* clone() const override;
+    bool advance(float elapsedSeconds);
+    void update(ComponentDirt value) override;
+
+    bool hasNestedStateMachines() const;
+    Span<NestedAnimation*> nestedAnimations();
+    NestedArtboard* nestedArtboard(std::string name) const;
+    NestedStateMachine* stateMachine(std::string name) const;
+    NestedInput* input(std::string name) const;
+    NestedInput* input(std::string name, std::string stateMachineName) const;
+
+    Vec2D measureLayout(float width,
+                        LayoutMeasureMode widthMode,
+                        float height,
+                        LayoutMeasureMode heightMode) override;
+    void controlSize(Vec2D size) override;
+
+    /// Convert a world space (relative to the artboard that this
+    /// NestedArtboard is a child of) to the local space of the Artboard
+    /// nested within. Returns true when the conversion succeeds, and false
+    /// when one is not possible.
+    bool worldToLocal(Vec2D world, Vec2D* local);
+    void syncStyleChanges();
+    void decodeDataBindPathIds(Span<const uint8_t> value) override;
+    void copyDataBindPathIds(const NestedArtboardBase& object) override;
+    std::vector<uint32_t> dataBindPathIds() { return m_DataBindPathIdsBuffer; };
+    void dataContextFromInstance(ViewModelInstance* viewModelInstance, DataContext* parent);
+    void internalDataContext(DataContext* dataContext, DataContext* parent);
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/nested_artboard_layout.hpp b/include/rive/nested_artboard_layout.hpp
new file mode 100644
index 0000000..f6876ea
--- /dev/null
+++ b/include/rive/nested_artboard_layout.hpp
@@ -0,0 +1,35 @@
+#ifndef _RIVE_NESTED_ARTBOARD_LAYOUT_HPP_
+#define _RIVE_NESTED_ARTBOARD_LAYOUT_HPP_
+#include "rive/generated/nested_artboard_layout_base.hpp"
+
+namespace rive
+{
+class NestedArtboardLayout : public NestedArtboardLayoutBase
+{
+public:
+#ifdef WITH_RIVE_LAYOUT
+    void* layoutNode();
+#endif
+    Core* clone() const override;
+    void markNestedLayoutDirty();
+    void update(ComponentDirt value) override;
+    StatusCode onAddedClean(CoreContext* context) override;
+
+    float actualInstanceWidth();
+    float actualInstanceHeight();
+
+protected:
+    void instanceWidthChanged() override;
+    void instanceHeightChanged() override;
+    void instanceWidthUnitsValueChanged() override;
+    void instanceHeightUnitsValueChanged() override;
+    void instanceWidthScaleTypeChanged() override;
+    void instanceHeightScaleTypeChanged() override;
+
+private:
+    void updateWidthOverride();
+    void updateHeightOverride();
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/nested_artboard_leaf.hpp b/include/rive/nested_artboard_leaf.hpp
new file mode 100644
index 0000000..a7fa3d5
--- /dev/null
+++ b/include/rive/nested_artboard_leaf.hpp
@@ -0,0 +1,15 @@
+#ifndef _RIVE_NESTED_ARTBOARD_LEAF_HPP_
+#define _RIVE_NESTED_ARTBOARD_LEAF_HPP_
+#include "rive/generated/nested_artboard_leaf_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class NestedArtboardLeaf : public NestedArtboardLeafBase
+{
+public:
+    Core* clone() const override;
+    void update(ComponentDirt value) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/node.hpp b/include/rive/node.hpp
new file mode 100644
index 0000000..90de2fb
--- /dev/null
+++ b/include/rive/node.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_NODE_HPP_
+#define _RIVE_NODE_HPP_
+#include "rive/generated/node_base.hpp"
+
+namespace rive
+{
+/// A Rive Node
+class Node : public NodeBase
+{
+protected:
+    void xChanged() override;
+    void yChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/open_url_event.hpp b/include/rive/open_url_event.hpp
new file mode 100644
index 0000000..fb8bc0e
--- /dev/null
+++ b/include/rive/open_url_event.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_OPEN_URL_EVENT_HPP_
+#define _RIVE_OPEN_URL_EVENT_HPP_
+#include "rive/generated/open_url_event_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class OpenUrlEvent : public OpenUrlEventBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/pointer_event.hpp b/include/rive/pointer_event.hpp
new file mode 100644
index 0000000..da1bb55
--- /dev/null
+++ b/include/rive/pointer_event.hpp
@@ -0,0 +1,27 @@
+#ifndef _RIVE_POINTER_EVENT_HPP_
+#define _RIVE_POINTER_EVENT_HPP_
+
+#include "rive/math/vec2d.hpp"
+
+namespace rive
+{
+
+enum class PointerEventType
+{
+    down, // The button has gone from up to down
+    move, // The pointer's position has changed
+    up,   // The button has gone from down to up
+};
+
+struct PointerEvent
+{
+    PointerEventType m_Type;
+    Vec2D m_Position;
+    int m_PointerIndex;
+
+    // add more fields as needed
+};
+
+} // namespace rive
+
+#endif
diff --git a/include/rive/refcnt.hpp b/include/rive/refcnt.hpp
new file mode 100644
index 0000000..2b0b5e4
--- /dev/null
+++ b/include/rive/refcnt.hpp
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2021 Rive
+ */
+
+#ifndef _RIVE_REFCNT_HPP_
+#define _RIVE_REFCNT_HPP_
+
+#include "rive/rive_types.hpp"
+
+#include <atomic>
+#include <cstddef>
+#include <type_traits>
+#include <utility>
+
+/*
+ *  RefCnt : Threadsafe shared pointer baseclass.
+ *
+ *  The reference count is set to one in the constructor, and goes up on every call to ref(), and
+ *  down on every call to unref(). When a call to unref() brings the counter to 0, the object is
+ *  casted to class "const T*" and deleted. Usage:
+ *
+ *    class MyClass : public RefCnt<MyClass>
+ *
+ *  rcp : template wrapper for subclasses of RefCnt, to manage assignment and parameter passing
+ *  to safely keep track of shared ownership.
+ *
+ *  Both of these inspired by Skia's SkRefCnt and sk_sp
+ */
+
+namespace rive
+{
+
+template <typename T> class RefCnt
+{
+public:
+    RefCnt() : m_refcnt(1) {}
+
+    void ref() const { (void)m_refcnt.fetch_add(+1, std::memory_order_relaxed); }
+
+    void unref() const
+    {
+        if (1 == m_refcnt.fetch_add(-1, std::memory_order_acq_rel))
+        {
+            static_cast<const T*>(this)->onRefCntReachedZero();
+        }
+    }
+
+    // not reliable in actual threaded scenarios, but useful (perhaps) for debugging
+    int32_t debugging_refcnt() const { return m_refcnt.load(std::memory_order_relaxed); }
+
+protected:
+    // Can be overloaded in the subclass if specialized delete behavior is required.
+    void onRefCntReachedZero() const { delete static_cast<const T*>(this); }
+
+private:
+    // mutable, so can be changed even on a const object
+    mutable std::atomic<int32_t> m_refcnt;
+
+    RefCnt(RefCnt&&) = delete;
+    RefCnt(const RefCnt&) = delete;
+    RefCnt& operator=(RefCnt&&) = delete;
+    RefCnt& operator=(const RefCnt&) = delete;
+};
+
+template <typename T> static inline T* safe_ref(T* obj)
+{
+    if (obj)
+    {
+        obj->ref();
+    }
+    return obj;
+}
+
+template <typename T> static inline void safe_unref(T* obj)
+{
+    if (obj)
+    {
+        obj->unref();
+    }
+}
+
+// rcp : smart point template for holding subclasses of RefCnt
+
+template <typename T> class rcp
+{
+public:
+    constexpr rcp() : m_ptr(nullptr) {}
+    constexpr rcp(std::nullptr_t) : m_ptr(nullptr) {}
+    explicit rcp(T* ptr) : m_ptr(ptr) {}
+
+    rcp(const rcp<T>& other) : m_ptr(safe_ref(other.get())) {}
+    rcp(rcp<T>&& other) : m_ptr(other.release()) {}
+
+    template <typename U,
+              typename = typename std::enable_if<std::is_convertible<U*, T*>::value>::type>
+    rcp(const rcp<U>& other) : m_ptr(safe_ref(other.get()))
+    {}
+
+    template <typename U,
+              typename = typename std::enable_if<std::is_convertible<U*, T*>::value>::type>
+    rcp(rcp<U>&& other) : m_ptr(other.release())
+    {}
+
+    /**
+     *  Calls unref() on the underlying object pointer.
+     */
+    ~rcp() { safe_unref(m_ptr); }
+
+    rcp<T>& operator=(std::nullptr_t)
+    {
+        this->reset();
+        return *this;
+    }
+
+    rcp<T>& operator=(const rcp<T>& other)
+    {
+        if (this != &other)
+        {
+            this->reset(safe_ref(other.get()));
+        }
+        return *this;
+    }
+
+    // move assignment operator
+    rcp<T>& operator=(rcp<T>&& other)
+    {
+        this->reset(other.release());
+        return *this;
+    }
+
+    T& operator*() const
+    {
+        assert(this->get() != nullptr);
+        return *this->get();
+    }
+
+    explicit operator bool() const { return this->get() != nullptr; }
+
+    T* get() const { return m_ptr; }
+    T* operator->() const { return m_ptr; }
+
+    // Unrefs the current pointer, and accepts the new pointer, but
+    // DOES NOT increment ownership of the new pointer.
+    void reset(T* ptr = nullptr)
+    {
+        // Calling m_ptr->unref() may call this->~() or this->reset(T*).
+        // http://wg21.cmeerw.net/lwg/issue998
+        // http://wg21.cmeerw.net/lwg/issue2262
+        T* oldPtr = m_ptr;
+        m_ptr = ptr;
+        safe_unref(oldPtr);
+    }
+
+    // This returns the bare point WITHOUT CHANGING ITS REFCNT, but removes it
+    // from this object, so the caller must manually manage its count.
+    T* release()
+    {
+        T* ptr = m_ptr;
+        m_ptr = nullptr;
+        return ptr;
+    }
+
+    void swap(rcp<T>& other) { std::swap(m_ptr, other.m_ptr); }
+
+private:
+    T* m_ptr;
+};
+
+template <typename T> inline void swap(rcp<T>& a, rcp<T>& b) { a.swap(b); }
+
+template <typename T, typename... Args> rcp<T> inline make_rcp(Args&&... args)
+{
+    return rcp<T>(new T(std::forward<Args>(args)...));
+}
+
+template <typename T> rcp<T> inline ref_rcp(T* ptr) { return rcp<T>(safe_ref(ptr)); }
+
+template <typename U,
+          typename T,
+          typename = typename std::enable_if<std::is_convertible<U*, T*>::value>::type>
+rcp<U> static_rcp_cast(rcp<T> ptr)
+{
+    return rcp<U>(static_cast<U*>(ptr.release()));
+}
+
+// == variants
+
+template <typename T> inline bool operator==(const rcp<T>& a, std::nullptr_t) { return !a; }
+template <typename T> inline bool operator==(std::nullptr_t, const rcp<T>& b) { return !b; }
+template <typename T, typename U> inline bool operator==(const rcp<T>& a, const rcp<U>& b)
+{
+    return a.get() == b.get();
+}
+
+// != variants
+
+template <typename T> inline bool operator!=(const rcp<T>& a, std::nullptr_t)
+{
+    return static_cast<bool>(a);
+}
+template <typename T> inline bool operator!=(std::nullptr_t, const rcp<T>& b)
+{
+    return static_cast<bool>(b);
+}
+template <typename T, typename U> inline bool operator!=(const rcp<T>& a, const rcp<U>& b)
+{
+    return a.get() != b.get();
+}
+
+} // namespace rive
+
+#endif
diff --git a/include/rive/relative_local_asset_loader.hpp b/include/rive/relative_local_asset_loader.hpp
new file mode 100644
index 0000000..c4dc317
--- /dev/null
+++ b/include/rive/relative_local_asset_loader.hpp
@@ -0,0 +1,57 @@
+#ifndef _RIVE_RELATIVE_LOCAL_ASSET_RESOLVER_HPP_
+#define _RIVE_RELATIVE_LOCAL_ASSET_RESOLVER_HPP_
+
+#include "rive/file_asset_loader.hpp"
+#include "rive/assets/file_asset.hpp"
+#include <cstdio>
+#include <string>
+#include "rive/span.hpp"
+
+namespace rive
+{
+class FileAsset;
+class Factory;
+
+/// An implementation of FileAssetLoader which finds the assets in a local
+/// path relative to the original .riv file looking for them.
+class RelativeLocalAssetLoader : public FileAssetLoader
+{
+private:
+    std::string m_Path;
+
+public:
+    RelativeLocalAssetLoader(std::string filename)
+    {
+        std::size_t finalSlash = filename.rfind('/');
+
+        if (finalSlash != std::string::npos)
+        {
+            m_Path = filename.substr(0, finalSlash + 1);
+        }
+    }
+
+    bool loadContents(FileAsset& asset,
+                      Span<const uint8_t> inBandBytes,
+                      rive::Factory* factory) override
+    {
+        std::string filename = m_Path + asset.uniqueFilename();
+        FILE* fp = fopen(filename.c_str(), "rb");
+        if (fp == nullptr)
+        {
+            fprintf(stderr, "Failed to find file at %s\n", filename.c_str());
+            return false;
+        }
+
+        fseek(fp, 0, SEEK_END);
+        const size_t length = ftell(fp);
+        fseek(fp, 0, SEEK_SET);
+        SimpleArray<uint8_t> bytes(length);
+        if (fread(bytes.data(), 1, length, fp) == length)
+        {
+            asset.decode(bytes, factory);
+        }
+        return true;
+    }
+};
+} // namespace rive
+#endif
diff --git a/include/rive/renderer.hpp b/include/rive/renderer.hpp
new file mode 100644
index 0000000..6e08ec9
--- /dev/null
+++ b/include/rive/renderer.hpp
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_RENDERER_HPP_
+#define _RIVE_RENDERER_HPP_
+
+#include "rive/enum_bitset.hpp"
+#include "rive/shapes/paint/color.hpp"
+#include "rive/command_path.hpp"
+#include "rive/layout.hpp"
+#include "rive/refcnt.hpp"
+#include "rive/math/aabb.hpp"
+#include "rive/math/mat2d.hpp"
+#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 <stdio.h>
+#include <cstdint>
+
+namespace rive
+{
+class Vec2D;
+
+// Helper that computes a matrix to "align" content (source) to fit inside frame (destination).
+Mat2D computeAlignment(Fit, Alignment, const AABB& frame, const AABB& content);
+
+enum class RenderBufferType
+{
+    index,
+    vertex,
+};
+
+enum class RenderBufferFlags
+{
+    none = 0,
+    mappedOnceAtInitialization = 1 << 0, // The client will map the buffer exactly one time, before
+                                         // rendering, and will never update it again.
+};
+RIVE_MAKE_ENUM_BITSET(RenderBufferFlags)
+
+class RenderBuffer : public RefCnt<RenderBuffer>, public enable_lite_rtti<RenderBuffer>
+{
+public:
+    RenderBuffer(RenderBufferType, RenderBufferFlags, size_t sizeInBytes);
+    virtual ~RenderBuffer();
+
+    RenderBufferType type() const { return m_type; }
+    RenderBufferFlags flags() const { return m_flags; }
+    size_t sizeInBytes() const { return m_sizeInBytes; }
+
+    void* map();
+    void unmap();
+
+protected:
+    virtual void* onMap() = 0;
+    virtual void onUnmap() = 0;
+
+private:
+    const RenderBufferType m_type;
+    const RenderBufferFlags m_flags;
+    const size_t m_sizeInBytes;
+    RIVE_DEBUG_CODE(size_t m_mapCount = 0;)
+    RIVE_DEBUG_CODE(size_t m_unmapCount = 0;)
+};
+
+enum class RenderPaintStyle
+{
+    stroke,
+    fill
+};
+
+/*
+ *  Base class for Render objects that specify the src colors.
+ *
+ *  Shaders are immutable, and sharable between multiple paints, etc.
+ *
+ *  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>, public enable_lite_rtti<RenderShader>
+{
+public:
+    RenderShader();
+    virtual ~RenderShader();
+};
+
+class RenderPaint : public RefCnt<RenderPaint>, public enable_lite_rtti<RenderPaint>
+{
+public:
+    RenderPaint();
+    virtual ~RenderPaint();
+
+    virtual void style(RenderPaintStyle style) = 0;
+    virtual void color(ColorInt value) = 0;
+    virtual void thickness(float value) = 0;
+    virtual void join(StrokeJoin value) = 0;
+    virtual void cap(StrokeCap value) = 0;
+    virtual void blendMode(BlendMode value) = 0;
+    virtual void shader(rcp<RenderShader>) = 0;
+    virtual void invalidateStroke() = 0;
+};
+
+class RenderImage : public RefCnt<RenderImage>, public enable_lite_rtti<RenderImage>
+{
+protected:
+    int m_Width = 0;
+    int m_Height = 0;
+    Mat2D m_uvTransform;
+
+public:
+    RenderImage();
+    RenderImage(const Mat2D& uvTransform);
+    virtual ~RenderImage();
+
+    int width() const { return m_Width; }
+    int height() const { return m_Height; }
+    const Mat2D& uvTransform() const { return m_uvTransform; }
+};
+
+class RenderPath : public CommandPath, public enable_lite_rtti<RenderPath>
+{
+public:
+    RenderPath();
+    ~RenderPath() override;
+
+    RenderPath* renderPath() override { return this; }
+    void addPath(CommandPath* path, const Mat2D& transform) override
+    {
+        addRenderPath(path->renderPath(), transform);
+    }
+
+    virtual void addRenderPath(RenderPath* path, const Mat2D& transform) = 0;
+};
+
+class Renderer
+{
+public:
+    virtual ~Renderer() {}
+    virtual void save() = 0;
+    virtual void restore() = 0;
+    virtual void transform(const Mat2D& transform) = 0;
+    virtual void drawPath(RenderPath* path, RenderPaint* paint) = 0;
+    virtual void clipPath(RenderPath* path) = 0;
+    virtual void drawImage(const RenderImage*, BlendMode, float opacity) = 0;
+    virtual void drawImageMesh(const RenderImage*,
+                               rcp<RenderBuffer> vertices_f32,
+                               rcp<RenderBuffer> uvCoords_f32,
+                               rcp<RenderBuffer> indices_u16,
+                               uint32_t vertexCount,
+                               uint32_t indexCount,
+                               BlendMode,
+                               float opacity) = 0;
+
+    // helpers
+
+    void translate(float x, float y);
+    void scale(float sx, float sy);
+    void rotate(float radians);
+
+    void align(Fit fit, Alignment alignment, const AABB& frame, const AABB& content)
+    {
+        transform(computeAlignment(fit, alignment, frame, content));
+    }
+};
+} // namespace rive
+#endif
diff --git a/include/rive/renderer_utils.hpp b/include/rive/renderer_utils.hpp
new file mode 100644
index 0000000..5850689
--- /dev/null
+++ b/include/rive/renderer_utils.hpp
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_RENDERER_UTILS_HPP_
+#define _RIVE_RENDERER_UTILS_HPP_
+
+#include "rive/rive_types.hpp"
+#include "rive/core/type_conversions.hpp"
+#include <string>
+
+template <size_t N, typename T> class AutoSTArray
+{
+    T m_storage[N];
+    T* m_ptr;
+    const size_t m_count;
+
+public:
+    AutoSTArray(size_t n) : m_count(n)
+    {
+        m_ptr = m_storage;
+        if (n > N)
+        {
+            m_ptr = new T[n];
+        }
+    }
+    ~AutoSTArray()
+    {
+        if (m_ptr != m_storage)
+        {
+            delete[] m_ptr;
+        }
+    }
+
+    size_t size() const { return m_count; }
+    int count() const { return rive::castTo<int>(m_count); }
+
+    T* data() const { return m_ptr; }
+
+    T& operator[](size_t index)
+    {
+        assert(index < m_count);
+        return m_ptr[index];
+    }
+};
+
+constexpr inline uint32_t make_tag(uint8_t a, uint8_t b, uint8_t c, uint8_t d)
+{
+    return (a << 24) | (b << 16) | (c << 8) | d;
+}
+
+static inline std::string tag2str(uint32_t tag)
+{
+    std::string str = "abcd";
+    str[0] = (tag >> 24) & 0xFF;
+    str[1] = (tag >> 16) & 0xFF;
+    str[2] = (tag >> 8) & 0xFF;
+    str[3] = (tag >> 0) & 0xFF;
+    return str;
+}
+
+#endif
diff --git a/include/rive/rive_types.hpp b/include/rive/rive_types.hpp
new file mode 100644
index 0000000..f7d884b
--- /dev/null
+++ b/include/rive/rive_types.hpp
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+// This should always be included by any other rive files,
+// as it performs basic self-consistency checks, and provides
+// shared common types and macros.
+
+#ifndef _RIVE_TYPES_HPP_
+#define _RIVE_TYPES_HPP_
+
+#include <memory>   // For unique_ptr.
+#include <string.h> // For memcpy.
+
+#if defined(DEBUG) && defined(NDEBUG)
+#error "can't determine if we're debug or release"
+#endif
+
+#if !defined(DEBUG) && !defined(NDEBUG)
+// we have to make a decision what mode we're in
+// historically this has been to look for NDEBUG, and in its
+// absence assume we're DEBUG.
+#define DEBUG 1
+// fyi - Xcode seems to set DEBUG (or not), so the above guess
+// doesn't work for them - so our projects may need to explicitly
+// set NDEBUG in our 'release' builds.
+#endif
+
+#ifdef NDEBUG
+#ifndef RELEASE
+#define RELEASE 1
+#endif
+#else // debug mode
+#ifndef DEBUG
+#define DEBUG 1
+#endif
+#endif
+
+// Some checks to guess what platform we're building for
+
+#ifdef __APPLE__
+
+#define RIVE_BUILD_FOR_APPLE
+#include <TargetConditionals.h>
+
+#if TARGET_OS_IPHONE
+#define RIVE_BUILD_FOR_IOS
+#elif TARGET_OS_MAC
+#define RIVE_BUILD_FOR_OSX
+#endif
+
+#endif
+
+// We really like these headers, so we include them all the time.
+
+#include <algorithm>
+#include <cassert>
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <type_traits>
+
+// Annotations to assert unreachable control flow.
+#if defined(__GNUC__) || defined(__clang__)
+#define RIVE_UNREACHABLE                                                                           \
+    assert(!(bool)"unreachable reached");                                                          \
+    __builtin_unreachable
+#elif _MSC_VER
+#define RIVE_UNREACHABLE()                                                                         \
+    assert(!(bool)"unreachable reached");                                                          \
+    __assume(0)
+#else
+#define RIVE_UNREACHABLE()                                                                         \
+    do                                                                                             \
+    {                                                                                              \
+        assert(!(bool)"unreachable reached");                                                      \
+    } while (0)
+#endif
+
+#if __cplusplus >= 201703L
+#define RIVE_MAYBE_UNUSED [[maybe_unused]]
+#else
+#define RIVE_MAYBE_UNUSED
+#endif
+
+#if __cplusplus >= 201703L
+#define RIVE_FALLTHROUGH [[fallthrough]]
+#elif defined(__clang__)
+#define RIVE_FALLTHROUGH [[clang::fallthrough]]
+#else
+#define RIVE_FALLTHROUGH
+#endif
+
+#if defined(__GNUC__) || defined(__clang__)
+#define RIVE_ALWAYS_INLINE inline __attribute__((always_inline))
+#else
+#define RIVE_ALWAYS_INLINE inline
+#endif
+
+#if defined(__GNUC__) || defined(__clang__)
+// Recommended in https://clang.llvm.org/docs/LanguageExtensions.html#feature-checking-macros
+#ifndef __has_builtin
+#define __has_builtin(x) 0
+#endif
+#else
+#define __has_builtin(x) 0
+#endif
+
+#if __has_builtin(__builtin_memcpy)
+#define RIVE_INLINE_MEMCPY __builtin_memcpy
+#else
+#define RIVE_INLINE_MEMCPY memcpy
+#endif
+
+#ifdef DEBUG
+#define RIVE_DEBUG_CODE(CODE) CODE
+#else
+#define RIVE_DEBUG_CODE(CODE)
+#endif
+
+// Backports of later stl functions.
+namespace rivestd
+{
+template <class T, class... Args> std::unique_ptr<T> make_unique(Args&&... args)
+{
+    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
+}
+} // namespace rivestd
+
+#endif // rive_types
diff --git a/include/rive/runtime_header.hpp b/include/rive/runtime_header.hpp
new file mode 100644
index 0000000..4d76009
--- /dev/null
+++ b/include/rive/runtime_header.hpp
@@ -0,0 +1,109 @@
+#ifndef _RIVE_RUNTIME_HEADER_HPP_
+#define _RIVE_RUNTIME_HEADER_HPP_
+
+#include "rive/core/binary_reader.hpp"
+#include <unordered_map>
+
+namespace rive
+{
+
+/// Rive file runtime header. The header is fonud at the beginning of every
+/// Rive runtime file, and begins with a specific 4-byte format: "RIVE".
+/// This is followed by the major and minor version of Rive used to create
+/// the file. Finally the owner and file ids are at the end of header; these
+/// unsigned integers may be zero.
+static constexpr char kRuntimeHeaderFingerprint[] = "RIVE";
+class RuntimeHeader
+{
+private:
+    int m_MajorVersion;
+    int m_MinorVersion;
+    int m_FileId;
+    std::unordered_map<int, int> m_PropertyToFieldIndex;
+
+public:
+    /// @returns the file's major version
+    int majorVersion() const { return m_MajorVersion; }
+    /// @returns the file's minor version
+    int minorVersion() const { return m_MinorVersion; }
+    /// @returns the file's id; may be zero
+    int fileId() const { return m_FileId; }
+
+    int propertyFieldId(int propertyKey) const
+    {
+        auto itr = m_PropertyToFieldIndex.find(propertyKey);
+        if (itr == m_PropertyToFieldIndex.end())
+        {
+            return -1;
+        }
+
+        return itr->second;
+    }
+
+    /// Reads the header from a binary buffer/
+    /// @param reader the binary reader attached to the buffer
+    /// @param header a pointer to the header where the data will be stored.
+    /// @returns true if the header is successfully read
+    static bool read(BinaryReader& reader, RuntimeHeader& header)
+    {
+        for (int i = 0; i < 4; i++)
+        {
+            auto b = reader.readByte();
+            if (kRuntimeHeaderFingerprint[i] != b)
+            {
+                return false;
+            }
+        }
+
+        header.m_MajorVersion = reader.readVarUintAs<int>();
+        if (reader.didOverflow())
+        {
+            return false;
+        }
+        header.m_MinorVersion = reader.readVarUintAs<int>();
+        if (reader.didOverflow())
+        {
+            return false;
+        }
+
+        header.m_FileId = reader.readVarUintAs<int>();
+
+        if (reader.didOverflow())
+        {
+            return false;
+        }
+
+        std::vector<int> propertyKeys;
+        for (int propertyKey = reader.readVarUintAs<int>(); propertyKey != 0;
+             propertyKey = reader.readVarUintAs<int>())
+        {
+            propertyKeys.push_back(propertyKey);
+            if (reader.didOverflow())
+            {
+                return false;
+            }
+        }
+
+        int currentInt = 0;
+        int currentBit = 8;
+        for (auto propertyKey : propertyKeys)
+        {
+            if (currentBit == 8)
+            {
+                currentInt = reader.readUint32();
+                currentBit = 0;
+            }
+            int fieldIndex = (currentInt >> currentBit) & 3;
+            header.m_PropertyToFieldIndex[propertyKey] = fieldIndex;
+            currentBit += 2;
+            if (reader.didOverflow())
+            {
+                return false;
+            }
+        }
+
+        return true;
+    }
+};
+} // namespace rive
+#endif
diff --git a/include/rive/scene.hpp b/include/rive/scene.hpp
new file mode 100644
index 0000000..7d36204
--- /dev/null
+++ b/include/rive/scene.hpp
@@ -0,0 +1,76 @@
+#ifndef _RIVE_SCENE_HPP_
+#define _RIVE_SCENE_HPP_
+
+#include "rive/animation/loop.hpp"
+#include "rive/math/aabb.hpp"
+#include "rive/math/vec2d.hpp"
+#include "rive/animation/keyed_callback_reporter.hpp"
+#include "rive/core/field_types/core_callback_type.hpp"
+#include "rive/hit_result.hpp"
+#include <string>
+
+namespace rive
+{
+class ArtboardInstance;
+class Renderer;
+class ViewModelInstance;
+
+class SMIInput;
+class SMIBool;
+class SMINumber;
+class SMITrigger;
+
+class Scene : public KeyedCallbackReporter, public CallbackContext
+{
+protected:
+    Scene(ArtboardInstance*);
+
+public:
+    ~Scene() override {}
+
+    Scene(Scene const& lhs) : m_artboardInstance(lhs.m_artboardInstance) {}
+
+    float width() const;
+    float height() const;
+    AABB bounds() const { return {0, 0, this->width(), this->height()}; }
+
+    virtual std::string name() const = 0;
+
+    // Returns onShot if this has no looping (e.g. a statemachine)
+    virtual Loop loop() const = 0;
+    // Returns true iff the Scene is known to not be fully opaque
+    virtual bool isTranslucent() const = 0;
+    // returns -1 for continuous
+    virtual float durationSeconds() const = 0;
+
+    // returns true if draw() should be called
+    virtual bool advanceAndApply(float elapsedSeconds) = 0;
+
+    void draw(Renderer*);
+
+    virtual void dataContextFromInstance(ViewModelInstance* viewModelInstance);
+
+    virtual HitResult pointerDown(Vec2D);
+    virtual HitResult pointerMove(Vec2D);
+    virtual HitResult pointerUp(Vec2D);
+    virtual HitResult pointerExit(Vec2D);
+
+    virtual size_t inputCount() const;
+    virtual SMIInput* input(size_t index) const;
+    virtual SMIBool* getBool(const std::string&) const;
+    virtual SMINumber* getNumber(const std::string&) const;
+    virtual SMITrigger* getTrigger(const std::string&) const;
+
+    /// Report which time based events have elapsed on a timeline within this
+    /// state machine.
+    void reportKeyedCallback(uint32_t objectId,
+                             uint32_t propertyKey,
+                             float elapsedSeconds) override;
+
+protected:
+    ArtboardInstance* m_artboardInstance;
+};
+
+} // namespace rive
+
+#endif
diff --git a/include/rive/shapes/clipping_shape.hpp b/include/rive/shapes/clipping_shape.hpp
new file mode 100644
index 0000000..dd020d6
--- /dev/null
+++ b/include/rive/shapes/clipping_shape.hpp
@@ -0,0 +1,36 @@
+#ifndef _RIVE_CLIPPING_SHAPE_HPP_
+#define _RIVE_CLIPPING_SHAPE_HPP_
+#include "rive/renderer.hpp"
+#include "rive/generated/shapes/clipping_shape_base.hpp"
+#include <vector>
+
+namespace rive
+{
+class Shape;
+class Node;
+class RenderPath;
+class ClippingShape : public ClippingShapeBase
+{
+private:
+    std::vector<Shape*> m_Shapes;
+    Node* m_Source = nullptr;
+    rcp<RenderPath> m_RenderPath;
+
+    // The actual render path used for clipping, which may be different from
+    // the stored render path. For example if there's only one clipping
+    // shape, we don't build a whole new path for it.
+    RenderPath* m_ClipRenderPath = nullptr;
+
+public:
+    Node* source() const { return m_Source; }
+    const std::vector<Shape*>& shapes() const { return m_Shapes; }
+    StatusCode onAddedClean(CoreContext* context) override;
+    StatusCode onAddedDirty(CoreContext* context) override;
+    void buildDependencies() override;
+    void update(ComponentDirt value) override;
+
+    RenderPath* renderPath() const { return m_ClipRenderPath; }
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/shapes/contour_mesh_vertex.hpp b/include/rive/shapes/contour_mesh_vertex.hpp
new file mode 100644
index 0000000..850b4ac
--- /dev/null
+++ b/include/rive/shapes/contour_mesh_vertex.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_CONTOUR_MESH_VERTEX_HPP_
+#define _RIVE_CONTOUR_MESH_VERTEX_HPP_
+#include "rive/generated/shapes/contour_mesh_vertex_base.hpp"
+
+namespace rive
+{
+class ContourMeshVertex : public ContourMeshVertexBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/shapes/cubic_asymmetric_vertex.hpp b/include/rive/shapes/cubic_asymmetric_vertex.hpp
new file mode 100644
index 0000000..98701f0
--- /dev/null
+++ b/include/rive/shapes/cubic_asymmetric_vertex.hpp
@@ -0,0 +1,17 @@
+#ifndef _RIVE_CUBIC_ASYMMETRIC_VERTEX_HPP_
+#define _RIVE_CUBIC_ASYMMETRIC_VERTEX_HPP_
+#include "rive/generated/shapes/cubic_asymmetric_vertex_base.hpp"
+namespace rive
+{
+class CubicAsymmetricVertex : public CubicAsymmetricVertexBase
+{
+protected:
+    void computeIn() override;
+    void computeOut() override;
+    void rotationChanged() override;
+    void inDistanceChanged() override;
+    void outDistanceChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/shapes/cubic_detached_vertex.hpp b/include/rive/shapes/cubic_detached_vertex.hpp
new file mode 100644
index 0000000..14e679d
--- /dev/null
+++ b/include/rive/shapes/cubic_detached_vertex.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_CUBIC_DETACHED_VERTEX_HPP_
+#define _RIVE_CUBIC_DETACHED_VERTEX_HPP_
+#include "rive/generated/shapes/cubic_detached_vertex_base.hpp"
+namespace rive
+{
+class CubicDetachedVertex : public CubicDetachedVertexBase
+{
+protected:
+    void computeIn() override;
+    void computeOut() override;
+
+    void inRotationChanged() override;
+    void inDistanceChanged() override;
+    void outRotationChanged() override;
+    void outDistanceChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/shapes/cubic_mirrored_vertex.hpp b/include/rive/shapes/cubic_mirrored_vertex.hpp
new file mode 100644
index 0000000..c32df04
--- /dev/null
+++ b/include/rive/shapes/cubic_mirrored_vertex.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_CUBIC_MIRRORED_VERTEX_HPP_
+#define _RIVE_CUBIC_MIRRORED_VERTEX_HPP_
+#include "rive/generated/shapes/cubic_mirrored_vertex_base.hpp"
+namespace rive
+{
+class CubicMirroredVertex : public CubicMirroredVertexBase
+{
+protected:
+    void computeIn() override;
+    void computeOut() override;
+    void rotationChanged() override;
+    void distanceChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/shapes/cubic_vertex.hpp b/include/rive/shapes/cubic_vertex.hpp
new file mode 100644
index 0000000..31ae2d9
--- /dev/null
+++ b/include/rive/shapes/cubic_vertex.hpp
@@ -0,0 +1,35 @@
+#ifndef _RIVE_CUBIC_VERTEX_HPP_
+#define _RIVE_CUBIC_VERTEX_HPP_
+#include "rive/generated/shapes/cubic_vertex_base.hpp"
+#include "rive/math/vec2d.hpp"
+
+namespace rive
+{
+class Vec2D;
+class CubicVertex : public CubicVertexBase
+{
+protected:
+    bool m_InValid = false;
+    bool m_OutValid = false;
+    Vec2D m_InPoint;
+    Vec2D m_OutPoint;
+
+    virtual void computeIn() = 0;
+    virtual void computeOut() = 0;
+
+public:
+    const Vec2D& outPoint();
+    const Vec2D& inPoint();
+    const Vec2D& renderOut();
+    const Vec2D& renderIn();
+
+    void outPoint(const Vec2D& value);
+    void inPoint(const Vec2D& value);
+    void xChanged() override;
+    void yChanged() override;
+
+    void deform(const Mat2D& worldTransform, const float* boneTransforms) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/shapes/ellipse.hpp b/include/rive/shapes/ellipse.hpp
new file mode 100644
index 0000000..750ade0
--- /dev/null
+++ b/include/rive/shapes/ellipse.hpp
@@ -0,0 +1,18 @@
+#ifndef _RIVE_ELLIPSE_HPP_
+#define _RIVE_ELLIPSE_HPP_
+#include "rive/generated/shapes/ellipse_base.hpp"
+#include "rive/shapes/cubic_detached_vertex.hpp"
+
+namespace rive
+{
+class Ellipse : public EllipseBase
+{
+    CubicDetachedVertex m_Vertex1, m_Vertex2, m_Vertex3, m_Vertex4;
+
+public:
+    Ellipse();
+    void update(ComponentDirt value) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/shapes/image.hpp b/include/rive/shapes/image.hpp
new file mode 100644
index 0000000..7d9510b
--- /dev/null
+++ b/include/rive/shapes/image.hpp
@@ -0,0 +1,37 @@
+#ifndef _RIVE_IMAGE_HPP_
+#define _RIVE_IMAGE_HPP_
+
+#include "rive/hit_info.hpp"
+#include "rive/generated/shapes/image_base.hpp"
+#include "rive/assets/file_asset_referencer.hpp"
+
+namespace rive
+{
+class ImageAsset;
+class Mesh;
+class Image : public ImageBase, public FileAssetReferencer
+{
+private:
+    Mesh* m_Mesh = nullptr;
+
+public:
+    Mesh* mesh() const;
+    void setMesh(Mesh* mesh);
+    ImageAsset* imageAsset() const { return (ImageAsset*)m_fileAsset; }
+    void draw(Renderer* renderer) override;
+    Core* hitTest(HitInfo*, const Mat2D&) override;
+    StatusCode import(ImportStack& importStack) override;
+    void setAsset(FileAsset*) override;
+    uint32_t assetId() override;
+    Core* clone() const override;
+    Vec2D measureLayout(float width,
+                        LayoutMeasureMode widthMode,
+                        float height,
+                        LayoutMeasureMode heightMode) override;
+    void controlSize(Vec2D size) override;
+    float width() const;
+    float height() const;
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/shapes/mesh.hpp b/include/rive/shapes/mesh.hpp
new file mode 100644
index 0000000..80e283d
--- /dev/null
+++ b/include/rive/shapes/mesh.hpp
@@ -0,0 +1,53 @@
+#ifndef _RIVE_MESH_HPP_
+#define _RIVE_MESH_HPP_
+#include "rive/generated/shapes/mesh_base.hpp"
+#include "rive/bones/skinnable.hpp"
+#include "rive/span.hpp"
+#include "rive/refcnt.hpp"
+#include "rive/renderer.hpp"
+
+namespace rive
+{
+class MeshVertex;
+
+class Mesh : public MeshBase, public Skinnable
+{
+
+protected:
+    class IndexBuffer : public std::vector<uint16_t>, public RefCnt<IndexBuffer>
+    {};
+    std::vector<MeshVertex*> m_Vertices;
+    rcp<IndexBuffer> m_IndexBuffer;
+    bool m_VertexRenderBufferDirty = true;
+
+    rcp<RenderBuffer> m_IndexRenderBuffer;
+    rcp<RenderBuffer> m_VertexRenderBuffer;
+    rcp<RenderBuffer> m_UVRenderBuffer;
+
+public:
+    StatusCode onAddedDirty(CoreContext* context) override;
+    StatusCode onAddedClean(CoreContext* context) override;
+    void markDrawableDirty();
+    void addVertex(MeshVertex* vertex);
+    void decodeTriangleIndexBytes(Span<const uint8_t> value) override;
+    void copyTriangleIndexBytes(const MeshBase& object) override;
+    void buildDependencies() override;
+    void update(ComponentDirt value) override;
+    void draw(Renderer* renderer, const RenderImage* image, BlendMode blendMode, float opacity);
+
+    void updateVertexRenderBuffer(Renderer* renderer);
+    void markSkinDirty() override;
+    Core* clone() const override;
+
+    /// Initialize the any buffers that will be shared amongst instances (the
+    /// instance are guaranteed to use the same RenderImage).
+    void initializeSharedBuffers(RenderImage* renderImage);
+
+#ifdef TESTING
+    std::vector<MeshVertex*>& vertices() { return m_Vertices; }
+    rcp<IndexBuffer> indices() { return m_IndexBuffer; }
+#endif
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/shapes/mesh_vertex.hpp b/include/rive/shapes/mesh_vertex.hpp
new file mode 100644
index 0000000..2b8c04f
--- /dev/null
+++ b/include/rive/shapes/mesh_vertex.hpp
@@ -0,0 +1,15 @@
+#ifndef _RIVE_MESH_VERTEX_HPP_
+#define _RIVE_MESH_VERTEX_HPP_
+#include "rive/generated/shapes/mesh_vertex_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class MeshVertex : public MeshVertexBase
+{
+public:
+    void markGeometryDirty() override;
+    StatusCode onAddedDirty(CoreContext* context) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/shapes/paint/blend_mode.hpp b/include/rive/shapes/paint/blend_mode.hpp
new file mode 100644
index 0000000..3b27246
--- /dev/null
+++ b/include/rive/shapes/paint/blend_mode.hpp
@@ -0,0 +1,25 @@
+#ifndef _RIVE_BLEND_MODE_HPP_
+#define _RIVE_BLEND_MODE_HPP_
+namespace rive
+{
+enum class BlendMode : unsigned char
+{
+    srcOver = 3,
+    screen = 14,
+    overlay = 15,
+    darken = 16,
+    lighten = 17,
+    colorDodge = 18,
+    colorBurn = 19,
+    hardLight = 20,
+    softLight = 21,
+    difference = 22,
+    exclusion = 23,
+    multiply = 24,
+    hue = 25,
+    saturation = 26,
+    color = 27,
+    luminosity = 28
+};
+}
+#endif
diff --git a/include/rive/shapes/paint/color.hpp b/include/rive/shapes/paint/color.hpp
new file mode 100644
index 0000000..cf01a85
--- /dev/null
+++ b/include/rive/shapes/paint/color.hpp
@@ -0,0 +1,36 @@
+#ifndef _RIVE_PAINT_COLOR_HPP_
+#define _RIVE_PAINT_COLOR_HPP_
+#include <cmath>
+#include <cstdint>
+
+namespace rive
+{
+using ColorInt = uint32_t;
+
+ColorInt colorARGB(int a, int r, int g, int b);
+
+unsigned int colorRed(ColorInt value);
+
+unsigned int colorGreen(ColorInt value);
+
+unsigned int colorBlue(ColorInt value);
+
+unsigned int colorAlpha(ColorInt value);
+
+void UnpackColorToRGBA8(ColorInt color, uint8_t out[4]);
+
+void UnpackColorToRGBA32F(ColorInt color, float out[4]);
+
+void UnpackColorToRGBA32FPremul(ColorInt color, float out[4]);
+
+float colorOpacity(unsigned int value);
+
+ColorInt colorWithAlpha(ColorInt value, unsigned int a);
+
+ColorInt colorWithOpacity(ColorInt value, float opacity);
+
+ColorInt colorModulateOpacity(ColorInt value, float opacity);
+
+ColorInt colorLerp(ColorInt from, ColorInt to, float mix);
+} // namespace rive
+#endif
diff --git a/include/rive/shapes/paint/fill.hpp b/include/rive/shapes/paint/fill.hpp
new file mode 100644
index 0000000..a9a0a64
--- /dev/null
+++ b/include/rive/shapes/paint/fill.hpp
@@ -0,0 +1,20 @@
+#ifndef _RIVE_FILL_HPP_
+#define _RIVE_FILL_HPP_
+#include "rive/generated/shapes/paint/fill_base.hpp"
+#include "rive/shapes/path_flags.hpp"
+namespace rive
+{
+class Fill : public FillBase
+{
+public:
+    RenderPaint* initRenderPaint(ShapePaintMutator* mutator) override;
+    PathFlags pathFlags() const override;
+    void draw(Renderer* renderer,
+              CommandPath* path,
+              const RawPath* rawPath,
+              RenderPaint* paint) override;
+    void applyTo(RenderPaint* renderPaint, float opacityModifier) const override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/shapes/paint/gradient_stop.hpp b/include/rive/shapes/paint/gradient_stop.hpp
new file mode 100644
index 0000000..f0e7db8
--- /dev/null
+++ b/include/rive/shapes/paint/gradient_stop.hpp
@@ -0,0 +1,17 @@
+#ifndef _RIVE_GRADIENT_STOP_HPP_
+#define _RIVE_GRADIENT_STOP_HPP_
+#include "rive/generated/shapes/paint/gradient_stop_base.hpp"
+namespace rive
+{
+class GradientStop : public GradientStopBase
+{
+public:
+    StatusCode onAddedDirty(CoreContext* context) override;
+
+protected:
+    void colorValueChanged() override;
+    void positionChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/shapes/paint/linear_gradient.hpp b/include/rive/shapes/paint/linear_gradient.hpp
new file mode 100644
index 0000000..0b4950b
--- /dev/null
+++ b/include/rive/shapes/paint/linear_gradient.hpp
@@ -0,0 +1,47 @@
+#ifndef _RIVE_LINEAR_GRADIENT_HPP_
+#define _RIVE_LINEAR_GRADIENT_HPP_
+#include "rive/generated/shapes/paint/linear_gradient_base.hpp"
+#include "rive/math/vec2d.hpp"
+#include "rive/shapes/paint/color.hpp"
+#include "rive/shapes/paint/shape_paint_mutator.hpp"
+#include <vector>
+
+namespace rive
+{
+class Node;
+class GradientStop;
+
+class LinearGradient : public LinearGradientBase, public ShapePaintMutator
+{
+private:
+    std::vector<GradientStop*> m_Stops;
+    Node* m_ShapePaintContainer = nullptr;
+
+public:
+    StatusCode onAddedDirty(CoreContext* context) override;
+    void addStop(GradientStop* stop);
+    void update(ComponentDirt value) override;
+    void markGradientDirty();
+    void markStopsDirty();
+    void applyTo(RenderPaint* renderPaint, float opacityModifier) const override;
+
+protected:
+    void buildDependencies() override;
+    void startXChanged() override;
+    void startYChanged() override;
+    void endXChanged() override;
+    void endYChanged() override;
+    void opacityChanged() override;
+    void renderOpacityChanged() override;
+    bool internalIsTranslucent() const override;
+
+    virtual void makeGradient(RenderPaint* renderPaint,
+                              Vec2D start,
+                              Vec2D end,
+                              const ColorInt[],
+                              const float[],
+                              size_t count) const;
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/shapes/paint/radial_gradient.hpp b/include/rive/shapes/paint/radial_gradient.hpp
new file mode 100644
index 0000000..ceb2f8f
--- /dev/null
+++ b/include/rive/shapes/paint/radial_gradient.hpp
@@ -0,0 +1,18 @@
+#ifndef _RIVE_RADIAL_GRADIENT_HPP_
+#define _RIVE_RADIAL_GRADIENT_HPP_
+#include "rive/generated/shapes/paint/radial_gradient_base.hpp"
+namespace rive
+{
+class RadialGradient : public RadialGradientBase
+{
+public:
+    void makeGradient(RenderPaint* renderPaint,
+                      Vec2D start,
+                      Vec2D end,
+                      const ColorInt[],
+                      const float[],
+                      size_t count) const override;
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/shapes/paint/shape_paint.hpp b/include/rive/shapes/paint/shape_paint.hpp
new file mode 100644
index 0000000..67e9660
--- /dev/null
+++ b/include/rive/shapes/paint/shape_paint.hpp
@@ -0,0 +1,63 @@
+#ifndef _RIVE_SHAPE_PAINT_HPP_
+#define _RIVE_SHAPE_PAINT_HPP_
+#include "rive/generated/shapes/paint/shape_paint_base.hpp"
+#include "rive/renderer.hpp"
+#include "rive/shapes/paint/blend_mode.hpp"
+#include "rive/shapes/paint/shape_paint_mutator.hpp"
+#include "rive/shapes/path_flags.hpp"
+#include "rive/math/raw_path.hpp"
+
+namespace rive
+{
+class RenderPaint;
+class ShapePaintMutator;
+class ShapePaint : public ShapePaintBase
+{
+protected:
+    rcp<RenderPaint> m_RenderPaint;
+    ShapePaintMutator* m_PaintMutator = nullptr;
+
+public:
+    StatusCode onAddedClean(CoreContext* context) override;
+
+    float renderOpacity() const { return m_PaintMutator->renderOpacity(); }
+    void renderOpacity(float value) { m_PaintMutator->renderOpacity(value); }
+
+    void blendMode(BlendMode value);
+
+    /// Creates a RenderPaint object for the provided ShapePaintMutator*.
+    /// This should be called only once as the ShapePaint manages the
+    /// lifecycle of the RenderPaint.
+    virtual RenderPaint* initRenderPaint(ShapePaintMutator* mutator);
+
+    virtual PathFlags pathFlags() const = 0;
+    bool isFlagged(PathFlags flags) const { return (int)(pathFlags() & flags) != 0x00; }
+
+    void draw(Renderer* renderer, CommandPath* path, const RawPath* rawPath = nullptr)
+    {
+        draw(renderer, path, rawPath, renderPaint());
+    }
+
+    virtual void draw(Renderer* renderer,
+                      CommandPath* path,
+                      // When every CommandPath stores a RawPath we can get rid
+                      // of this argument.
+                      const RawPath* rawPath,
+                      RenderPaint* paint) = 0;
+
+    RenderPaint* renderPaint() { return m_RenderPaint.get(); }
+
+    /// Get the component that represents the ShapePaintMutator for this
+    /// ShapePaint. It'll be one of SolidColor, LinearGradient, or
+    /// RadialGradient.
+    Component* paint() const { return m_PaintMutator->component(); }
+
+    bool isTranslucent() const { return !this->isVisible() || m_PaintMutator->isTranslucent(); }
+
+    /// Apply this ShapePaint to an external RenderPaint and optionally modulate
+    /// the opacity by opacityModifer.
+    virtual void applyTo(RenderPaint* renderPaint, float opacityModifier) const = 0;
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/shapes/paint/shape_paint_mutator.hpp b/include/rive/shapes/paint/shape_paint_mutator.hpp
new file mode 100644
index 0000000..3b59217
--- /dev/null
+++ b/include/rive/shapes/paint/shape_paint_mutator.hpp
@@ -0,0 +1,39 @@
+#ifndef _RIVE_SHAPE_PAINT_MUTATOR_HPP_
+#define _RIVE_SHAPE_PAINT_MUTATOR_HPP_
+
+namespace rive
+{
+class Component;
+class RenderPaint;
+class ShapePaintMutator
+{
+private:
+    float m_RenderOpacity = 1.0f;
+    RenderPaint* m_RenderPaint = nullptr;
+    /// The Component providing this ShapePaintMutator interface.
+    Component* m_Component = nullptr;
+
+protected:
+    /// Hook up this paint mutator as the mutator for the shape paint
+    /// expected to be the parent.
+    bool initPaintMutator(Component* component);
+    virtual void renderOpacityChanged() = 0;
+
+    RenderPaint* renderPaint() const { return m_RenderPaint; }
+
+    virtual bool internalIsTranslucent() const = 0;
+
+public:
+    virtual ~ShapePaintMutator() {}
+
+    float renderOpacity() const { return m_RenderOpacity; }
+    void renderOpacity(float value);
+
+    Component* component() const { return m_Component; }
+
+    bool isTranslucent() const { return m_RenderOpacity < 1 || this->internalIsTranslucent(); }
+
+    virtual void applyTo(RenderPaint* renderPaint, float opacityModifier) const = 0;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/shapes/paint/solid_color.hpp b/include/rive/shapes/paint/solid_color.hpp
new file mode 100644
index 0000000..7e9ccca
--- /dev/null
+++ b/include/rive/shapes/paint/solid_color.hpp
@@ -0,0 +1,20 @@
+#ifndef _RIVE_SOLID_COLOR_HPP_
+#define _RIVE_SOLID_COLOR_HPP_
+#include "rive/generated/shapes/paint/solid_color_base.hpp"
+#include "rive/shapes/paint/shape_paint_mutator.hpp"
+namespace rive
+{
+class SolidColor : public SolidColorBase, public ShapePaintMutator
+{
+public:
+    StatusCode onAddedDirty(CoreContext* context) override;
+    void applyTo(RenderPaint* renderPaint, float opacityModifier) const override;
+
+protected:
+    void renderOpacityChanged() override;
+    void colorValueChanged() override;
+    bool internalIsTranslucent() const override;
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/shapes/paint/stroke.hpp b/include/rive/shapes/paint/stroke.hpp
new file mode 100644
index 0000000..99a7a0a
--- /dev/null
+++ b/include/rive/shapes/paint/stroke.hpp
@@ -0,0 +1,34 @@
+#ifndef _RIVE_STROKE_HPP_
+#define _RIVE_STROKE_HPP_
+#include "rive/generated/shapes/paint/stroke_base.hpp"
+#include "rive/shapes/path_flags.hpp"
+namespace rive
+{
+class StrokeEffect;
+class Stroke : public StrokeBase
+{
+private:
+    StrokeEffect* m_Effect = nullptr;
+
+public:
+    RenderPaint* initRenderPaint(ShapePaintMutator* mutator) override;
+    PathFlags pathFlags() const override;
+    void draw(Renderer* renderer,
+              CommandPath* path,
+              const RawPath* rawPath,
+              RenderPaint* paint) override;
+    void addStrokeEffect(StrokeEffect* effect);
+    bool hasStrokeEffect() { return m_Effect != nullptr; }
+    void invalidateEffects();
+    bool isVisible() const override;
+    void invalidateRendering();
+    void applyTo(RenderPaint* renderPaint, float opacityModifier) const override;
+
+protected:
+    void thicknessChanged() override;
+    void capChanged() override;
+    void joinChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/shapes/paint/stroke_cap.hpp b/include/rive/shapes/paint/stroke_cap.hpp
new file mode 100644
index 0000000..243a973
--- /dev/null
+++ b/include/rive/shapes/paint/stroke_cap.hpp
@@ -0,0 +1,18 @@
+#ifndef _RIVE_STROKE_CAP_HPP_
+#define _RIVE_STROKE_CAP_HPP_
+namespace rive
+{
+/// Style used for stroke line endings.
+enum class StrokeCap : unsigned int
+{
+    /// Flat edge at the start/end of the stroke.
+    butt = 0,
+
+    /// Circular edge at the start/end of the stroke.
+    round = 1,
+
+    /// Flat protruding edge at the start/end of the stroke.
+    square = 2
+};
+} // namespace rive
+#endif
diff --git a/include/rive/shapes/paint/stroke_effect.hpp b/include/rive/shapes/paint/stroke_effect.hpp
new file mode 100644
index 0000000..72ed716
--- /dev/null
+++ b/include/rive/shapes/paint/stroke_effect.hpp
@@ -0,0 +1,20 @@
+#ifndef _RIVE_STROKE_EFFECT_HPP_
+#define _RIVE_STROKE_EFFECT_HPP_
+
+#include "rive/rive_types.hpp"
+
+namespace rive
+{
+class Factory;
+class RenderPath;
+class RawPath;
+
+class StrokeEffect
+{
+public:
+    virtual ~StrokeEffect() {}
+    virtual RenderPath* effectPath(const RawPath& source, Factory*) = 0;
+    virtual void invalidateEffect() = 0;
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/shapes/paint/stroke_join.hpp b/include/rive/shapes/paint/stroke_join.hpp
new file mode 100644
index 0000000..b0f3810
--- /dev/null
+++ b/include/rive/shapes/paint/stroke_join.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_STROKE_JOIN_HPP_
+#define _RIVE_STROKE_JOIN_HPP_
+namespace rive
+{
+/// Style used for stroke segment joins when there is a sharp change.
+enum class StrokeJoin : unsigned int
+{
+    /// Makes a sharp corner at the joint.
+    miter = 0,
+
+    /// Smoothens the joint with a circular/semi-circular shape at the
+    /// joint.
+    round = 1,
+
+    /// Creates a beveled edge at the joint.
+    bevel = 2
+};
+} // namespace rive
+#endif
diff --git a/include/rive/shapes/paint/trim_path.hpp b/include/rive/shapes/paint/trim_path.hpp
new file mode 100644
index 0000000..150af98
--- /dev/null
+++ b/include/rive/shapes/paint/trim_path.hpp
@@ -0,0 +1,46 @@
+#ifndef _RIVE_TRIM_PATH_HPP_
+#define _RIVE_TRIM_PATH_HPP_
+#include "rive/generated/shapes/paint/trim_path_base.hpp"
+#include "rive/shapes/paint/stroke_effect.hpp"
+#include "rive/renderer.hpp"
+#include "rive/math/raw_path.hpp"
+#include "rive/math/contour_measure.hpp"
+
+namespace rive
+{
+enum class TrimPathMode : uint8_t
+{
+    sequential = 1,
+    synchronized = 2
+
+};
+
+class TrimPath : public TrimPathBase, public StrokeEffect
+{
+public:
+    StatusCode onAddedClean(CoreContext* context) override;
+    RenderPath* effectPath(const RawPath& source, Factory*) override;
+    void invalidateEffect() override;
+
+    void startChanged() override;
+    void endChanged() override;
+    void offsetChanged() override;
+    void modeValueChanged() override;
+
+    TrimPathMode mode() const { return (TrimPathMode)modeValue(); }
+
+    StatusCode onAddedDirty(CoreContext* context) override;
+
+    const RawPath& rawPath() const { return m_rawPath; }
+
+private:
+    void invalidateTrim();
+    void trimRawPath(const RawPath& source);
+    RawPath m_rawPath;
+    std::vector<rcp<ContourMeasure>> m_contours;
+    rcp<RenderPath> m_trimmedPath;
+    RenderPath* m_renderPath = nullptr;
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/shapes/parametric_path.hpp b/include/rive/shapes/parametric_path.hpp
new file mode 100644
index 0000000..a7469fe
--- /dev/null
+++ b/include/rive/shapes/parametric_path.hpp
@@ -0,0 +1,25 @@
+#ifndef _RIVE_PARAMETRIC_PATH_HPP_
+#define _RIVE_PARAMETRIC_PATH_HPP_
+#include "rive/math/aabb.hpp"
+#include "rive/generated/shapes/parametric_path_base.hpp"
+namespace rive
+{
+class ParametricPath : public ParametricPathBase
+{
+public:
+    Vec2D measureLayout(float width,
+                        LayoutMeasureMode widthMode,
+                        float height,
+                        LayoutMeasureMode heightMode) override;
+    void controlSize(Vec2D size) override;
+    void markPathDirty(bool sendToLayout = true) override;
+
+protected:
+    void widthChanged() override;
+    void heightChanged() override;
+    void originXChanged() override;
+    void originYChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/shapes/path.hpp b/include/rive/shapes/path.hpp
new file mode 100644
index 0000000..0984138
--- /dev/null
+++ b/include/rive/shapes/path.hpp
@@ -0,0 +1,77 @@
+#ifndef _RIVE_PATH_HPP_
+#define _RIVE_PATH_HPP_
+#include "rive/command_path.hpp"
+#include "rive/generated/shapes/path_base.hpp"
+#include "rive/math/mat2d.hpp"
+#include "rive/math/raw_path.hpp"
+#include "rive/shapes/shape_paint_container.hpp"
+#include <vector>
+
+namespace rive
+{
+class Shape;
+class PathVertex;
+
+#ifdef ENABLE_QUERY_FLAT_VERTICES
+/// Optionally compiled in for tools that need to compute per frame world
+/// transformed path vertices. These should not be used at runtime as it's
+/// not optimized for performance (it does a lot of memory allocation).
+
+/// A flattened path is composed of only linear
+/// and cubic vertices. No corner vertices and it's entirely in world space.
+/// This is helpful for getting a close to identical representation of the
+/// vertices used to issue the high level path draw commands.
+class FlattenedPath
+{
+private:
+    std::vector<PathVertex*> m_Vertices;
+
+public:
+    ~FlattenedPath();
+
+    const std::vector<PathVertex*>& vertices() const { return m_Vertices; }
+    void addVertex(PathVertex* vertex, const Mat2D& transform);
+};
+#endif
+
+class Path : public PathBase
+{
+protected:
+    Shape* m_Shape = nullptr;
+    std::vector<PathVertex*> m_Vertices;
+    bool m_deferredPathDirt = false;
+    PathFlags m_pathFlags = PathFlags::none;
+    RawPath m_rawPath;
+
+public:
+    Shape* shape() const { return m_Shape; }
+    StatusCode onAddedClean(CoreContext* context) override;
+    void buildDependencies() override;
+    virtual const Mat2D& pathTransform() const;
+    bool collapse(bool value) override;
+    const RawPath& rawPath() const { return m_rawPath; }
+    void update(ComponentDirt value) override;
+
+    void addFlags(PathFlags flags);
+    bool isFlagged(PathFlags flags) const;
+
+    bool canDeferPathUpdate();
+    void addVertex(PathVertex* vertex);
+
+    virtual void markPathDirty(bool sendToLayout = true);
+    virtual bool isPathClosed() const { return true; }
+    void onDirty(ComponentDirt dirt) override;
+    inline bool isHidden() const { return (pathFlags() & 0x1) == 0x1; }
+#ifdef ENABLE_QUERY_FLAT_VERTICES
+    FlattenedPath* makeFlat(bool transformToParent);
+#endif
+
+#ifdef TESTING
+    std::vector<PathVertex*>& vertices() { return m_Vertices; }
+#endif
+
+    void buildPath(RawPath&) const;
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/shapes/path_composer.hpp b/include/rive/shapes/path_composer.hpp
new file mode 100644
index 0000000..b9b3c8f
--- /dev/null
+++ b/include/rive/shapes/path_composer.hpp
@@ -0,0 +1,39 @@
+#ifndef _RIVE_PATH_COMPOSER_HPP_
+#define _RIVE_PATH_COMPOSER_HPP_
+#include "rive/component.hpp"
+#include "rive/refcnt.hpp"
+#include "rive/math/raw_path.hpp"
+
+namespace rive
+{
+class Shape;
+class CommandPath;
+class RenderPath;
+class PathComposer : public Component
+{
+
+public:
+    PathComposer(Shape* shape);
+    Shape* shape() const { return m_shape; }
+    void buildDependencies() override;
+    void onDirty(ComponentDirt dirt) override;
+    void update(ComponentDirt value) override;
+
+    CommandPath* localPath() const { return m_localPath.get(); }
+    CommandPath* worldPath() const { return m_worldPath.get(); }
+
+    const RawPath& localRawPath() const { return m_localRawPath; }
+    const RawPath& worldRawPath() const { return m_worldRawPath; }
+
+    void pathCollapseChanged();
+
+private:
+    Shape* m_shape;
+    RawPath m_localRawPath;
+    RawPath m_worldRawPath;
+    rcp<CommandPath> m_localPath;
+    rcp<CommandPath> m_worldPath;
+    bool m_deferredPathDirt;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/shapes/path_flags.hpp b/include/rive/shapes/path_flags.hpp
new file mode 100644
index 0000000..172ed42
--- /dev/null
+++ b/include/rive/shapes/path_flags.hpp
@@ -0,0 +1,66 @@
+#ifndef _RIVE_PATH_FLAGS_HPP_
+#define _RIVE_PATH_FLAGS_HPP_
+
+#include "rive/rive_types.hpp"
+
+namespace rive
+{
+enum class PathFlags : uint8_t
+{
+    none = 0,
+    local = 1 << 1,
+    world = 1 << 2,
+    clipping = 1 << 3,
+    followPath = 1 << 4,
+    neverDeferUpdate = 1 << 5,
+};
+
+inline constexpr PathFlags operator&(PathFlags lhs, PathFlags rhs)
+{
+    return static_cast<PathFlags>(static_cast<std::underlying_type<PathFlags>::type>(lhs) &
+                                  static_cast<std::underlying_type<PathFlags>::type>(rhs));
+}
+
+inline constexpr PathFlags operator^(PathFlags lhs, PathFlags rhs)
+{
+    return static_cast<PathFlags>(static_cast<std::underlying_type<PathFlags>::type>(lhs) ^
+                                  static_cast<std::underlying_type<PathFlags>::type>(rhs));
+}
+
+inline constexpr PathFlags operator|(PathFlags lhs, PathFlags rhs)
+{
+    return static_cast<PathFlags>(static_cast<std::underlying_type<PathFlags>::type>(lhs) |
+                                  static_cast<std::underlying_type<PathFlags>::type>(rhs));
+}
+
+inline constexpr PathFlags operator~(PathFlags rhs)
+{
+    return static_cast<PathFlags>(~static_cast<std::underlying_type<PathFlags>::type>(rhs));
+}
+
+inline PathFlags& operator|=(PathFlags& lhs, PathFlags rhs)
+{
+    lhs = static_cast<PathFlags>(static_cast<std::underlying_type<PathFlags>::type>(lhs) |
+                                 static_cast<std::underlying_type<PathFlags>::type>(rhs));
+
+    return lhs;
+}
+
+inline PathFlags& operator&=(PathFlags& lhs, PathFlags rhs)
+{
+    lhs = static_cast<PathFlags>(static_cast<std::underlying_type<PathFlags>::type>(lhs) &
+                                 static_cast<std::underlying_type<PathFlags>::type>(rhs));
+
+    return lhs;
+}
+
+inline PathFlags& operator^=(PathFlags& lhs, PathFlags rhs)
+{
+    lhs = static_cast<PathFlags>(static_cast<std::underlying_type<PathFlags>::type>(lhs) ^
+                                 static_cast<std::underlying_type<PathFlags>::type>(rhs));
+
+    return lhs;
+}
+} // namespace rive
+
+#endif
diff --git a/include/rive/shapes/path_vertex.hpp b/include/rive/shapes/path_vertex.hpp
new file mode 100644
index 0000000..1df0872
--- /dev/null
+++ b/include/rive/shapes/path_vertex.hpp
@@ -0,0 +1,17 @@
+#ifndef _RIVE_PATH_VERTEX_HPP_
+#define _RIVE_PATH_VERTEX_HPP_
+#include "rive/bones/weight.hpp"
+#include "rive/generated/shapes/path_vertex_base.hpp"
+#include "rive/math/mat2d.hpp"
+namespace rive
+{
+class PathVertex : public PathVertexBase
+{
+
+public:
+    StatusCode onAddedDirty(CoreContext* context) override;
+    void markGeometryDirty() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/shapes/points_path.hpp b/include/rive/shapes/points_path.hpp
new file mode 100644
index 0000000..743bf9c
--- /dev/null
+++ b/include/rive/shapes/points_path.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_POINTS_PATH_HPP_
+#define _RIVE_POINTS_PATH_HPP_
+#include "rive/bones/skinnable.hpp"
+#include "rive/generated/shapes/points_path_base.hpp"
+namespace rive
+{
+class PointsPath : public PointsPathBase, public Skinnable
+{
+public:
+    bool isPathClosed() const override { return isClosed(); }
+    void buildDependencies() override;
+    void update(ComponentDirt value) override;
+    void markPathDirty(bool sendToLayout = true) override;
+    void markSkinDirty() override;
+    const Mat2D& pathTransform() const override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/shapes/polygon.hpp b/include/rive/shapes/polygon.hpp
new file mode 100644
index 0000000..7d2ed4b
--- /dev/null
+++ b/include/rive/shapes/polygon.hpp
@@ -0,0 +1,27 @@
+#ifndef _RIVE_POLYGON_HPP_
+#define _RIVE_POLYGON_HPP_
+#include "rive/generated/shapes/polygon_base.hpp"
+#include "rive/shapes/path_vertex.hpp"
+#include "rive/shapes/straight_vertex.hpp"
+#include <vector>
+namespace rive
+{
+class Polygon : public PolygonBase
+{
+protected:
+    std::vector<StraightVertex> m_PolygonVertices;
+
+public:
+    Polygon();
+    ~Polygon() override;
+    void update(ComponentDirt value) override;
+
+protected:
+    void cornerRadiusChanged() override;
+    void pointsChanged() override;
+    virtual std::size_t vertexCount();
+    virtual void buildPolygon();
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/shapes/rectangle.hpp b/include/rive/shapes/rectangle.hpp
new file mode 100644
index 0000000..26bcdc1
--- /dev/null
+++ b/include/rive/shapes/rectangle.hpp
@@ -0,0 +1,24 @@
+#ifndef _RIVE_RECTANGLE_HPP_
+#define _RIVE_RECTANGLE_HPP_
+#include "rive/generated/shapes/rectangle_base.hpp"
+#include "rive/shapes/straight_vertex.hpp"
+
+namespace rive
+{
+class Rectangle : public RectangleBase
+{
+    StraightVertex m_Vertex1, m_Vertex2, m_Vertex3, m_Vertex4;
+
+public:
+    Rectangle();
+    void update(ComponentDirt value) override;
+
+protected:
+    void cornerRadiusTLChanged() override;
+    void cornerRadiusTRChanged() override;
+    void cornerRadiusBLChanged() override;
+    void cornerRadiusBRChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/shapes/shape.hpp b/include/rive/shapes/shape.hpp
new file mode 100644
index 0000000..cfb2d26
--- /dev/null
+++ b/include/rive/shapes/shape.hpp
@@ -0,0 +1,79 @@
+#ifndef _RIVE_SHAPE_HPP_
+#define _RIVE_SHAPE_HPP_
+
+#include "rive/hit_info.hpp"
+#include "rive/generated/shapes/shape_base.hpp"
+#include "rive/shapes/path_composer.hpp"
+#include "rive/shapes/shape_paint_container.hpp"
+#include "rive/drawable_flag.hpp"
+#include <vector>
+
+namespace rive
+{
+class Path;
+class PathComposer;
+class HitTester;
+class Shape : public ShapeBase, public ShapePaintContainer
+{
+private:
+    PathComposer m_PathComposer;
+    std::vector<Path*> m_Paths;
+    AABB m_WorldBounds;
+
+    bool m_WantDifferencePath = false;
+
+    Artboard* getArtboard() override { return artboard(); }
+
+public:
+    Shape();
+    void buildDependencies() override;
+    bool collapse(bool value) override;
+    bool canDeferPathUpdate();
+    void addPath(Path* path);
+    void addToRenderPath(RenderPath* commandPath, const Mat2D& transform);
+    std::vector<Path*>& paths() { return m_Paths; }
+
+    bool wantDifferencePath() const { return m_WantDifferencePath; }
+
+    void update(ComponentDirt value) override;
+    void draw(Renderer* renderer) override;
+    Core* hitTest(HitInfo*, const Mat2D&) override;
+    bool hitTest(const IAABB& area) const;
+
+    const PathComposer* pathComposer() const { return &m_PathComposer; }
+    PathComposer* pathComposer() { return &m_PathComposer; }
+
+    void pathChanged();
+    void addFlags(PathFlags flags);
+    bool isFlagged(PathFlags flags) const;
+    StatusCode onAddedDirty(CoreContext* context) override;
+    bool isEmpty();
+    void pathCollapseChanged();
+
+    AABB worldBounds()
+    {
+        if ((static_cast<DrawableFlag>(drawableFlags()) & DrawableFlag::WorldBoundsClean) !=
+            DrawableFlag::WorldBoundsClean)
+        {
+            drawableFlags(drawableFlags() |
+                          static_cast<unsigned short>(DrawableFlag::WorldBoundsClean));
+            m_WorldBounds = computeWorldBounds();
+        }
+        return m_WorldBounds;
+    }
+    void markBoundsDirty()
+    {
+        drawableFlags(drawableFlags() &
+                      ~static_cast<unsigned short>(DrawableFlag::WorldBoundsClean));
+    }
+
+    AABB computeWorldBounds(const Mat2D* xform = nullptr) const;
+    AABB computeLocalBounds() const;
+    Vec2D measureLayout(float width,
+                        LayoutMeasureMode widthMode,
+                        float height,
+                        LayoutMeasureMode heightMode) override;
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/shapes/shape_paint_container.hpp b/include/rive/shapes/shape_paint_container.hpp
new file mode 100644
index 0000000..8d394ce
--- /dev/null
+++ b/include/rive/shapes/shape_paint_container.hpp
@@ -0,0 +1,46 @@
+#ifndef _RIVE_SHAPE_PAINT_CONTAINER_HPP_
+#define _RIVE_SHAPE_PAINT_CONTAINER_HPP_
+#include "rive/refcnt.hpp"
+#include "rive/shapes/path_flags.hpp"
+#include <vector>
+
+namespace rive
+{
+class Artboard;
+class ShapePaint;
+class Component;
+
+class CommandPath;
+
+class ShapePaintContainer
+{
+    friend class ShapePaint;
+
+protected:
+    // Need this to access our artboard. We are treated as a mixin, either
+    // as a Shape or Artboard, so both of those will override this.
+    virtual Artboard* getArtboard() = 0;
+
+    PathFlags m_pathFlags = PathFlags::none;
+    std::vector<ShapePaint*> m_ShapePaints;
+    void addPaint(ShapePaint* paint);
+
+    // TODO: void draw(Renderer* renderer, PathComposer& composer);
+public:
+    static ShapePaintContainer* from(Component* component);
+
+    virtual ~ShapePaintContainer() {}
+
+    PathFlags pathFlags() const;
+
+    void invalidateStrokeEffects();
+
+    void propagateOpacity(float opacity);
+
+#ifdef TESTING
+    const std::vector<ShapePaint*>& shapePaints() const { return m_ShapePaints; }
+#endif
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/shapes/star.hpp b/include/rive/shapes/star.hpp
new file mode 100644
index 0000000..903bd51
--- /dev/null
+++ b/include/rive/shapes/star.hpp
@@ -0,0 +1,20 @@
+#ifndef _RIVE_STAR_HPP_
+#define _RIVE_STAR_HPP_
+#include "rive/generated/shapes/star_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class Star : public StarBase
+{
+public:
+    Star();
+    void update(ComponentDirt value) override;
+
+protected:
+    void innerRadiusChanged() override;
+    std::size_t vertexCount() override;
+    void buildPolygon() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/shapes/straight_vertex.hpp b/include/rive/shapes/straight_vertex.hpp
new file mode 100644
index 0000000..ae0e4e3
--- /dev/null
+++ b/include/rive/shapes/straight_vertex.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_STRAIGHT_VERTEX_HPP_
+#define _RIVE_STRAIGHT_VERTEX_HPP_
+#include "rive/generated/shapes/straight_vertex_base.hpp"
+namespace rive
+{
+class StraightVertex : public StraightVertexBase
+{
+protected:
+    void radiusChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/shapes/triangle.hpp b/include/rive/shapes/triangle.hpp
new file mode 100644
index 0000000..ff44a5b
--- /dev/null
+++ b/include/rive/shapes/triangle.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_TRIANGLE_HPP_
+#define _RIVE_TRIANGLE_HPP_
+#include "rive/generated/shapes/triangle_base.hpp"
+#include "rive/shapes/straight_vertex.hpp"
+
+namespace rive
+{
+class Triangle : public TriangleBase
+{
+private:
+    StraightVertex m_Vertex1, m_Vertex2, m_Vertex3;
+
+public:
+    Triangle();
+    void update(ComponentDirt value) override;
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/shapes/vertex.hpp b/include/rive/shapes/vertex.hpp
new file mode 100644
index 0000000..37624e2
--- /dev/null
+++ b/include/rive/shapes/vertex.hpp
@@ -0,0 +1,34 @@
+#ifndef _RIVE_VERTEX_HPP_
+#define _RIVE_VERTEX_HPP_
+#include "rive/bones/weight.hpp"
+#include "rive/generated/shapes/vertex_base.hpp"
+#include "rive/math/mat2d.hpp"
+namespace rive
+{
+class Vertex : public VertexBase
+{
+    friend class Weight;
+
+private:
+    Weight* m_Weight = nullptr;
+    void weight(Weight* value) { m_Weight = value; }
+
+public:
+    template <typename T> T* weight() { return m_Weight->as<T>(); }
+    virtual void deform(const Mat2D& worldTransform, const float* boneTransforms);
+    bool hasWeight() { return m_Weight != nullptr; }
+    Vec2D renderTranslation();
+
+protected:
+    virtual void markGeometryDirty() = 0;
+    void xChanged() override;
+    void yChanged() override;
+
+#ifdef TESTING
+public:
+    Weight* weight() { return m_Weight; }
+#endif
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/simple_array.hpp b/include/rive/simple_array.hpp
new file mode 100644
index 0000000..110c46f
--- /dev/null
+++ b/include/rive/simple_array.hpp
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_SIMPLE_ARRAY_HPP_
+#define _RIVE_SIMPLE_ARRAY_HPP_
+
+#include "rive/rive_types.hpp"
+
+#include <initializer_list>
+#include <type_traits>
+#include <cstring>
+#include <iterator>
+
+namespace rive
+{
+
+template <typename T> class SimpleArrayBuilder;
+
+#ifdef TESTING
+namespace SimpleArrayTesting
+{
+extern int mallocCount;
+extern int reallocCount;
+extern int freeCount;
+void resetCounters();
+} // namespace SimpleArrayTesting
+#endif
+
+// Helper for constructing and destructing arrays of objects.
+template <typename T, bool IsPOD = std::is_pod<T>()> class SimpleArrayHelper
+{
+public:
+    static_assert(!std::is_pod<T>(), "This helper is for non-POD types.");
+    static void DefaultConstructArray(T* ptr, T* end)
+    {
+        for (; ptr < end; ++ptr)
+            new (ptr) T();
+    }
+    static void CopyConstructArray(const T* first, const T* end, T* ptr)
+    {
+        for (; first < end; ++first, ++ptr)
+            new (ptr) T(*first);
+    }
+    static void DestructArray(T* ptr, T* end)
+    {
+        for (; ptr < end; ++ptr)
+            ptr->~T();
+    }
+};
+
+// Specialized helper for constructing and destructing arrays of POD objects.
+template <typename T> class SimpleArrayHelper<T, true>
+{
+public:
+    static_assert(std::is_pod<T>(), "This helper is only for POD types.");
+    static void DefaultConstructArray(T* ptr, T* end) {}
+    static void CopyConstructArray(const T* first, const T* end, T* ptr)
+    {
+        memcpy(ptr, first, reinterpret_cast<uintptr_t>(end) - reinterpret_cast<uintptr_t>(first));
+    }
+    static void DestructArray(T* ptr, T* end) {}
+};
+
+/// Lightweight heap array meant to be used when knowing the exact memory layout
+/// of the simple array is necessary, like marshaling the data to Dart/C#/Wasm.
+/// Note that it intentionally doesn't have push/add/resize functionality as
+/// that's reserved for a special case builder that should be to build up the
+/// array. This saves the structure from needing to store extra ptrs and keeps
+/// it optimally sized for marshaling. See SimpleArrayBuilder<T> below for push
+/// functionality.
+
+template <typename T> class SimpleArray
+{
+public:
+    SimpleArray() : m_ptr(nullptr), m_size(0) {}
+    SimpleArray(size_t size) : m_ptr(static_cast<T*>(malloc(size * sizeof(T)))), m_size(size)
+    {
+        SimpleArrayHelper<T>::DefaultConstructArray(m_ptr, m_ptr + m_size);
+#ifdef TESTING
+        SimpleArrayTesting::mallocCount++;
+#endif
+    }
+    SimpleArray(const T* ptr, size_t size) : SimpleArray(size)
+    {
+        assert(ptr <= ptr + size);
+        SimpleArrayHelper<T>::CopyConstructArray(ptr, ptr + size, m_ptr);
+    }
+
+    constexpr SimpleArray(const SimpleArray<T>& other) : SimpleArray(other.m_ptr, other.m_size) {}
+
+    SimpleArray(SimpleArray<T>&& other) : m_ptr(other.m_ptr), m_size(other.m_size)
+    {
+        other.m_ptr = nullptr;
+        other.m_size = 0;
+    }
+
+    SimpleArray(SimpleArrayBuilder<T>&& other);
+
+    SimpleArray<T>& operator=(const SimpleArray<T>& other) = delete;
+
+    SimpleArray<T>& operator=(SimpleArray<T>&& other)
+    {
+        SimpleArrayHelper<T>::DestructArray(m_ptr, m_ptr + m_size);
+        free(m_ptr);
+        m_ptr = other.m_ptr;
+        m_size = other.m_size;
+        other.m_ptr = nullptr;
+        other.m_size = 0;
+        return *this;
+    }
+
+    SimpleArray<T>& operator=(SimpleArrayBuilder<T>&& other);
+
+    template <typename Container>
+    constexpr SimpleArray(Container& c) : SimpleArray(c.data(), c.size())
+    {}
+    constexpr SimpleArray(const std::initializer_list<T>& il) : SimpleArray(il.begin(), il.size())
+    {}
+    ~SimpleArray()
+    {
+        SimpleArrayHelper<T>::DestructArray(m_ptr, m_ptr + m_size);
+        free(m_ptr);
+#ifdef TESTING
+        SimpleArrayTesting::freeCount++;
+#endif
+    }
+
+    T& operator[](size_t index) const { return m_ptr[index]; }
+
+    constexpr T* data() const { return m_ptr; }
+    constexpr size_t size() const { return m_size; }
+    constexpr bool empty() const { return m_size == 0; }
+
+    constexpr T* begin() const { return m_ptr; }
+    constexpr T* end() const { return m_ptr + m_size; }
+
+    constexpr T& front() const { return (*this)[0]; }
+    constexpr T& back() const { return (*this)[m_size - 1]; }
+
+    // returns byte-size of the entire simple array
+    constexpr size_t size_bytes() const { return m_size * sizeof(T); }
+
+    // Makes rive::SimpleArray std::Container compatible
+    // https://en.cppreference.com/w/cpp/named_req/Container
+    typedef typename std::remove_cv<T>::type value_type;
+    typedef T& reference;
+    typedef T const& const_reference;
+    typedef T* iterator;
+    typedef T const* const_iterator;
+    typedef std::ptrdiff_t difference_type;
+    typedef size_t size_type;
+
+protected:
+    T* m_ptr;
+    size_t m_size;
+};
+
+/// Extension of SimpleArray which can progressively expand as contents are
+/// pushed/added/written to it. Can be released as a simple SimpleArray.
+template <typename T> class SimpleArrayBuilder : public SimpleArray<T>
+{
+    friend class SimpleArray<T>;
+
+public:
+    SimpleArrayBuilder(size_t reserve) : SimpleArray<T>(reserve)
+    {
+        assert(this->m_ptr <= this->m_ptr + this->m_size);
+        m_write = this->m_ptr;
+    }
+
+    SimpleArrayBuilder() : SimpleArrayBuilder(0) {}
+
+    void add(const T& value)
+    {
+        growToFit();
+        *m_write++ = value;
+    }
+
+    void add(T&& value)
+    {
+        growToFit();
+        *m_write++ = std::move(value);
+    }
+
+    // Allows iterating just the written content.
+    constexpr size_t capacity() const { return this->m_size; }
+    constexpr size_t size() const { return m_write - this->m_ptr; }
+    constexpr bool empty() const { return size() == 0; }
+    constexpr T* begin() const { return this->m_ptr; }
+    constexpr T* end() const { return m_write; }
+
+    constexpr T& front() const { return (*this)[0]; }
+    constexpr T& back() const { return *(m_write - 1); }
+
+private:
+    void growToFit()
+    {
+        if (m_write == this->m_ptr + this->m_size)
+        {
+            auto writeOffset = m_write - this->m_ptr;
+            this->resize(std::max((size_t)1, this->m_size * 2));
+            m_write = this->m_ptr + writeOffset;
+        }
+    }
+
+    void resize(size_t size)
+    {
+        if (size == this->m_size)
+        {
+            return;
+        }
+#ifdef TESTING
+        SimpleArrayTesting::reallocCount++;
+#endif
+        // Call destructor for elements when sizing down.
+        SimpleArrayHelper<T>::DestructArray(this->m_ptr + size, this->m_ptr + this->m_size);
+        this->m_ptr = static_cast<T*>(realloc(this->m_ptr, size * sizeof(T)));
+        // Call constructor for elements when sizing up.
+        SimpleArrayHelper<T>::DefaultConstructArray(this->m_ptr + this->m_size, this->m_ptr + size);
+        this->m_size = size;
+    }
+
+    T* m_write;
+};
+
+template <typename T> SimpleArray<T>::SimpleArray(SimpleArrayBuilder<T>&& other)
+{
+    // Bring the capacity down to the actual size (this should keep the same
+    // ptr, but that's not guaranteed, so we copy the ptr after the realloc).
+    other.resize(other.size());
+    m_ptr = other.m_ptr;
+    m_size = other.m_size;
+    other.m_ptr = nullptr;
+    other.m_size = 0;
+}
+
+template <typename T> SimpleArray<T>& SimpleArray<T>::operator=(SimpleArrayBuilder<T>&& other)
+{
+    other.resize(other.size());
+    this->m_ptr = other.m_ptr;
+    this->m_size = other.m_size;
+    other.m_ptr = nullptr;
+    other.m_size = 0;
+    return *this;
+}
+
+} // namespace rive
+
+#endif
diff --git a/include/rive/solo.hpp b/include/rive/solo.hpp
new file mode 100644
index 0000000..8458d76
--- /dev/null
+++ b/include/rive/solo.hpp
@@ -0,0 +1,18 @@
+#ifndef _RIVE_SOLO_HPP_
+#define _RIVE_SOLO_HPP_
+#include "rive/generated/solo_base.hpp"
+namespace rive
+{
+class Solo : public SoloBase
+{
+public:
+    void activeComponentIdChanged() override;
+    StatusCode onAddedClean(CoreContext* context) override;
+    bool collapse(bool value) override;
+
+private:
+    void propagateCollapse(bool collapse);
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/span.hpp b/include/rive/span.hpp
new file mode 100644
index 0000000..a90e581
--- /dev/null
+++ b/include/rive/span.hpp
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_SPAN_HPP_
+#define _RIVE_SPAN_HPP_
+
+#include "rive/rive_types.hpp"
+
+#include <initializer_list>
+#include <type_traits>
+#include <iterator>
+
+/*
+ *  Span : cheap impl of std::span (which is C++20)
+ *
+ *  Inspired by Skia's SkSpan
+ */
+
+namespace rive
+{
+
+template <typename T> class Span
+{
+    T* m_Ptr;
+    size_t m_Size;
+
+public:
+    Span() : m_Ptr(nullptr), m_Size(0) {}
+    Span(T* ptr, size_t size) : m_Ptr(ptr), m_Size(size) { assert(ptr <= ptr + size); }
+
+    // Handle Span<foo> --> Span<const foo>
+    template <typename U, typename = typename std::enable_if<std::is_same<const U, T>::value>::type>
+    constexpr Span(const Span<U>& that) : Span(that.data(), that.size())
+    {}
+    template <typename Container> constexpr Span(Container& c) : Span(c.data(), c.size()) {}
+
+    T& operator[](size_t index) const
+    {
+        assert(index < m_Size);
+        return m_Ptr[index];
+    }
+
+    constexpr T* data() const { return m_Ptr; }
+    constexpr size_t size() const { return m_Size; }
+    constexpr bool empty() const { return m_Size == 0; }
+
+    constexpr T* begin() const { return m_Ptr; }
+    constexpr T* end() const { return m_Ptr + m_Size; }
+
+    constexpr T& front() const { return (*this)[0]; }
+    constexpr T& back() const { return (*this)[m_Size - 1]; }
+
+    // returns byte-size of the entire span
+    constexpr size_t size_bytes() const { return m_Size * sizeof(T); }
+
+    constexpr size_t count() const { return m_Size; }
+
+    Span<T> subset(size_t offset, size_t size) const
+    {
+        assert(offset <= m_Size);
+        assert(size <= m_Size - offset);
+        return {m_Ptr + offset, size};
+    }
+
+    // Makes rive::Span std::Container compatible
+    // https://en.cppreference.com/w/cpp/named_req/Container
+    typedef typename std::remove_cv<T>::type value_type;
+    typedef T& reference;
+    typedef T const& const_reference;
+    typedef T* iterator;
+    typedef T const* const_iterator;
+    typedef std::ptrdiff_t difference_type;
+    typedef size_t size_type;
+};
+
+template <typename T> Span<T> make_span(T* ptr, size_t size) { return Span<T>(ptr, size); }
+
+} // namespace rive
+
+#endif
diff --git a/include/rive/static_scene.hpp b/include/rive/static_scene.hpp
new file mode 100644
index 0000000..63147f8
--- /dev/null
+++ b/include/rive/static_scene.hpp
@@ -0,0 +1,26 @@
+#ifndef _RIVE_STATIC_SCENE_HPP_
+#define _RIVE_STATIC_SCENE_HPP_
+
+#include "rive/artboard.hpp"
+#include "rive/scene.hpp"
+
+namespace rive
+{
+class StaticScene : public Scene
+{
+public:
+    StaticScene(ArtboardInstance*);
+    ~StaticScene() override;
+
+    bool isTranslucent() const override;
+
+    std::string name() const override;
+
+    Loop loop() const override;
+
+    float durationSeconds() const override;
+
+    bool advanceAndApply(float seconds) override;
+};
+} // namespace rive
+#endif
diff --git a/include/rive/status_code.hpp b/include/rive/status_code.hpp
new file mode 100644
index 0000000..a7967ea
--- /dev/null
+++ b/include/rive/status_code.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_STATUS_CODE_HPP_
+#define _RIVE_STATUS_CODE_HPP_
+
+#include "rive/rive_types.hpp"
+
+namespace rive
+{
+enum class StatusCode : unsigned char
+{
+    Ok,
+    MissingObject,
+    InvalidObject,
+    FailedInversion
+};
+}
+#endif
diff --git a/include/rive/text/font_hb.hpp b/include/rive/text/font_hb.hpp
new file mode 100644
index 0000000..ea112b6
--- /dev/null
+++ b/include/rive/text/font_hb.hpp
@@ -0,0 +1,60 @@
+#ifndef _RIVE_FONT_HB_HPP_
+#define _RIVE_FONT_HB_HPP_
+
+#include "rive/factory.hpp"
+#include "rive/text_engine.hpp"
+
+#include <unordered_map>
+
+struct hb_font_t;
+struct hb_draw_funcs_t;
+struct hb_feature_t;
+
+class HBFont : public rive::Font
+{
+public:
+    // We assume ownership of font!
+    HBFont(hb_font_t* font);
+    ~HBFont() override;
+
+    Axis getAxis(uint16_t index) const override;
+    uint16_t getAxisCount() const override;
+    float getAxisValue(uint32_t axisTag) const override;
+    uint32_t getFeatureValue(uint32_t featureTag) const override;
+
+    rive::RawPath getPath(rive::GlyphID) const override;
+    rive::SimpleArray<rive::Paragraph> onShapeText(rive::Span<const rive::Unichar>,
+                                                   rive::Span<const rive::TextRun>) const override;
+    rive::SimpleArray<uint32_t> features() const override;
+    rive::rcp<Font> withOptions(rive::Span<const Coord> variableAxes,
+                                rive::Span<const Feature> features) const override;
+
+    bool hasGlyph(rive::Span<const rive::Unichar>) const override;
+
+    static rive::rcp<rive::Font> Decode(rive::Span<const uint8_t>);
+    static rive::rcp<rive::Font> FromSystem(void* systemFont);
+    hb_font_t* font() const { return m_font; }
+
+private:
+    HBFont(hb_font_t* font,
+           std::unordered_map<uint32_t, float> axisValues,
+           std::unordered_map<uint32_t, uint32_t> featureValues,
+           std::vector<hb_feature_t> features);
+
+public:
+    hb_font_t* m_font;
+
+    // The features list to pass directly to Harfbuzz.
+    std::vector<hb_feature_t> m_features;
+
+private:
+    hb_draw_funcs_t* m_drawFuncs;
+
+    // Feature value lookup based on tag.
+    std::unordered_map<uint32_t, uint32_t> m_featureValues;
+
+    // Axis value lookup based on for the feature.
+    std::unordered_map<uint32_t, float> m_axisValues;
+};
+
+#endif
diff --git a/include/rive/text/glyph_lookup.hpp b/include/rive/text/glyph_lookup.hpp
new file mode 100644
index 0000000..8355ca6
--- /dev/null
+++ b/include/rive/text/glyph_lookup.hpp
@@ -0,0 +1,27 @@
+#ifndef _RIVE_TEXT_GLYPH_LOOKUP_HPP_
+#define _RIVE_TEXT_GLYPH_LOOKUP_HPP_
+#include "rive/text_engine.hpp"
+#include <stdint.h>
+#include <vector>
+
+namespace rive
+{
+
+/// Stores the glyphId/Index representing the unicode point at i.
+class GlyphLookup
+{
+public:
+    void compute(Span<const Unichar> text, const SimpleArray<Paragraph>& shape);
+
+private:
+    std::vector<uint32_t> m_glyphIndices;
+
+public:
+    uint32_t count(uint32_t index) const;
+
+    bool empty() const { return m_glyphIndices.empty(); }
+    void clear() { m_glyphIndices.clear(); }
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/text/raw_text.hpp b/include/rive/text/raw_text.hpp
new file mode 100644
index 0000000..2d76fb8
--- /dev/null
+++ b/include/rive/text/raw_text.hpp
@@ -0,0 +1,96 @@
+#ifndef _RIVE_RENDER_TEXT_HPP_
+#define _RIVE_RENDER_TEXT_HPP_
+
+#ifdef WITH_RIVE_TEXT
+
+#include "rive/text/text.hpp"
+
+namespace rive
+{
+class Factory;
+
+class RawText
+{
+public:
+    RawText(Factory* factory);
+
+    /// Returns true if the text object contains no text.
+    bool empty() const;
+
+    /// Appends a run to the text object.
+    void append(const std::string& text,
+                rcp<RenderPaint> paint,
+                rcp<Font> font,
+                float size = 16.0f,
+                float lineHeight = -1.0f,
+                float letterSpacing = 0.0f);
+
+    /// Resets the text object to empty state (no text).
+    void clear();
+
+    /// Draw the text using renderer. Second argument is optional to override
+    /// all paints provided with run styles
+    void render(Renderer* renderer, rcp<RenderPaint> paint = nullptr);
+
+    TextSizing sizing() const;
+    TextOverflow overflow() const;
+    TextAlign align() const;
+    float maxWidth() const;
+    float maxHeight() const;
+    float paragraphSpacing() const;
+
+    void sizing(TextSizing value);
+
+    /// How text that overflows when TextSizing::fixed is used.
+    void overflow(TextOverflow value);
+
+    /// How text aligns within the bounds.
+    void align(TextAlign value);
+
+    /// The width at which the text will wrap when using any sizing but TextSizing::auto.
+    void maxWidth(float value);
+
+    /// The height at which the text will overflow when using TextSizing::fixed.
+    void maxHeight(float value);
+
+    /// The vertical space between paragraphs delineated by a return character.
+    void paragraphSpacing(float value);
+
+    /// Returns the bounds of the text object (helpful for aligning multiple
+    /// text objects/procredurally drawn shapes).
+    AABB bounds();
+
+private:
+    void update();
+    struct RenderStyle
+    {
+        rcp<RenderPaint> paint;
+        rcp<RenderPath> path;
+        bool isEmpty;
+    };
+    SimpleArray<Paragraph> m_shape;
+    SimpleArray<SimpleArray<GlyphLine>> m_lines;
+
+    StyledText m_styled;
+    Factory* m_factory;
+    std::vector<RenderStyle> m_styles;
+    std::vector<RenderStyle*> m_renderStyles;
+    bool m_dirty = false;
+    float m_paragraphSpacing = 0.0f;
+
+    TextOrigin m_origin = TextOrigin::top;
+    TextSizing m_sizing = TextSizing::autoWidth;
+    TextOverflow m_overflow = TextOverflow::visible;
+    TextAlign m_align = TextAlign::left;
+    float m_maxWidth = 0.0f;
+    float m_maxHeight = 0.0f;
+    std::vector<OrderedLine> m_orderedLines;
+    GlyphRun m_ellipsisRun;
+    AABB m_bounds;
+    rcp<RenderPath> m_clipRenderPath;
+};
+} // namespace rive
+
+#endif // WITH_RIVE_TEXT
+
+#endif
diff --git a/include/rive/text/text.hpp b/include/rive/text/text.hpp
new file mode 100644
index 0000000..b0267ab
--- /dev/null
+++ b/include/rive/text/text.hpp
@@ -0,0 +1,260 @@
+#ifndef _RIVE_TEXT_CORE_HPP_
+#define _RIVE_TEXT_CORE_HPP_
+#include "rive/generated/text/text_base.hpp"
+#include "rive/math/aabb.hpp"
+#include "rive/text/text_value_run.hpp"
+#include "rive/text_engine.hpp"
+#include "rive/simple_array.hpp"
+#include <vector>
+#include "rive/text/glyph_lookup.hpp"
+namespace rive
+{
+
+enum class TextSizing : uint8_t
+{
+    autoWidth,
+    autoHeight,
+    fixed
+};
+
+enum class TextOverflow : uint8_t
+{
+    visible,
+    hidden,
+    clipped,
+    ellipsis
+};
+
+enum class TextOrigin : uint8_t
+{
+    top,
+    baseline
+};
+
+class OrderedLine;
+class TextModifierGroup;
+
+class StyledText
+{
+private:
+    /// Represents the unicode characters making up the entire text string
+    /// displayed. Only valid after update.
+    std::vector<Unichar> m_value;
+    std::vector<TextRun> m_runs;
+
+public:
+    bool empty() const;
+    void clear();
+    void append(rcp<Font> font,
+                float size,
+                float lineHeight,
+                float letterSpacing,
+                const std::string& text,
+                uint16_t styleId);
+    const std::vector<Unichar>& unichars() const { return m_value; }
+    const std::vector<TextRun>& runs() const { return m_runs; }
+
+    void swapRuns(std::vector<TextRun>& otherRuns) { m_runs.swap(otherRuns); }
+};
+
+// STL-style iterator for individual glyphs in a line, simplfies call sites from
+// needing to iterate both runs and glyphs within those runs per line. A single
+// iterator allows iterating all the glyphs in the line and provides the correct
+// run they belong to (this also takes into account bidi which can put the runs
+// in different order from how they were provided by the line breaker).
+//
+//   for (auto [run, glyphIndex] : orderedLine) { ... }
+//
+class GlyphItr
+{
+public:
+    GlyphItr() = default;
+    GlyphItr(const OrderedLine* line, const rive::GlyphRun* const* run, uint32_t glyphIndex) :
+        m_line(line), m_run(run), m_glyphIndex(glyphIndex)
+    {}
+
+    void tryAdvanceRun();
+
+    bool operator!=(const GlyphItr& that) const
+    {
+        return m_run != that.m_run || m_glyphIndex != that.m_glyphIndex;
+    }
+    bool operator==(const GlyphItr& that) const
+    {
+        return m_run == that.m_run && m_glyphIndex == that.m_glyphIndex;
+    }
+
+    GlyphItr& operator++();
+
+    std::tuple<const GlyphRun*, uint32_t> operator*() const { return {*m_run, m_glyphIndex}; }
+
+private:
+    const OrderedLine* m_line;
+    const rive::GlyphRun* const* m_run;
+    uint32_t m_glyphIndex;
+};
+
+// Represents a line of text with runs ordered visually. Also tracks logical
+// start/end which will defer when using bidi.
+class OrderedLine
+{
+public:
+    OrderedLine(const Paragraph& paragraph,
+                const GlyphLine& line,
+                float lineWidth, // for ellipsis
+                bool wantEllipsis,
+                bool isEllipsisLineLast,
+                GlyphRun* ellipsisRun);
+
+    bool buildEllipsisRuns(std::vector<const GlyphRun*>& logicalRuns,
+                           const Paragraph& paragraph,
+                           const GlyphLine& line,
+                           float lineWidth,
+                           bool isEllipsisLineLast,
+                           GlyphRun* ellipsisRun);
+    const GlyphRun* startLogical() const { return m_startLogical; }
+    const GlyphRun* endLogical() const { return m_endLogical; }
+    const std::vector<const GlyphRun*>& runs() const { return m_runs; }
+
+    GlyphItr begin() const
+    {
+        auto runItr = m_runs.data();
+        auto itr = GlyphItr(this, runItr, startGlyphIndex(*runItr));
+        itr.tryAdvanceRun();
+        return itr;
+    }
+
+    GlyphItr end() const
+    {
+        auto runItr = m_runs.data() + (m_runs.size() == 0 ? 0 : m_runs.size() - 1);
+        return GlyphItr(this, runItr, endGlyphIndex(*runItr));
+    }
+
+private:
+    const GlyphRun* m_startLogical = nullptr;
+    const GlyphRun* m_endLogical = nullptr;
+    uint32_t m_startGlyphIndex;
+    uint32_t m_endGlyphIndex;
+    std::vector<const GlyphRun*> m_runs;
+
+public:
+    const GlyphRun* lastRun() const { return m_runs.back(); }
+    uint32_t startGlyphIndex(const GlyphRun* run) const
+    {
+        switch (run->dir)
+        {
+            case TextDirection::ltr:
+                return m_startLogical == run ? m_startGlyphIndex : 0;
+            case TextDirection::rtl:
+                return (m_endLogical == run ? m_endGlyphIndex : (uint32_t)run->glyphs.size()) - 1;
+        }
+        RIVE_UNREACHABLE();
+    }
+    uint32_t endGlyphIndex(const GlyphRun* run) const
+    {
+        switch (run->dir)
+        {
+            case TextDirection::ltr:
+                return m_endLogical == run ? m_endGlyphIndex : (uint32_t)run->glyphs.size();
+            case TextDirection::rtl:
+                return (m_startLogical == run ? m_startGlyphIndex : 0) - 1;
+        }
+        RIVE_UNREACHABLE();
+    }
+};
+
+class TextStyle;
+class Text : public TextBase
+{
+public:
+    void draw(Renderer* renderer) override;
+    Core* hitTest(HitInfo*, const Mat2D&) override;
+    void addRun(TextValueRun* run);
+    void addModifierGroup(TextModifierGroup* group);
+    void markShapeDirty(bool sendToLayout = true);
+    void modifierShapeDirty();
+    void markPaintDirty();
+    void update(ComponentDirt value) override;
+
+    TextSizing sizing() const { return (TextSizing)sizingValue(); }
+    TextSizing effectiveSizing() const
+    {
+        return std::isnan(m_layoutHeight) ? sizing() : TextSizing::fixed;
+    }
+    TextOverflow overflow() const { return (TextOverflow)overflowValue(); }
+    TextOrigin textOrigin() const { return (TextOrigin)originValue(); }
+    void overflow(TextOverflow value) { return overflowValue((uint32_t)value); }
+    void buildRenderStyles();
+    const TextStyle* styleFromShaperId(uint16_t id) const;
+    bool modifierRangesNeedShape() const;
+    AABB localBounds() const override;
+    void originXChanged() override;
+    void originYChanged() override;
+
+    Vec2D measureLayout(float width,
+                        LayoutMeasureMode widthMode,
+                        float height,
+                        LayoutMeasureMode heightMode) override;
+    void controlSize(Vec2D size) override;
+    float effectiveWidth() { return std::isnan(m_layoutWidth) ? width() : m_layoutWidth; }
+    float effectiveHeight() { return std::isnan(m_layoutHeight) ? height() : m_layoutHeight; }
+#ifdef WITH_RIVE_TEXT
+    const std::vector<TextValueRun*>& runs() const { return m_runs; }
+    static SimpleArray<SimpleArray<GlyphLine>> BreakLines(const SimpleArray<Paragraph>& paragraphs,
+                                                          float width,
+                                                          TextAlign align);
+#endif
+
+    bool haveModifiers() const
+    {
+#ifdef WITH_RIVE_TEXT
+        return !m_modifierGroups.empty();
+#else
+        return false;
+#endif
+    }
+#ifdef TESTING
+    const std::vector<OrderedLine>& orderedLines() const { return m_orderedLines; }
+    const std::vector<TextModifierGroup*>& modifierGroups() const { return m_modifierGroups; }
+    const SimpleArray<Paragraph>& shape() const { return m_shape; }
+    const std::vector<Unichar>& unichars() const { return m_styledText.unichars(); }
+#endif
+
+protected:
+    void alignValueChanged() override;
+    void sizingValueChanged() override;
+    void overflowValueChanged() override;
+    void widthChanged() override;
+    void heightChanged() override;
+    void paragraphSpacingChanged() override;
+    bool makeStyled(StyledText& styledText, bool withModifiers = true) const;
+    void originValueChanged() override;
+
+private:
+#ifdef WITH_RIVE_TEXT
+    void updateOriginWorldTransform();
+    std::vector<TextValueRun*> m_runs;
+    std::vector<TextStyle*> m_renderStyles;
+    SimpleArray<Paragraph> m_shape;
+    SimpleArray<Paragraph> m_modifierShape;
+    SimpleArray<SimpleArray<GlyphLine>> m_lines;
+    SimpleArray<SimpleArray<GlyphLine>> m_modifierLines;
+    // Runs ordered by paragraph line.
+    std::vector<OrderedLine> m_orderedLines;
+    GlyphRun m_ellipsisRun;
+    rcp<RenderPath> m_clipRenderPath;
+    AABB m_bounds;
+    std::vector<TextModifierGroup*> m_modifierGroups;
+
+    StyledText m_styledText;
+    StyledText m_modifierStyledText;
+
+    GlyphLookup m_glyphLookup;
+#endif
+    float m_layoutWidth = NAN;
+    float m_layoutHeight = NAN;
+    Vec2D measure(Vec2D maxSize);
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/text/text_modifier.hpp b/include/rive/text/text_modifier.hpp
new file mode 100644
index 0000000..6d4b653
--- /dev/null
+++ b/include/rive/text/text_modifier.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_TEXT_MODIFIER_HPP_
+#define _RIVE_TEXT_MODIFIER_HPP_
+#include "rive/generated/text/text_modifier_base.hpp"
+
+namespace rive
+{
+class TextModifier : public TextModifierBase
+{
+public:
+    StatusCode onAddedDirty(CoreContext* context) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/text/text_modifier_flags.hpp b/include/rive/text/text_modifier_flags.hpp
new file mode 100644
index 0000000..7487cd5
--- /dev/null
+++ b/include/rive/text/text_modifier_flags.hpp
@@ -0,0 +1,73 @@
+#ifndef _RIVE_TEXT_MODIFIER_FLAGS_HPP_
+#define _RIVE_TEXT_MODIFIER_FLAGS_HPP_
+
+#include <cstdint>
+
+namespace rive
+{
+
+enum class TextModifierFlags : uint8_t
+{
+    modifyOrigin = 1 << 0,
+    modifyTranslation = 1 << 2,
+    modifyRotation = 1 << 3,
+    modifyScale = 1 << 4,
+    modifyOpacity = 1 << 5,
+    invertOpacity = 1 << 6
+};
+
+inline constexpr TextModifierFlags operator&(TextModifierFlags lhs, TextModifierFlags rhs)
+{
+    return static_cast<TextModifierFlags>(
+        static_cast<std::underlying_type<TextModifierFlags>::type>(lhs) &
+        static_cast<std::underlying_type<TextModifierFlags>::type>(rhs));
+}
+
+inline constexpr TextModifierFlags operator^(TextModifierFlags lhs, TextModifierFlags rhs)
+{
+    return static_cast<TextModifierFlags>(
+        static_cast<std::underlying_type<TextModifierFlags>::type>(lhs) ^
+        static_cast<std::underlying_type<TextModifierFlags>::type>(rhs));
+}
+
+inline constexpr TextModifierFlags operator|(TextModifierFlags lhs, TextModifierFlags rhs)
+{
+    return static_cast<TextModifierFlags>(
+        static_cast<std::underlying_type<TextModifierFlags>::type>(lhs) |
+        static_cast<std::underlying_type<TextModifierFlags>::type>(rhs));
+}
+
+inline constexpr TextModifierFlags operator~(TextModifierFlags rhs)
+{
+    return static_cast<TextModifierFlags>(
+        ~static_cast<std::underlying_type<TextModifierFlags>::type>(rhs));
+}
+
+inline TextModifierFlags& operator|=(TextModifierFlags& lhs, TextModifierFlags rhs)
+{
+    lhs = static_cast<TextModifierFlags>(
+        static_cast<std::underlying_type<TextModifierFlags>::type>(lhs) |
+        static_cast<std::underlying_type<TextModifierFlags>::type>(rhs));
+
+    return lhs;
+}
+
+inline TextModifierFlags& operator&=(TextModifierFlags& lhs, TextModifierFlags rhs)
+{
+    lhs = static_cast<TextModifierFlags>(
+        static_cast<std::underlying_type<TextModifierFlags>::type>(lhs) &
+        static_cast<std::underlying_type<TextModifierFlags>::type>(rhs));
+
+    return lhs;
+}
+
+inline TextModifierFlags& operator^=(TextModifierFlags& lhs, TextModifierFlags rhs)
+{
+    lhs = static_cast<TextModifierFlags>(
+        static_cast<std::underlying_type<TextModifierFlags>::type>(lhs) ^
+        static_cast<std::underlying_type<TextModifierFlags>::type>(rhs));
+
+    return lhs;
+}
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/include/rive/text/text_modifier_group.hpp b/include/rive/text/text_modifier_group.hpp
new file mode 100644
index 0000000..99c021b
--- /dev/null
+++ b/include/rive/text/text_modifier_group.hpp
@@ -0,0 +1,106 @@
+#ifndef _RIVE_TEXT_MODIFIER_GROUP_HPP_
+#define _RIVE_TEXT_MODIFIER_GROUP_HPP_
+#include "rive/generated/text/text_modifier_group_base.hpp"
+#include "rive/text/text_modifier_flags.hpp"
+#include "rive/text_engine.hpp"
+
+#include <vector>
+
+namespace rive
+{
+class TextModifierRange;
+class TextModifier;
+class TextShapeModifier;
+class GlyphLookup;
+class Text;
+class StyledText;
+class TextModifierGroup : public TextModifierGroupBase
+{
+public:
+    StatusCode onAddedDirty(CoreContext* context) override;
+
+    void addModifierRange(TextModifierRange* range);
+    void addModifier(TextModifier* modifier);
+    void rangeChanged();
+    void rangeTypeChanged();
+    void shapeModifierChanged();
+    void clearRangeMaps();
+    void computeRangeMap(Span<const Unichar> text,
+                         const rive::SimpleArray<rive::Paragraph>& shape,
+                         const SimpleArray<SimpleArray<GlyphLine>>& lines,
+                         const GlyphLookup& glyphLookup);
+    void computeCoverage(uint32_t textSize);
+    float glyphCoverage(uint32_t textIndex, uint32_t codePointCount);
+    float coverage(uint32_t textIndex)
+    {
+        assert(textIndex < m_coverage.size());
+        return m_coverage[textIndex];
+    }
+    void transform(float amount, Mat2D& ctm);
+    TextRun modifyShape(const Text& text, TextRun run, float strength);
+    void applyShapeModifiers(const Text& text, StyledText& styledText);
+
+    bool modifiesTransform() const
+    {
+        return (modifierFlags() &
+                (uint32_t)(TextModifierFlags::modifyTranslation |
+                           TextModifierFlags::modifyRotation | TextModifierFlags::modifyScale |
+                           TextModifierFlags::modifyOrigin)) != 0;
+    }
+
+    bool modifiesOpacity() const
+    {
+        return (modifierFlags() & (uint32_t)TextModifierFlags::modifyOpacity) != 0;
+    }
+
+    bool modifiesRotation() const
+    {
+        return (modifierFlags() & (uint32_t)TextModifierFlags::modifyRotation) != 0;
+    }
+
+    bool modifiesTranslation() const
+    {
+        return (modifierFlags() & (uint32_t)TextModifierFlags::modifyTranslation) != 0;
+    }
+
+    bool modifiesScale() const
+    {
+        return (modifierFlags() & (uint32_t)TextModifierFlags::modifyScale) != 0;
+    }
+
+    bool modifiesOrigin() const
+    {
+        return (modifierFlags() & (uint32_t)TextModifierFlags::modifyOrigin) != 0;
+    }
+
+    float computeOpacity(float current, float t) const;
+    bool needsShape() const;
+
+#ifdef TESTING
+    const std::vector<TextModifierRange*>& ranges() const { return m_ranges; }
+    const std::vector<TextModifier*>& modifiers() const { return m_modifiers; }
+#endif
+
+protected:
+    void modifierFlagsChanged() override;
+    void originXChanged() override;
+    void originYChanged() override;
+    void opacityChanged() override;
+    void xChanged() override;
+    void yChanged() override;
+    void rotationChanged() override;
+    void scaleXChanged() override;
+    void scaleYChanged() override;
+
+private:
+    std::vector<TextModifierRange*> m_ranges;
+    std::vector<TextModifier*> m_modifiers;
+    std::vector<TextShapeModifier*> m_shapeModifiers;
+    std::vector<float> m_coverage;
+    rcp<Font> m_variableFont;
+    std::vector<Font::Coord> m_variationCoords;
+    std::vector<TextRun> m_nextTextRuns;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/text/text_modifier_range.hpp b/include/rive/text/text_modifier_range.hpp
new file mode 100644
index 0000000..7aa1b95
--- /dev/null
+++ b/include/rive/text/text_modifier_range.hpp
@@ -0,0 +1,155 @@
+#ifndef _RIVE_TEXT_MODIFIER_RANGE_HPP_
+#define _RIVE_TEXT_MODIFIER_RANGE_HPP_
+#include "rive/generated/text/text_modifier_range_base.hpp"
+#include "rive/text_engine.hpp"
+#include <vector>
+
+namespace rive
+{
+enum class TextRangeUnits : uint8_t
+{
+    characters,
+    charactersExcludingSpaces,
+    words,
+    lines
+};
+
+enum class TextRangeMode : uint8_t
+{
+    add,
+    subtract,
+    multiply,
+    min,
+    max,
+    difference
+};
+
+enum class TextRangeType : uint8_t
+{
+    percentage,
+    unitIndex
+};
+
+enum class TextRangeInterpolator : uint8_t
+{
+    linear,
+    cubic
+};
+
+class GlyphLookup;
+class TextValueRun;
+class RangeMapper
+{
+public:
+    uint32_t unitCount() { return (uint32_t)m_unitLengths.size(); }
+    uint32_t unitCharacterIndexCount() { return (uint32_t)m_unitCharacterIndices.size(); }
+
+    void clear();
+    bool empty() { return m_unitLengths.empty(); }
+
+    /// Compute ranges of words.
+    void fromWords(Span<const Unichar> text, uint32_t start, uint32_t end);
+
+    /// Compute ranges of characters in text.
+    void fromCharacters(Span<const Unichar> text,
+                        uint32_t start,
+                        uint32_t end,
+                        const GlyphLookup& glyphLookup,
+                        bool withoutSpaces = false);
+
+    /// Compute ranges of lines.
+    void fromLines(Span<const Unichar> text,
+                   uint32_t start,
+                   uint32_t end,
+                   const SimpleArray<Paragraph>& shape,
+                   const SimpleArray<SimpleArray<GlyphLine>>& lines,
+                   const GlyphLookup& glyphLookup);
+
+    float unitToCharacterRange(float word) const;
+
+    uint32_t unitCharacterIndex(uint32_t at) const
+    {
+        assert(at < m_unitCharacterIndices.size());
+        return m_unitCharacterIndices[at];
+    }
+
+    uint32_t unitLength(uint32_t at) const
+    {
+        assert(at < m_unitLengths.size());
+        return m_unitLengths[at];
+    }
+
+    // Add (some of) unit at indexFrom to indexTo where it falls within the start/end offset.
+    void addRange(uint32_t indexFrom, uint32_t indexTo, uint32_t startOffset, uint32_t endOffset);
+
+private:
+    /// Each item in this list represents the index (in unicode codepoints) of
+    /// the selectable element. Always has length 1+unitLengths.length as it's
+    /// expected to always include the final index with 0 length.
+    std::vector<uint32_t> m_unitCharacterIndices;
+
+    /// Each item in this list represents the length of the matching element at
+    /// the same index in the _unitIndices list.
+    std::vector<uint32_t> m_unitLengths;
+};
+
+class CubicInterpolatorComponent;
+class TextModifierRange : public TextModifierRangeBase
+{
+public:
+    StatusCode onAddedDirty(CoreContext* context) override;
+
+    void clearRangeMap();
+    void computeRange(Span<const Unichar> text,
+                      const SimpleArray<Paragraph>& shape,
+                      const SimpleArray<SimpleArray<GlyphLine>>& lines,
+                      const GlyphLookup& glyphLookup);
+    void computeCoverage(Span<float> coverage);
+
+    TextRangeUnits units() const { return (TextRangeUnits)unitsValue(); }
+    TextRangeType type() const { return (TextRangeType)typeValue(); }
+    TextRangeMode mode() const { return (TextRangeMode)modeValue(); }
+    void addChild(Component* component) override;
+    bool needsShape() const;
+
+#ifdef TESTING
+    CubicInterpolatorComponent* interpolator() const { return m_interpolator; }
+#endif
+
+protected:
+    void modifyFromChanged() override;
+    void modifyToChanged() override;
+    void strengthChanged() override;
+    void unitsValueChanged() override;
+    void typeValueChanged() override;
+    void modeValueChanged() override;
+    void clampChanged() override;
+    void falloffFromChanged() override;
+    void falloffToChanged() override;
+    void offsetChanged() override;
+
+private:
+    RangeMapper m_rangeMapper;
+    // Cache indices.
+    float m_indexFrom = 0.0f;
+    float m_indexTo = 0.0f;
+    float m_indexFalloffFrom = 0.0f;
+    float m_indexFalloffTo = 0.0f;
+    CubicInterpolatorComponent* m_interpolator = nullptr;
+    TextValueRun* m_run = nullptr;
+
+    float offsetModifyFrom() const { return modifyFrom() + offset(); }
+    float offsetModifyTo() const { return modifyTo() + offset(); }
+    float offsetFalloffFrom() const { return falloffFrom() + offset(); }
+    float offsetFalloffTo() const { return falloffTo() + offset(); }
+
+    float coverageAt(float t);
+
+public:
+#ifdef TESTING
+    TextValueRun* run() const { return m_run; }
+#endif
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/text/text_shape_modifier.hpp b/include/rive/text/text_shape_modifier.hpp
new file mode 100644
index 0000000..d8b14de
--- /dev/null
+++ b/include/rive/text/text_shape_modifier.hpp
@@ -0,0 +1,19 @@
+#ifndef _RIVE_TEXT_SHAPE_MODIFIER_HPP_
+#define _RIVE_TEXT_SHAPE_MODIFIER_HPP_
+#include "rive/generated/text/text_shape_modifier_base.hpp"
+#include <unordered_map>
+
+namespace rive
+{
+class Font;
+class TextShapeModifier : public TextShapeModifierBase
+{
+public:
+    virtual float modify(Font* font,
+                         std::unordered_map<uint32_t, float>& variations,
+                         float fontSize,
+                         float strength) const = 0;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/text/text_style.hpp b/include/rive/text/text_style.hpp
new file mode 100644
index 0000000..68bdf0b
--- /dev/null
+++ b/include/rive/text/text_style.hpp
@@ -0,0 +1,66 @@
+#ifndef _RIVE_TEXT_STYLE_HPP_
+#define _RIVE_TEXT_STYLE_HPP_
+#include "rive/generated/text/text_style_base.hpp"
+#include "rive/shapes/shape_paint_container.hpp"
+#include "rive/assets/file_asset_referencer.hpp"
+#include "rive/assets/file_asset.hpp"
+#include "rive/assets/font_asset.hpp"
+#include <unordered_map>
+
+namespace rive
+{
+class FontAsset;
+class Font;
+class FileAsset;
+class Renderer;
+class RenderPath;
+class RenderPaint;
+
+class TextVariationHelper;
+class TextStyleAxis;
+class TextStyleFeature;
+class TextStyle : public TextStyleBase, public ShapePaintContainer, public FileAssetReferencer
+{
+private:
+    Artboard* getArtboard() override { return artboard(); }
+
+public:
+    TextStyle();
+    void buildDependencies() override;
+    const rcp<Font> font() const;
+    void setAsset(FileAsset*) override;
+    uint32_t assetId() override;
+    StatusCode import(ImportStack& importStack) override;
+
+    FontAsset* fontAsset() const { return (FontAsset*)m_fileAsset; }
+
+    bool addPath(const RawPath& rawPath, float opacity);
+    void rewindPath();
+    void draw(Renderer* renderer);
+    Core* clone() const override;
+    void addVariation(TextStyleAxis* axis);
+    void addFeature(TextStyleFeature* feature);
+    void updateVariableFont();
+    StatusCode onAddedClean(CoreContext* context) override;
+    void onDirty(ComponentDirt dirt) override;
+
+protected:
+    void fontSizeChanged() override;
+    void lineHeightChanged() override;
+    void letterSpacingChanged() override;
+
+private:
+    std::unique_ptr<TextVariationHelper> m_variationHelper;
+    std::unordered_map<float, rcp<RenderPath>> m_opacityPaths;
+    rcp<Font> m_variableFont;
+    rcp<RenderPath> m_path;
+    bool m_hasContents = false;
+    std::vector<Font::Coord> m_coords;
+    std::vector<TextStyleAxis*> m_variations;
+    std::vector<rcp<RenderPaint>> m_paintPool;
+    std::vector<TextStyleFeature*> m_styleFeatures;
+    std::vector<Font::Feature> m_features;
+};
+} // namespace rive
+
+#endif
diff --git a/include/rive/text/text_style_axis.hpp b/include/rive/text/text_style_axis.hpp
new file mode 100644
index 0000000..695df58
--- /dev/null
+++ b/include/rive/text/text_style_axis.hpp
@@ -0,0 +1,16 @@
+#ifndef _RIVE_TEXT_STYLE_AXIS_HPP_
+#define _RIVE_TEXT_STYLE_AXIS_HPP_
+#include "rive/generated/text/text_style_axis_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TextStyleAxis : public TextStyleAxisBase
+{
+public:
+    StatusCode onAddedDirty(CoreContext* context) override;
+    void tagChanged() override;
+    void axisValueChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/text/text_style_feature.hpp b/include/rive/text/text_style_feature.hpp
new file mode 100644
index 0000000..fd0bb75
--- /dev/null
+++ b/include/rive/text/text_style_feature.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_TEXT_STYLE_FEATURE_HPP_
+#define _RIVE_TEXT_STYLE_FEATURE_HPP_
+#include "rive/generated/text/text_style_feature_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class TextStyleFeature : public TextStyleFeatureBase
+{
+public:
+    StatusCode onAddedDirty(CoreContext* context) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/text/text_value_run.hpp b/include/rive/text/text_value_run.hpp
new file mode 100644
index 0000000..5a61882
--- /dev/null
+++ b/include/rive/text/text_value_run.hpp
@@ -0,0 +1,43 @@
+#ifndef _RIVE_TEXT_VALUE_RUN_HPP_
+#define _RIVE_TEXT_VALUE_RUN_HPP_
+#include "rive/generated/text/text_value_run_base.hpp"
+#include "rive/text/utf.hpp"
+
+namespace rive
+{
+class TextStyle;
+class TextValueRun : public TextValueRunBase
+{
+public:
+    StatusCode onAddedClean(CoreContext* context) override;
+    StatusCode onAddedDirty(CoreContext* context) override;
+    TextStyle* style() { return m_style; }
+    uint32_t length()
+    {
+        if (m_length == -1)
+        {
+
+            const uint8_t* ptr = (const uint8_t*)text().c_str();
+            uint32_t n = 0;
+            while (*ptr)
+            {
+                UTF::NextUTF8(&ptr);
+                n += 1;
+            }
+            m_length = n;
+        }
+        return m_length;
+    }
+    uint32_t offset() const;
+
+protected:
+    void textChanged() override;
+    void styleIdChanged() override;
+
+private:
+    TextStyle* m_style = nullptr;
+    uint32_t m_length = -1;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/text/text_variation_modifier.hpp b/include/rive/text/text_variation_modifier.hpp
new file mode 100644
index 0000000..f5bab27
--- /dev/null
+++ b/include/rive/text/text_variation_modifier.hpp
@@ -0,0 +1,18 @@
+#ifndef _RIVE_TEXT_VARIATION_MODIFIER_HPP_
+#define _RIVE_TEXT_VARIATION_MODIFIER_HPP_
+#include "rive/generated/text/text_variation_modifier_base.hpp"
+
+namespace rive
+{
+class TextVariationModifier : public TextVariationModifierBase
+{
+public:
+    float modify(Font* font,
+                 std::unordered_map<uint32_t, float>& variations,
+                 float fontSize,
+                 float strength) const override;
+    void axisValueChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/text/utf.hpp b/include/rive/text/utf.hpp
new file mode 100644
index 0000000..54b8f60
--- /dev/null
+++ b/include/rive/text/utf.hpp
@@ -0,0 +1,27 @@
+#ifndef _RIVE_UTF_HPP_
+#define _RIVE_UTF_HPP_
+
+#include "rive/text_engine.hpp"
+
+namespace rive
+{
+
+class UTF
+{
+public:
+    // returns the number of bytes needed in this sequence
+    // For ascii, this will return 1
+    static int CountUTF8Length(const uint8_t utf8[]);
+
+    // Return the unichar pointed to by the utf8 pointer, and then
+    // update the pointer to point to the next sequence.
+    static Unichar NextUTF8(const uint8_t** utf8Ptr);
+
+    // Convert the unichar into (1 or 2) utf16 values, and return
+    // the number of values.
+    static int ToUTF16(Unichar uni, uint16_t utf16[]);
+};
+
+} // namespace rive
+
+#endif
diff --git a/include/rive/text_engine.hpp b/include/rive/text_engine.hpp
new file mode 100644
index 0000000..a0ccd63
--- /dev/null
+++ b/include/rive/text_engine.hpp
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_TEXT_ENGINE_HPP_
+#define _RIVE_TEXT_ENGINE_HPP_
+
+#include "rive/math/raw_path.hpp"
+#include "rive/refcnt.hpp"
+#include "rive/span.hpp"
+#include "rive/simple_array.hpp"
+
+namespace rive
+{
+
+// Representation of a single unicode codepoint.
+using Unichar = uint32_t;
+// Id for a glyph within a font.
+using GlyphID = uint16_t;
+
+struct TextRun;
+struct GlyphRun;
+
+bool isWhiteSpace(Unichar c);
+
+// Direction a paragraph or run flows in.
+enum class TextDirection : uint8_t
+{
+    ltr = 0,
+    rtl = 1
+};
+
+// The alignment of each word wrapped line in a paragraph.
+enum class TextAlign : uint8_t
+{
+    left = 0,
+    right = 1,
+    center = 2
+};
+
+// A horizontal line of text within a paragraph, after line-breaking.
+struct GlyphLine
+{
+    uint32_t startRunIndex;
+    uint32_t startGlyphIndex;
+    uint32_t endRunIndex;
+    uint32_t endGlyphIndex;
+    float startX;
+    float top = 0, baseline = 0, bottom = 0;
+
+    bool operator==(const GlyphLine& o) const
+    {
+        return startRunIndex == o.startRunIndex && startGlyphIndex == o.startGlyphIndex &&
+               endRunIndex == o.endRunIndex && endGlyphIndex == o.endGlyphIndex;
+    }
+
+    GlyphLine() :
+        startRunIndex(0), startGlyphIndex(0), endRunIndex(0), endGlyphIndex(0), startX(0.0f)
+    {}
+    GlyphLine(uint32_t run, uint32_t index) :
+        startRunIndex(run),
+        startGlyphIndex(index),
+        endRunIndex(run),
+        endGlyphIndex(index),
+        startX(0.0f)
+    {}
+
+    bool empty() const { return startRunIndex == endRunIndex && startGlyphIndex == endGlyphIndex; }
+
+    static SimpleArray<GlyphLine> BreakLines(Span<const GlyphRun> runs, float width);
+
+    // Compute values for top/baseline/bottom per line
+    static void ComputeLineSpacing(bool isFirstLine,
+                                   Span<GlyphLine>,
+                                   Span<const GlyphRun>,
+                                   float width,
+                                   TextAlign align);
+
+    static float ComputeMaxWidth(Span<GlyphLine> lines, Span<const GlyphRun> runs);
+};
+
+// A paragraph represents of set of runs that flow in a specific direction. The
+// runs are always provided in LTR and must be drawn in reverse when the
+// baseDirection is RTL. These are built by the system during shaping where the
+// user provided string and text styling is converted to shaped paragraphs.
+struct Paragraph
+{
+    SimpleArray<GlyphRun> runs;
+    TextDirection baseDirection;
+};
+
+// An abstraction for interfacing with an individual font.
+class Font : public RefCnt<Font>
+{
+public:
+    virtual ~Font() {}
+
+    struct LineMetrics
+    {
+        float ascent, descent;
+    };
+
+    const LineMetrics& lineMetrics() const { return m_lineMetrics; }
+
+    // Variable axis available for the font.
+    struct Axis
+    {
+        uint32_t tag;
+        float min;
+        float def; // default value
+        float max;
+    };
+
+    // Variable axis setting.
+    struct Coord
+    {
+        uint32_t axis;
+        float value;
+    };
+
+    // Returns the count of variable axes available for this font.
+    virtual uint16_t getAxisCount() const = 0;
+
+    // Returns the definition of the Axis at the provided index.
+    virtual Axis getAxis(uint16_t index) const = 0;
+
+    // Value for the axis, if a Coord has been provided the value from the Coord
+    // will be used. Otherwise the default value for the axis will be returned.
+    virtual float getAxisValue(uint32_t axisTag) const = 0;
+
+    // Font feature.
+    struct Feature
+    {
+        uint32_t tag;
+        uint32_t value;
+    };
+
+    // Returns the features available for this font.
+    virtual SimpleArray<uint32_t> features() const = 0;
+
+    virtual bool hasGlyph(rive::Span<const rive::Unichar>) const = 0;
+
+    // Value for the feature, if no value has been provided a (uint32_t)-1 is
+    // returned to signal that the text engine will pick the best feature value
+    // for the content.
+    virtual uint32_t getFeatureValue(uint32_t featureTag) const = 0;
+
+    rcp<Font> makeAtCoords(Span<const Coord> coords) const
+    {
+        return withOptions(coords, Span<const Feature>(nullptr, 0));
+    }
+
+    rcp<Font> makeAtCoord(Coord c) { return this->makeAtCoords(Span<const Coord>(&c, 1)); }
+
+    virtual rcp<Font> withOptions(Span<const Coord> variableAxes,
+                                  Span<const Feature> features) const = 0;
+
+    // Returns a 1-point path for this glyph. It will be positioned
+    // relative to (0,0) with the typographic baseline at y = 0.
+    //
+    virtual RawPath getPath(GlyphID) const = 0;
+
+    SimpleArray<Paragraph> shapeText(Span<const Unichar> text, Span<const TextRun> runs) const;
+
+    // If the platform can supply fallback font(s), set this function pointer.
+    // It will be called with a span of unichars, and the platform attempts to
+    // return a font that can draw (at least some of) them. If no font is available
+    // just return nullptr.
+
+    using FallbackProc = rive::rcp<rive::Font> (*)(rive::Span<const rive::Unichar>);
+    static FallbackProc gFallbackProc;
+    static bool gFallbackProcEnabled;
+
+protected:
+    Font(const LineMetrics& lm) : m_lineMetrics(lm) {}
+
+    virtual SimpleArray<Paragraph> onShapeText(Span<const Unichar> text,
+                                               Span<const TextRun> runs) const = 0;
+
+private:
+    /// The font specified line metrics (automatic line metrics).
+    const LineMetrics m_lineMetrics;
+};
+
+// A user defined styling guide for a set of unicode codepoints within a larger text string.
+struct TextRun
+{
+    rcp<Font> font;
+    float size;
+    float lineHeight;
+    float letterSpacing;
+    uint32_t unicharCount;
+    uint32_t script;
+    uint16_t styleId;
+    TextDirection dir;
+};
+
+// The corresponding system generated run for the user provided TextRuns. GlyphRuns may not match
+// TextRuns if the system needs to split the run (for fallback fonts) or if codepoints get
+// ligated/shaped to a single glyph.
+struct GlyphRun
+{
+    GlyphRun(size_t glyphCount = 0) :
+        glyphs(glyphCount),
+        textIndices(glyphCount),
+        advances(glyphCount),
+        xpos(glyphCount + 1),
+        offsets(glyphCount)
+    {}
+
+    GlyphRun(SimpleArray<GlyphID> glyphIds,
+             SimpleArray<uint32_t> offsets,
+             SimpleArray<float> ws,
+             SimpleArray<float> xs,
+             SimpleArray<rive::Vec2D> offs) :
+        glyphs(glyphIds), textIndices(offsets), advances(ws), xpos(xs), offsets(offs)
+    {}
+
+    rcp<Font> font;
+    float size;
+    float lineHeight;
+    float letterSpacing;
+
+    // List of glyphs, represented by font specific glyph ids. Length is equal to number of glyphs
+    // in the run.
+    SimpleArray<GlyphID> glyphs;
+
+    // Index in the unicode text array representing the text displayed in this run. Because each
+    // glyph can be composed of multiple unicode values, this index points to the first index in the
+    // unicode text. Length is equal to number of glyphs in the run.
+    SimpleArray<uint32_t> textIndices;
+
+    // X position of each glyph in visual order (xpos is in logical/memory order).
+    SimpleArray<float> advances;
+
+    // X position of each glyph, with an extra value at the end for the right most extent of the
+    // last glyph.
+    SimpleArray<float> xpos;
+
+    // X and Y offset each glyphs draws at relative to its baseline and advance position.
+    SimpleArray<rive::Vec2D> offsets;
+
+    // List of possible indices to line break at. Has a stride of 2 uint32_ts where each pair marks
+    // the start and end of a word, with the exception of a return character (forced linebreak)
+    // which is represented as a 0 length word (where start/end index is the same).
+    SimpleArray<uint32_t> breaks;
+
+    // The unique identifier for the styling (fill/stroke colors, anything not determined by the
+    // font or font size) applied to this run.
+    uint16_t styleId;
+
+    // The text direction (LTR = 0/RTL = 1)
+    TextDirection dir;
+};
+
+} // namespace rive
+#endif
diff --git a/include/rive/transform_component.hpp b/include/rive/transform_component.hpp
new file mode 100644
index 0000000..36580a6
--- /dev/null
+++ b/include/rive/transform_component.hpp
@@ -0,0 +1,68 @@
+#ifndef _RIVE_TRANSFORM_COMPONENT_HPP_
+#define _RIVE_TRANSFORM_COMPONENT_HPP_
+#include "rive/generated/transform_component_base.hpp"
+#include "rive/math/aabb.hpp"
+#include "rive/math/mat2d.hpp"
+#include "rive/layout/layout_measure_mode.hpp"
+
+namespace rive
+{
+class Constraint;
+class WorldTransformComponent;
+class AABB;
+class TransformComponent : public TransformComponentBase
+{
+protected:
+    Mat2D m_Transform;
+    float m_RenderOpacity = 0.0f;
+    WorldTransformComponent* m_ParentTransformComponent = nullptr;
+    std::vector<Constraint*> m_Constraints;
+
+protected:
+    void updateConstraints();
+
+public:
+    bool collapse(bool value) override;
+    const std::vector<Constraint*>& constraints() const { return m_Constraints; }
+    StatusCode onAddedClean(CoreContext* context) override;
+    void buildDependencies() override;
+    void update(ComponentDirt value) override;
+    virtual void updateTransform();
+    virtual void updateWorldTransform();
+    void markTransformDirty();
+
+    /// Opacity inherited by any child of this transform component. This'll
+    /// later get overridden by effect layers.
+    float childOpacity() override { return m_RenderOpacity; }
+    float renderOpacity() const { return m_RenderOpacity; }
+
+    const Mat2D& transform() const;
+
+    /// Explicitly dangerous. Use transform/worldTransform when you don't
+    /// need to transform things outside of their hierarchy.
+    Mat2D& mutableTransform();
+
+    virtual float x() const = 0;
+    virtual float y() const = 0;
+
+    void rotationChanged() override;
+    void scaleXChanged() override;
+    void scaleYChanged() override;
+
+    void addConstraint(Constraint* constraint);
+    virtual AABB localBounds() const;
+    void markDirtyIfConstrained();
+
+    virtual Vec2D measureLayout(float width,
+                                LayoutMeasureMode widthMode,
+                                float height,
+                                LayoutMeasureMode heightMode)
+    {
+        return Vec2D();
+    }
+
+    virtual void controlSize(Vec2D size) {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/transform_space.hpp b/include/rive/transform_space.hpp
new file mode 100644
index 0000000..3e9c3c9
--- /dev/null
+++ b/include/rive/transform_space.hpp
@@ -0,0 +1,11 @@
+#ifndef _RIVE_TRANSFORM_SPACE_HPP_
+#define _RIVE_TRANSFORM_SPACE_HPP_
+namespace rive
+{
+enum class TransformSpace : unsigned int
+{
+    world = 0,
+    local = 1
+};
+}
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/data_enum.hpp b/include/rive/viewmodel/data_enum.hpp
new file mode 100644
index 0000000..ed8addb
--- /dev/null
+++ b/include/rive/viewmodel/data_enum.hpp
@@ -0,0 +1,25 @@
+#ifndef _RIVE_DATA_ENUM_HPP_
+#define _RIVE_DATA_ENUM_HPP_
+#include "rive/generated/viewmodel/data_enum_base.hpp"
+#include "rive/viewmodel/data_enum_value.hpp"
+#include <stdio.h>
+namespace rive
+{
+class DataEnum : public DataEnumBase
+{
+private:
+    std::vector<DataEnumValue*> m_Values;
+
+public:
+    void addValue(DataEnumValue* value);
+    std::vector<DataEnumValue*> values() { return m_Values; };
+    std::string value(std::string name);
+    std::string value(uint32_t index);
+    bool value(std::string name, std::string value);
+    bool value(uint32_t index, std::string value);
+    int valueIndex(std::string name);
+    int valueIndex(uint32_t index);
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/data_enum_value.hpp b/include/rive/viewmodel/data_enum_value.hpp
new file mode 100644
index 0000000..de64216
--- /dev/null
+++ b/include/rive/viewmodel/data_enum_value.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_DATA_ENUM_VALUE_HPP_
+#define _RIVE_DATA_ENUM_VALUE_HPP_
+#include "rive/generated/viewmodel/data_enum_value_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class DataEnumValue : public DataEnumValueBase
+{
+public:
+    StatusCode import(ImportStack& importStack) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/viewmodel.hpp b/include/rive/viewmodel/viewmodel.hpp
new file mode 100644
index 0000000..2dbc600
--- /dev/null
+++ b/include/rive/viewmodel/viewmodel.hpp
@@ -0,0 +1,26 @@
+#ifndef _RIVE_VIEW_MODEL_HPP_
+#define _RIVE_VIEW_MODEL_HPP_
+#include "rive/generated/viewmodel/viewmodel_base.hpp"
+#include "rive/viewmodel/viewmodel_property.hpp"
+#include "rive/viewmodel/viewmodel_instance.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ViewModel : public ViewModelBase
+{
+private:
+    std::vector<ViewModelProperty*> m_Properties;
+    std::vector<ViewModelInstance*> m_Instances;
+
+public:
+    void addProperty(ViewModelProperty* property);
+    ViewModelProperty* property(const std::string& name);
+    ViewModelProperty* property(size_t index);
+    void addInstance(ViewModelInstance* value);
+    ViewModelInstance* instance(size_t index);
+    ViewModelInstance* instance(const std::string& name);
+    ViewModelInstance* defaultInstance();
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/viewmodel_component.hpp b/include/rive/viewmodel/viewmodel_component.hpp
new file mode 100644
index 0000000..28354b4
--- /dev/null
+++ b/include/rive/viewmodel/viewmodel_component.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_VIEW_MODEL_COMPONENT_HPP_
+#define _RIVE_VIEW_MODEL_COMPONENT_HPP_
+#include "rive/generated/viewmodel/viewmodel_component_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ViewModelComponent : public ViewModelComponentBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/viewmodel_instance.hpp b/include/rive/viewmodel/viewmodel_instance.hpp
new file mode 100644
index 0000000..3a6375a
--- /dev/null
+++ b/include/rive/viewmodel/viewmodel_instance.hpp
@@ -0,0 +1,32 @@
+#ifndef _RIVE_VIEW_MODEL_INSTANCE_HPP_
+#define _RIVE_VIEW_MODEL_INSTANCE_HPP_
+#include "rive/generated/viewmodel/viewmodel_instance_base.hpp"
+#include "rive/viewmodel/viewmodel_instance_value.hpp"
+#include "rive/component.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ViewModel;
+class ViewModelInstance : public ViewModelInstanceBase
+{
+private:
+    std::vector<ViewModelInstanceValue*> m_PropertyValues;
+    ViewModel* m_ViewModel;
+
+public:
+    void addValue(ViewModelInstanceValue* value);
+    ViewModelInstanceValue* propertyValue(const uint32_t id);
+    ViewModelInstanceValue* propertyValue(const std::string& name);
+    std::vector<ViewModelInstanceValue*> propertyValues();
+    ViewModelInstanceValue* propertyFromPath(std::vector<uint32_t>* path, size_t index);
+    void viewModel(ViewModel* value);
+    ViewModel* viewModel() const;
+    void onComponentDirty(Component* component);
+    void setAsRoot();
+    void setRoot(ViewModelInstance* value);
+    Core* clone() const override;
+    StatusCode import(ImportStack& importStack) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/viewmodel_instance_boolean.hpp b/include/rive/viewmodel/viewmodel_instance_boolean.hpp
new file mode 100644
index 0000000..c00c159
--- /dev/null
+++ b/include/rive/viewmodel/viewmodel_instance_boolean.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_VIEW_MODEL_INSTANCE_BOOLEAN_HPP_
+#define _RIVE_VIEW_MODEL_INSTANCE_BOOLEAN_HPP_
+#include "rive/generated/viewmodel/viewmodel_instance_boolean_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ViewModelInstanceBoolean : public ViewModelInstanceBooleanBase
+{
+protected:
+    void propertyValueChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/viewmodel_instance_color.hpp b/include/rive/viewmodel/viewmodel_instance_color.hpp
new file mode 100644
index 0000000..833ddbb
--- /dev/null
+++ b/include/rive/viewmodel/viewmodel_instance_color.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_VIEW_MODEL_INSTANCE_COLOR_HPP_
+#define _RIVE_VIEW_MODEL_INSTANCE_COLOR_HPP_
+#include "rive/generated/viewmodel/viewmodel_instance_color_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ViewModelInstanceColor : public ViewModelInstanceColorBase
+{
+public:
+    void propertyValueChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/viewmodel_instance_enum.hpp b/include/rive/viewmodel/viewmodel_instance_enum.hpp
new file mode 100644
index 0000000..6980ae4
--- /dev/null
+++ b/include/rive/viewmodel/viewmodel_instance_enum.hpp
@@ -0,0 +1,18 @@
+#ifndef _RIVE_VIEW_MODEL_INSTANCE_ENUM_HPP_
+#define _RIVE_VIEW_MODEL_INSTANCE_ENUM_HPP_
+#include "rive/generated/viewmodel/viewmodel_instance_enum_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ViewModelInstanceEnum : public ViewModelInstanceEnumBase
+{
+public:
+    bool value(std::string name);
+    bool value(uint32_t index);
+
+protected:
+    void propertyValueChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/viewmodel_instance_list.hpp b/include/rive/viewmodel/viewmodel_instance_list.hpp
new file mode 100644
index 0000000..8f1b934
--- /dev/null
+++ b/include/rive/viewmodel/viewmodel_instance_list.hpp
@@ -0,0 +1,26 @@
+#ifndef _RIVE_VIEW_MODEL_INSTANCE_LIST_HPP_
+#define _RIVE_VIEW_MODEL_INSTANCE_LIST_HPP_
+#include "rive/generated/viewmodel/viewmodel_instance_list_base.hpp"
+#include "rive/viewmodel/viewmodel_instance_list_item.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ViewModelInstanceList : public ViewModelInstanceListBase
+{
+public:
+    void addItem(ViewModelInstanceListItem* listItem);
+    void insertItem(int index, ViewModelInstanceListItem* listItem);
+    void removeItem(int index);
+    void removeItem(ViewModelInstanceListItem* listItem);
+    std::vector<ViewModelInstanceListItem*> listItems() { return m_ListItems; };
+    ViewModelInstanceListItem* item(uint32_t index);
+    void swap(uint32_t index1, uint32_t index2);
+    Core* clone() const override;
+
+protected:
+    std::vector<ViewModelInstanceListItem*> m_ListItems;
+    void propertyValueChanged();
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/viewmodel_instance_list_item.hpp b/include/rive/viewmodel/viewmodel_instance_list_item.hpp
new file mode 100644
index 0000000..d09ab72
--- /dev/null
+++ b/include/rive/viewmodel/viewmodel_instance_list_item.hpp
@@ -0,0 +1,23 @@
+#ifndef _RIVE_VIEW_MODEL_INSTANCE_LIST_ITEM_HPP_
+#define _RIVE_VIEW_MODEL_INSTANCE_LIST_ITEM_HPP_
+#include "rive/generated/viewmodel/viewmodel_instance_list_item_base.hpp"
+#include "rive/viewmodel/viewmodel_instance.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ViewModelInstanceListItem : public ViewModelInstanceListItemBase
+{
+private:
+    ViewModelInstance* m_viewModelInstance;
+    Artboard* m_artboard;
+
+public:
+    void viewModelInstance(ViewModelInstance* value) { m_viewModelInstance = value; };
+    ViewModelInstance* viewModelInstance() { return m_viewModelInstance; }
+    void artboard(Artboard* value) { m_artboard = value; };
+    Artboard* artboard() { return m_artboard; }
+    StatusCode import(ImportStack& importStack) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/viewmodel_instance_number.hpp b/include/rive/viewmodel/viewmodel_instance_number.hpp
new file mode 100644
index 0000000..a5f40e9
--- /dev/null
+++ b/include/rive/viewmodel/viewmodel_instance_number.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_VIEW_MODEL_INSTANCE_NUMBER_HPP_
+#define _RIVE_VIEW_MODEL_INSTANCE_NUMBER_HPP_
+#include "rive/generated/viewmodel/viewmodel_instance_number_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ViewModelInstanceNumber : public ViewModelInstanceNumberBase
+{
+protected:
+    void propertyValueChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/viewmodel_instance_string.hpp b/include/rive/viewmodel/viewmodel_instance_string.hpp
new file mode 100644
index 0000000..a7234e8
--- /dev/null
+++ b/include/rive/viewmodel/viewmodel_instance_string.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_VIEW_MODEL_INSTANCE_STRING_HPP_
+#define _RIVE_VIEW_MODEL_INSTANCE_STRING_HPP_
+#include "rive/generated/viewmodel/viewmodel_instance_string_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ViewModelInstanceString : public ViewModelInstanceStringBase
+{
+public:
+    void propertyValueChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/viewmodel_instance_value.hpp b/include/rive/viewmodel/viewmodel_instance_value.hpp
new file mode 100644
index 0000000..484aa8e
--- /dev/null
+++ b/include/rive/viewmodel/viewmodel_instance_value.hpp
@@ -0,0 +1,31 @@
+#ifndef _RIVE_VIEW_MODEL_INSTANCE_VALUE_HPP_
+#define _RIVE_VIEW_MODEL_INSTANCE_VALUE_HPP_
+#include "rive/generated/viewmodel/viewmodel_instance_value_base.hpp"
+#include "rive/viewmodel/viewmodel_property.hpp"
+#include "rive/dependency_helper.hpp"
+#include "rive/component.hpp"
+#include "rive/component_dirt.hpp"
+#include <stdio.h>
+namespace rive
+{
+class DataBind;
+class ViewModelInstance;
+class ViewModelInstanceValue : public ViewModelInstanceValueBase
+{
+private:
+    ViewModelProperty* m_ViewModelProperty;
+
+protected:
+    DependencyHelper<ViewModelInstance, DataBind> m_DependencyHelper;
+    void addDirt(ComponentDirt value);
+
+public:
+    StatusCode import(ImportStack& importStack) override;
+    void viewModelProperty(ViewModelProperty* value);
+    ViewModelProperty* viewModelProperty();
+    void addDependent(DataBind* value);
+    virtual void setRoot(ViewModelInstance* value);
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/viewmodel_instance_viewmodel.hpp b/include/rive/viewmodel/viewmodel_instance_viewmodel.hpp
new file mode 100644
index 0000000..d2ea9ab
--- /dev/null
+++ b/include/rive/viewmodel/viewmodel_instance_viewmodel.hpp
@@ -0,0 +1,23 @@
+#ifndef _RIVE_VIEW_MODEL_INSTANCE_VIEW_MODEL_HPP_
+#define _RIVE_VIEW_MODEL_INSTANCE_VIEW_MODEL_HPP_
+#include "rive/generated/viewmodel/viewmodel_instance_viewmodel_base.hpp"
+#include "rive/viewmodel/viewmodel_instance.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ViewModelInstanceViewModel : public ViewModelInstanceViewModelBase
+{
+private:
+    ViewModelInstance* m_referenceViewModelInstance;
+
+public:
+    void referenceViewModelInstance(ViewModelInstance* value)
+    {
+        m_referenceViewModelInstance = value;
+    };
+    ViewModelInstance* referenceViewModelInstance() { return m_referenceViewModelInstance; }
+    void setRoot(ViewModelInstance* value) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/viewmodel_property.hpp b/include/rive/viewmodel/viewmodel_property.hpp
new file mode 100644
index 0000000..87b3a25
--- /dev/null
+++ b/include/rive/viewmodel/viewmodel_property.hpp
@@ -0,0 +1,14 @@
+#ifndef _RIVE_VIEW_MODEL_PROPERTY_HPP_
+#define _RIVE_VIEW_MODEL_PROPERTY_HPP_
+#include "rive/generated/viewmodel/viewmodel_property_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ViewModelProperty : public ViewModelPropertyBase
+{
+public:
+    StatusCode import(ImportStack& importStack) override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/viewmodel_property_boolean.hpp b/include/rive/viewmodel/viewmodel_property_boolean.hpp
new file mode 100644
index 0000000..c86fb61
--- /dev/null
+++ b/include/rive/viewmodel/viewmodel_property_boolean.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_VIEW_MODEL_PROPERTY_BOOLEAN_HPP_
+#define _RIVE_VIEW_MODEL_PROPERTY_BOOLEAN_HPP_
+#include "rive/generated/viewmodel/viewmodel_property_boolean_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ViewModelPropertyBoolean : public ViewModelPropertyBooleanBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/viewmodel_property_color.hpp b/include/rive/viewmodel/viewmodel_property_color.hpp
new file mode 100644
index 0000000..ffc1191
--- /dev/null
+++ b/include/rive/viewmodel/viewmodel_property_color.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_VIEW_MODEL_PROPERTY_COLOR_HPP_
+#define _RIVE_VIEW_MODEL_PROPERTY_COLOR_HPP_
+#include "rive/generated/viewmodel/viewmodel_property_color_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ViewModelPropertyColor : public ViewModelPropertyColorBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/viewmodel_property_enum.hpp b/include/rive/viewmodel/viewmodel_property_enum.hpp
new file mode 100644
index 0000000..41582e4
--- /dev/null
+++ b/include/rive/viewmodel/viewmodel_property_enum.hpp
@@ -0,0 +1,27 @@
+#ifndef _RIVE_VIEW_MODEL_PROPERTY_ENUM_HPP_
+#define _RIVE_VIEW_MODEL_PROPERTY_ENUM_HPP_
+#include "rive/generated/viewmodel/viewmodel_property_enum_base.hpp"
+#include "rive/viewmodel/data_enum.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ViewModelPropertyEnum : public ViewModelPropertyEnumBase
+{
+
+public:
+    std::string value(std::string name);
+    std::string value(uint32_t index);
+    bool value(std::string name, std::string value);
+    bool value(uint32_t index, std::string value);
+    int valueIndex(std::string name);
+    int valueIndex(uint32_t index);
+    void dataEnum(DataEnum* value);
+    DataEnum* dataEnum();
+
+private:
+    DataEnum* m_DataEnum;
+};
+
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/viewmodel_property_list.hpp b/include/rive/viewmodel/viewmodel_property_list.hpp
new file mode 100644
index 0000000..a4fa054
--- /dev/null
+++ b/include/rive/viewmodel/viewmodel_property_list.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_VIEW_MODEL_PROPERTY_LIST_HPP_
+#define _RIVE_VIEW_MODEL_PROPERTY_LIST_HPP_
+#include "rive/generated/viewmodel/viewmodel_property_list_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ViewModelPropertyList : public ViewModelPropertyListBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/viewmodel_property_number.hpp b/include/rive/viewmodel/viewmodel_property_number.hpp
new file mode 100644
index 0000000..0ba2468
--- /dev/null
+++ b/include/rive/viewmodel/viewmodel_property_number.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_VIEW_MODEL_PROPERTY_NUMBER_HPP_
+#define _RIVE_VIEW_MODEL_PROPERTY_NUMBER_HPP_
+#include "rive/generated/viewmodel/viewmodel_property_number_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ViewModelPropertyNumber : public ViewModelPropertyNumberBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/viewmodel_property_string.hpp b/include/rive/viewmodel/viewmodel_property_string.hpp
new file mode 100644
index 0000000..0ff3745
--- /dev/null
+++ b/include/rive/viewmodel/viewmodel_property_string.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_VIEW_MODEL_PROPERTY_STRING_HPP_
+#define _RIVE_VIEW_MODEL_PROPERTY_STRING_HPP_
+#include "rive/generated/viewmodel/viewmodel_property_string_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ViewModelPropertyString : public ViewModelPropertyStringBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/viewmodel/viewmodel_property_viewmodel.hpp b/include/rive/viewmodel/viewmodel_property_viewmodel.hpp
new file mode 100644
index 0000000..32ae213
--- /dev/null
+++ b/include/rive/viewmodel/viewmodel_property_viewmodel.hpp
@@ -0,0 +1,13 @@
+#ifndef _RIVE_VIEW_MODEL_PROPERTY_VIEW_MODEL_HPP_
+#define _RIVE_VIEW_MODEL_PROPERTY_VIEW_MODEL_HPP_
+#include "rive/generated/viewmodel/viewmodel_property_viewmodel_base.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ViewModelPropertyViewModel : public ViewModelPropertyViewModelBase
+{
+public:
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/world_transform_component.hpp b/include/rive/world_transform_component.hpp
new file mode 100644
index 0000000..f5c97b2
--- /dev/null
+++ b/include/rive/world_transform_component.hpp
@@ -0,0 +1,26 @@
+#ifndef _RIVE_WORLD_TRANSFORM_COMPONENT_HPP_
+#define _RIVE_WORLD_TRANSFORM_COMPONENT_HPP_
+#include "rive/generated/world_transform_component_base.hpp"
+#include "rive/math/mat2d.hpp"
+
+namespace rive
+{
+class TransformComponent;
+class WorldTransformComponent : public WorldTransformComponentBase
+{
+    friend class TransformComponent;
+
+protected:
+    Mat2D m_WorldTransform;
+
+public:
+    void markWorldTransformDirty();
+    virtual float childOpacity();
+    Mat2D& mutableWorldTransform();
+    const Mat2D& worldTransform() const;
+    Vec2D worldTranslation() const { return m_WorldTransform.translation(); }
+    void opacityChanged() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/utils/auto_cf.hpp b/include/utils/auto_cf.hpp
new file mode 100644
index 0000000..5512a0b
--- /dev/null
+++ b/include/utils/auto_cf.hpp
@@ -0,0 +1,74 @@
+#pragma once
+
+#include "rive/rive_types.hpp"
+
+#ifdef RIVE_BUILD_FOR_APPLE
+
+#if defined(RIVE_BUILD_FOR_OSX)
+#include <ApplicationServices/ApplicationServices.h>
+#elif defined(RIVE_BUILD_FOR_IOS)
+#include <CoreFoundation/CoreFoundation.h>
+#endif
+
+template <typename T> class AutoCF
+{
+    T m_obj;
+
+public:
+    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(AutoCF&& other)
+    {
+        m_obj = other.m_obj;
+        other.m_obj = nullptr;
+    }
+    ~AutoCF()
+    {
+        if (m_obj)
+        {
+            CFRelease(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; }
+};
+
+#endif // RIVE_BUILD_FOR_APPLE
diff --git a/include/utils/factory_utils.hpp b/include/utils/factory_utils.hpp
new file mode 100644
index 0000000..27ee8b0
--- /dev/null
+++ b/include/utils/factory_utils.hpp
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_FACTORY_UTILS_HPP_
+#define _RIVE_FACTORY_UTILS_HPP_
+
+#include "rive/factory.hpp"
+
+namespace rive
+{
+
+// Generic subclass of RenderBuffer that just stores the data on the cpu.
+//
+class DataRenderBuffer : public lite_rtti_override<RenderBuffer, DataRenderBuffer>
+{
+public:
+    DataRenderBuffer(RenderBufferType type, RenderBufferFlags flags, size_t sizeInBytes) :
+        lite_rtti_override(type, flags, sizeInBytes)
+    {
+        m_storage = malloc(sizeInBytes);
+    }
+
+    ~DataRenderBuffer() { free(m_storage); }
+
+    const float* f32s() const { return reinterpret_cast<const float*>(m_storage); }
+
+    const uint16_t* u16s() const { return reinterpret_cast<const uint16_t*>(m_storage); }
+
+    const Vec2D* vecs() const { return reinterpret_cast<const Vec2D*>(f32s()); }
+
+protected:
+    void* onMap() override { return m_storage; }
+    void onUnmap() override {}
+
+private:
+    void* m_storage;
+};
+
+} // namespace rive
+
+#endif
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/include/utils/no_op_factory.hpp b/include/utils/no_op_factory.hpp
new file mode 100644
index 0000000..59186e5
--- /dev/null
+++ b/include/utils/no_op_factory.hpp
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_NOOP_FACTORY_HPP_
+#define _RIVE_NOOP_FACTORY_HPP_
+
+#include "rive/factory.hpp"
+
+namespace rive
+{
+
+class NoOpFactory : public Factory
+{
+    rcp<RenderBuffer> makeRenderBuffer(RenderBufferType, RenderBufferFlags, size_t) override;
+
+    rcp<RenderShader> makeLinearGradient(float sx,
+                                         float sy,
+                                         float ex,
+                                         float ey,
+                                         const ColorInt colors[], // [count]
+                                         const float stops[],     // [count]
+                                         size_t count) override;
+
+    rcp<RenderShader> makeRadialGradient(float cx,
+                                         float cy,
+                                         float radius,
+                                         const ColorInt colors[], // [count]
+                                         const float stops[],     // [count]
+                                         size_t count) override;
+
+    rcp<RenderPath> makeRenderPath(RawPath&, FillRule) override;
+
+    rcp<RenderPath> makeEmptyRenderPath() override;
+
+    rcp<RenderPaint> makeRenderPaint() override;
+
+    rcp<RenderImage> decodeImage(Span<const uint8_t>) override;
+};
+} // namespace rive
+#endif
diff --git a/include/utils/no_op_renderer.hpp b/include/utils/no_op_renderer.hpp
new file mode 100644
index 0000000..5cedb34
--- /dev/null
+++ b/include/utils/no_op_renderer.hpp
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_NOOP_RENDERER_HPP_
+#define _RIVE_NOOP_RENDERER_HPP_
+
+#include "rive/renderer.hpp"
+
+namespace rive
+{
+
+class NoOpRenderer : public Renderer
+{
+public:
+    void save() override {}
+    void restore() override {}
+    void transform(const Mat2D&) override {}
+    void drawPath(RenderPath* path, RenderPaint* paint) override {}
+    void clipPath(RenderPath* path) override {}
+    void drawImage(const RenderImage*, BlendMode, float) override {}
+    void drawImageMesh(const RenderImage*,
+                       rcp<RenderBuffer>,
+                       rcp<RenderBuffer>,
+                       rcp<RenderBuffer>,
+                       uint32_t vertexCount,
+                       uint32_t indexCount,
+                       BlendMode,
+                       float) override
+    {}
+};
+
+} // namespace rive
+
+#endif
diff --git a/premake5_v2.lua b/premake5_v2.lua
new file mode 100644
index 0000000..35acb87
--- /dev/null
+++ b/premake5_v2.lua
@@ -0,0 +1,193 @@
+dofile('rive_build_config.lua')
+
+filter({ 'options:with_rive_tools' })
+do
+    defines({ 'WITH_RIVE_TOOLS' })
+end
+filter({ 'options:with_rive_text' })
+do
+    defines({ 'WITH_RIVE_TEXT' })
+end
+filter({ 'options:with_rive_audio=system' })
+do
+    defines({ 'WITH_RIVE_AUDIO', 'MA_NO_RESOURCE_MANAGER' })
+end
+
+filter({ 'options:with_rive_audio=external' })
+do
+    defines({
+        'WITH_RIVE_AUDIO',
+        'EXTERNAL_RIVE_AUDIO_ENGINE',
+        'MA_NO_DEVICE_IO',
+        'MA_NO_RESOURCE_MANAGER',
+    })
+end
+filter({ 'options:with_rive_layout' })
+do
+    defines({ 'WITH_RIVE_LAYOUT' })
+end
+filter({})
+
+dependencies = path.getabsolute('dependencies/')
+dofile(path.join(dependencies, 'premake5_harfbuzz_v2.lua'))
+dofile(path.join(dependencies, 'premake5_sheenbidi_v2.lua'))
+dofile(path.join(dependencies, 'premake5_miniaudio_v2.lua'))
+dofile(path.join(dependencies, 'premake5_yoga_v2.lua'))
+
+project('rive')
+do
+    kind('StaticLib')
+    cppdialect('C++11')
+    includedirs({
+        'include',
+        harfbuzz .. '/src',
+        sheenbidi .. '/Headers',
+        miniaudio,
+        yoga,
+    })
+
+    filter('action:xcode4')
+    do
+        -- xcode doesnt like angle brackets except for -isystem
+        -- should use externalincludedirs but GitHub runners dont have latest premake5 binaries
+        buildoptions({ '-isystem' .. yoga })
+    end
+    filter({})
+
+    defines({ 'YOGA_EXPORT=' })
+
+    files({ 'src/**.cpp' })
+
+    flags({ 'FatalCompileWarnings' })
+
+    filter({ 'options:with_rive_text', 'options:not no-harfbuzz-renames' })
+    do
+        includedirs({
+            dependencies,
+        })
+        forceincludes({ 'rive_harfbuzz_renames.h' })
+    end
+
+    filter({ 'options:not no-yoga-renames' })
+    do
+        includedirs({
+            dependencies,
+        })
+        forceincludes({ 'rive_yoga_renames.h' })
+    end
+
+    filter({ 'system:linux' })
+    do
+        defines({ 'MA_NO_RUNTIME_LINKING' })
+    end
+
+    filter({ 'system:macosx' })
+    do
+        buildoptions({
+            -- this triggers too much on linux, so just enable here for now
+            '-Wimplicit-float-conversion',
+        })
+    end
+
+    -- filter {'toolset:not msc', 'files:src/audio/audio_engine.cpp'}
+    filter({ 'system:not windows', 'files:src/audio/audio_engine.cpp' })
+    do
+        buildoptions({ '-Wno-implicit-int-conversion' })
+    end
+
+    filter({ 'system:windows', 'files:src/audio/audio_engine.cpp' })
+    do
+        -- Too many warnings from miniaudio.h
+        removeflags({ 'FatalCompileWarnings' })
+    end
+
+    filter({ 'system:windows', 'toolset:clang', 'files:src/audio/audio_engine.cpp' })
+    do
+        buildoptions({
+            '-Wno-nonportable-system-include-path',
+            '-Wno-zero-as-null-pointer-constant',
+            '-Wno-missing-prototypes',
+            '-Wno-cast-qual',
+            '-Wno-format-nonliteral',
+            '-Wno-cast-align',
+            '-Wno-covered-switch-default',
+            '-Wno-comma',
+            '-Wno-tautological-type-limit-compare',
+            '-Wno-extra-semi-stmt',
+            '-Wno-tautological-constant-out-of-range-compare',
+            '-Wno-implicit-fallthrough',
+            '-Wno-implicit-int-conversion',
+            '-Wno-undef',
+            '-Wno-unused-function',
+        })
+    end
+
+    -- filter 'files:src/audio/audio_engine.cpp'
+    -- do
+    --     buildoptions {
+    --         '-Wno-implicit-int-conversion'
+    --     }
+    -- end
+
+    filter({ 'system:macosx', 'options:variant=runtime' })
+    do
+        buildoptions({
+            '-Wimplicit-float-conversion -fembed-bitcode -arch arm64 -arch x86_64 -isysroot '
+                .. (os.getenv('MACOS_SYSROOT') or ''),
+        })
+    end
+
+    filter({ 'system:ios' })
+    do
+        buildoptions({ '-flto=full', '-Wno-implicit-int-conversion' })
+        files({ 'src/audio/audio_engine.m' })
+    end
+
+    filter('system:windows')
+    do
+        architecture('x64')
+        defines({ '_USE_MATH_DEFINES' })
+    end
+
+    filter('system:macosx or system:ios')
+    do
+        files({ 'src/text/font_hb_apple.mm' })
+    end
+end
+
+newoption({
+    trigger = 'variant',
+    value = 'type',
+    description = 'Choose a particular variant to build',
+    allowed = {
+        { 'system', 'Builds the static library for the provided system' },
+        { 'emulator', 'Builds for an emulator/simulator for the provided system' },
+        {
+            'runtime',
+            'Build the static library specifically targeting our runtimes',
+        },
+    },
+    default = 'system',
+})
+
+newoption({
+    trigger = 'with_rive_tools',
+    description = 'Enables tools usually not necessary for runtime.',
+})
+
+newoption({
+    trigger = 'with_rive_text',
+    description = 'Compiles in text features.',
+})
+
+newoption({
+    trigger = 'with_rive_audio',
+    value = 'disabled',
+    description = 'The audio mode to use.',
+    allowed = { { 'disabled' }, { 'system' }, { 'external' } },
+})
+
+newoption({
+    trigger = 'with_rive_layout',
+    description = 'Compiles in layout features.',
+})
diff --git a/rivinfo/build.sh b/rivinfo/build.sh
new file mode 100755
index 0000000..e0d0eb5
--- /dev/null
+++ b/rivinfo/build.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+# dir=$(pwd)
+
+# cd ../renderer
+# ./build.sh $@
+
+# cd $dir
+
+cd build
+
+OPTION=$1
+
+if [ "$OPTION" = 'help' ]; then
+    echo build.sh - build debug library
+    echo build.sh clean - clean the build
+    echo build.sh release - build release library
+elif [ "$OPTION" = "clean" ]; then
+    echo Cleaning project ...
+    # TODO: fix premake5 clean to bubble the clean command to dependent projects
+    premake5 gmake && make clean
+elif [ "$OPTION" = "release" ]; then
+    premake5 gmake && make config=release -j7
+else
+    premake5 gmake && make -j7
+fi
diff --git a/rivinfo/build/premake5.lua b/rivinfo/build/premake5.lua
new file mode 100644
index 0000000..dba3e5f
--- /dev/null
+++ b/rivinfo/build/premake5.lua
@@ -0,0 +1,65 @@
+workspace('rive')
+configurations({ 'debug', 'release' })
+
+project('rivinfo')
+kind('ConsoleApp')
+language('C++')
+cppdialect('C++17')
+targetdir('%{cfg.system}/bin/%{cfg.buildcfg}')
+objdir('%{cfg.system}/obj/%{cfg.buildcfg}')
+includedirs({
+    '../../include',
+    '../../test',
+    '/usr/local/include',
+    '/usr/include',
+})
+
+if os.host() == 'macosx' then
+    links({
+        'Cocoa.framework',
+        'CoreFoundation.framework',
+        'IOKit.framework',
+        'Security.framework',
+        'bz2',
+        'iconv',
+        'lzma',
+        'rive',
+        'z', -- lib av format
+    })
+else
+    links({ 'm', 'rive', 'z', 'dl' })
+end
+
+libdirs({
+    '../../build/%{cfg.system}/bin/%{cfg.buildcfg}',
+    '/usr/local/lib',
+    '/usr/lib',
+})
+
+files({ '../**.cpp', '../../utils/no_op_factory.cpp' })
+
+buildoptions({ '-Wall', '-fno-rtti', '-g' })
+
+filter('configurations:debug')
+defines({ 'DEBUG' })
+symbols('On')
+
+filter('configurations:release')
+defines({ 'RELEASE' })
+defines({ 'NDEBUG' })
+optimize('On')
+
+-- Clean Function --
+newaction({
+    trigger = 'clean',
+    description = 'clean the build',
+    execute = function()
+        print('clean the build...')
+        os.rmdir('./bin')
+        os.rmdir('./obj')
+        os.remove('Makefile')
+        -- no wildcards in os.remove, so use shell
+        os.execute('rm *.make')
+        print('build cleaned')
+    end,
+})
diff --git a/rivinfo/main.cpp b/rivinfo/main.cpp
new file mode 100644
index 0000000..681bb7d
--- /dev/null
+++ b/rivinfo/main.cpp
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "rive/artboard.hpp"
+#include "rive/file.hpp"
+#include "rive/animation/linear_animation_instance.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+#include "utils/no_op_factory.hpp"
+
+class JSoner
+{
+    std::vector<bool> m_IsArray;
+
+    void tab()
+    {
+        for (int i = 0; i < m_IsArray.size(); ++i)
+        {
+            printf("\t");
+        }
+    }
+    void add_c(const char key[], char c)
+    {
+        this->tab();
+        if (key)
+        {
+            printf("\"%s\": %c\n", key, c);
+        }
+        else
+        {
+            printf("%c\n", c);
+        }
+    }
+
+public:
+    JSoner() {}
+    ~JSoner()
+    {
+        while (!m_IsArray.empty())
+        {
+            this->pop();
+        }
+    }
+
+    void add(const char key[], const char value[])
+    {
+        this->tab();
+        printf("\"%s\": \"%s\"\n", key, value);
+    }
+    void pushArray(const char key[] = nullptr)
+    {
+        this->add_c(key, '[');
+        m_IsArray.push_back(true);
+    }
+    void pushStruct(const char key[] = nullptr)
+    {
+        this->add_c(key, '{');
+        m_IsArray.push_back(false);
+    }
+    void pop()
+    {
+        assert(!m_IsArray.empty());
+        char c = m_IsArray.front() ? ']' : '}';
+        m_IsArray.pop_back();
+
+        this->tab();
+        printf("%c\n", c);
+    }
+
+    void add(const char key[], int value) { this->add(key, std::to_string(value).c_str()); }
+};
+
+//////////////////////////////////////////////////
+
+static void dump(JSoner& js, rive::LinearAnimationInstance* anim)
+{
+    js.pushStruct();
+    js.add("name", anim->name().c_str());
+    js.add("duration", std::to_string(anim->durationSeconds()).c_str());
+    js.add("loop", std::to_string(anim->loopValue()).c_str());
+    js.pop();
+}
+
+static void dump(JSoner& js, rive::StateMachineInstance* smi)
+{
+    js.pushStruct();
+    js.add("name", smi->name().c_str());
+    if (auto count = smi->inputCount())
+    {
+        js.pushArray("inputs");
+        for (auto i = 0; i < count; ++i)
+        {
+            auto inp = smi->input(i);
+            js.add("name", inp->name().c_str());
+        }
+        js.pop();
+    }
+    js.pop();
+}
+
+static void dump(JSoner& js, rive::ArtboardInstance* abi)
+{
+    js.pushStruct();
+    js.add("name", abi->name().c_str());
+    if (auto count = abi->animationCount())
+    {
+        js.pushArray("animations");
+        for (size_t i = 0; i < count; ++i)
+        {
+            dump(js, abi->animationAt(i).get());
+        }
+        js.pop();
+    }
+    if (auto count = abi->stateMachineCount())
+    {
+        js.pushArray("machines");
+        for (size_t i = 0; i < count; ++i)
+        {
+            dump(js, abi->stateMachineAt(i).get());
+        }
+        js.pop();
+    }
+    js.pop();
+}
+
+static void dump(JSoner& js, rive::File* file)
+{
+    auto count = file->artboardCount();
+    js.pushArray("artboards");
+    for (size_t i = 0; i < count; ++i)
+    {
+        dump(js, file->artboardAt(i).get());
+    }
+    js.pop();
+}
+
+static std::unique_ptr<rive::File> open_file(const char name[])
+{
+    FILE* f = fopen(name, "rb");
+    if (!f)
+    {
+        return nullptr;
+    }
+
+    fseek(f, 0, SEEK_END);
+    auto length = ftell(f);
+    fseek(f, 0, SEEK_SET);
+
+    std::vector<uint8_t> bytes(length);
+
+    if (fread(bytes.data(), 1, length, f) != length)
+    {
+        printf("Failed to read file into bytes array\n");
+        return nullptr;
+    }
+
+    static rive::NoOpFactory gFactory;
+    return rive::File::import(bytes, &gFactory);
+}
+
+static bool is_arg(const char arg[], const char target[], const char alt[] = nullptr)
+{
+    return !strcmp(arg, target) || (arg && !strcmp(arg, alt));
+}
+
+int main(int argc, const char* argv[])
+{
+    const char* filename = nullptr;
+
+    for (int i = 1; i < argc; ++i)
+    {
+        if (is_arg(argv[i], "--file", "-f"))
+        {
+            filename = argv[++i];
+            continue;
+        }
+        printf("Unrecognized argument %s\n", argv[i]);
+        return 1;
+    }
+
+    if (!filename)
+    {
+        printf("Need --file filename\n");
+        return 1;
+    }
+
+    auto file = open_file(filename);
+    if (!file)
+    {
+        printf("Can't open %s\n", filename);
+        return 1;
+    }
+
+    JSoner js;
+    js.pushStruct();
+    dump(js, file.get());
+    return 0;
+}
diff --git a/skia/dependencies/cache_helper.sh b/skia/dependencies/cache_helper.sh
new file mode 100755
index 0000000..f7e7026
--- /dev/null
+++ b/skia/dependencies/cache_helper.sh
@@ -0,0 +1,98 @@
+#!/bin/bash
+
+# make sure we stop if we encounter an error
+set -e
+
+# required envs
+RIVE_RUNTIME_DIR="${RIVE_RUNTIME_DIR:=../..}"
+SKIA_DIR_NAME="${SKIA_DIR_NAME:=skia}"
+SKIA_REPO=${SKIA_REPO:-https://github.com/rive-app/skia}
+SKIA_BRANCH=${SKIA_BRANCH:-rive}
+SKIA_COMMIT=${SKIA_COMMIT}
+COMPILE_TARGET="${COMPILE_TARGET:-$(uname -s)_$(uname -m)}"
+CACHE_NAME="${CACHE_NAME:=skia}"
+OUTPUT_CACHE="${OUTPUT_CACHE:=out}"
+ARCHIVE_CONTENTS_NAME="${ARCHIVE_CONTENTS_NAME:=archive_contents}"
+
+# lets just make sure this exists, or fail
+if [[ ! -d $RIVE_RUNTIME_DIR ]]; then
+    echo "Cannot find $RIVE_RUNTIME_DIR, bad setup"
+    exit 1
+fi
+
+# computed environment variables
+SKIA_DEPENDENCIES_DIR="$RIVE_RUNTIME_DIR/skia/dependencies"
+SKIA_DIR="$SKIA_DEPENDENCIES_DIR/$SKIA_DIR_NAME"
+
+# gotta switch into a non .git folder to check the remote repo's hash
+# this avoid issues with corrupted git repos throwing irrelevant errors
+pushd ~
+SKIA_COMMIT_HASH="$(git ls-remote $SKIA_REPO $SKIA_BRANCH | awk '{print $1}')"
+popd
+
+ARCHIVE_CONTENTS_PATH="$SKIA_DIR/$ARCHIVE_CONTENTS_NAME"
+echo $ARCHIVE_CONTENTS_PATH
+
+ARCHIVE_CONTENTS="missing"
+if test -f "$ARCHIVE_CONTENTS_PATH"; then
+    ARCHIVE_CONTENTS="$(cat $ARCHIVE_CONTENTS_PATH)"
+fi
+
+# TODO: could add OS_RELEASE in if portability is a problem
+# TODO: hmm how do we know the make skia script.. i guess its an arg? a back arg?
+if [[ $OSTYPE == 'darwin'* ]]; then
+    # md5 -r == md5sum
+    CONFIGURE_VERSION=$(md5 -r cache_helper.sh | awk '{print $1}')
+    MAKE_SKIA_HASH=$(md5 -r $SKIA_DEPENDENCIES_DIR/$MAKE_SKIA_FILE | awk '{print $1}')
+    BUILD_HASH=$(md5 -r -s "$SKIA_COMMIT_HASH $MAKE_SKIA_HASH $CONFIGURE_VERSION" | awk '{print $1}')
+else
+    CONFIGURE_VERSION=$(md5sum cache_helper.sh | awk '{print $1}')
+    MAKE_SKIA_HASH=$(md5sum $SKIA_DEPENDENCIES_DIR/$MAKE_SKIA_FILE | awk '{print $1}')
+    BUILD_HASH=$(echo "$SKIA_COMMIT_HASH $MAKE_SKIA_HASH $CONFIGURE_VERSION" | md5sum | awk '{print $1}')
+fi
+
+echo "Created hash: $BUILD_HASH from skia_commit=$SKIA_COMMIT_HASH make_skia_script=$MAKE_SKIA_HASH configure_script=$CONFIGURE_VERSION"
+
+EXPECTED_ARCHIVE_CONTENTS="$BUILD_HASH"_"$COMPILE_TARGET"
+
+ARCHIVE_FILE_NAME="$CACHE_NAME"_"$EXPECTED_ARCHIVE_CONTENTS.tar.gz"
+ARCHIVE_URL="https://cdn.rive.app/archives/$ARCHIVE_FILE_NAME"
+ARCHIVE_PATH="$SKIA_DIR/$ARCHIVE_FILE_NAME"
+
+pull_cache() {
+    echo "Grabbing cached build from $ARCHIVE_URL"
+    mkdir -p $SKIA_DIR
+    curl --output $SKIA_DIR/$ARCHIVE_FILE_NAME $ARCHIVE_URL
+    pushd $SKIA_DIR
+    tar -xf $ARCHIVE_FILE_NAME out include $ARCHIVE_CONTENTS_NAME third_party modules
+}
+
+is_build_cached_remotely() {
+    echo "Checking for cache build $ARCHIVE_URL"
+    if curl --output /dev/null --head --silent --fail $ARCHIVE_URL; then
+        return 0
+    else
+        return 1
+    fi
+}
+
+upload_cache() {
+    pushd $SKIA_DEPENDENCIES_DIR
+    echo $EXPECTED_ARCHIVE_CONTENTS >$SKIA_DIR_NAME/$ARCHIVE_CONTENTS_NAME
+    # not really sure about this third party biz
+    # also we are caching on a per architecture path here, but out could contain more :thinking:
+    tar -C $SKIA_DIR_NAME -cf $SKIA_DIR_NAME/$ARCHIVE_FILE_NAME $OUTPUT_CACHE $ARCHIVE_CONTENTS_NAME include third_party/libpng third_party/externals/libpng modules
+    popd
+    # # if we're configured to upload the archive back into our cache, lets do it!
+    echo "Uploading to s3://2d-public/archives/$ARCHIVE_FILE_NAME"
+    ls $ARCHIVE_PATH
+    aws s3 cp $ARCHIVE_PATH s3://2d-public/archives/$ARCHIVE_FILE_NAME
+}
+
+is_build_cached_locally() {
+    if [ "$EXPECTED_ARCHIVE_CONTENTS" == "$ARCHIVE_CONTENTS" ]; then
+        return 0
+    else
+        return 1
+    fi
+}
diff --git a/skia/dependencies/get_skia.sh b/skia/dependencies/get_skia.sh
new file mode 100755
index 0000000..f479ef9
--- /dev/null
+++ b/skia/dependencies/get_skia.sh
@@ -0,0 +1,42 @@
+#!/bin/sh
+
+set -e
+
+# Requires depot_tools and git:
+#   https://skia.org/user/download
+# Build notes:
+#   https://skia.org/user/build
+# GLFW requires CMake
+getSkia() {
+    SKIA_REPO=$1
+    SKIA_STABLE_BRANCH=$2
+    FOLDER_NAME=$3
+    # -----------------------------
+    # Get & Build Skia
+    # -----------------------------
+    if [ ! -d $FOLDER_NAME ]; then
+        echo "Cloning Skia into $FOLDER_NAME."
+        git clone $SKIA_REPO $FOLDER_NAME
+    else
+        echo "Already have Skia in $FOLDER_NAME, update it."
+        cd $FOLDER_NAME && git fetch && git pull
+        cd ..
+    fi
+
+    cd $FOLDER_NAME
+
+    # switch to a stable branch
+    echo "Checking out stable branch $SKIA_STABLE_BRANCH"
+    git checkout $SKIA_STABLE_BRANCH
+    python tools/git-sync-deps
+    cd ..
+}
+
+if [ $# -eq 0 ]; then
+    # No arguments supplied; checkout the default skia repos.
+    getSkia https://github.com/rive-app/skia rive skia_rive_optimized
+    getSkia https://github.com/google/skia chrome/m99 skia
+else
+    # The caller specified which skia repo to checkout.
+    getSkia $@
+fi
diff --git a/skia/dependencies/get_skia2.sh b/skia/dependencies/get_skia2.sh
new file mode 100755
index 0000000..d99cd5b
--- /dev/null
+++ b/skia/dependencies/get_skia2.sh
@@ -0,0 +1,87 @@
+#!/bin/bash
+
+set -e
+
+# Requires depot_tools and git: 
+#   https://skia.org/user/download
+# Build notes:
+#   https://skia.org/user/build
+# GLFW requires CMake
+
+SKIA_REPO="${SKIA_REPO:-https://github.com/rive-app/skia}"
+SKIA_BRANCH="${SKIA_BRANCH:-rive}"
+
+# note: we have imports in recorder that rely on this being "skia"
+# either: change that in recorder, or check out skia into a subfolder
+# both have consequences. 
+SKIA_DIR_NAME="${SKIA_DIR_NAME:-skia}"
+
+_skiaExists() {
+    if test -d $SKIA_DIR_NAME/.git; then 
+        return 0
+    else 
+        return 1
+    fi
+}
+
+_skiaHasExpectedRemote() {
+    if test -d $SKIA_DIR_NAME/.git; then
+        pushd $SKIA_DIR_NAME
+        if git remote -v |grep "$SKIA_REPO"; then
+            popd
+            return 0
+        else 
+            popd
+            echo "Skia it is using the wrong remote."
+            return 1
+        fi 
+    else
+        echo "Skia is expected to be a git repo"
+        return 1
+    fi 
+}
+
+_updateSkia() {
+    pushd $SKIA_DIR_NAME
+    git fetch && git pull
+    popd
+}
+
+_cloneSkia() {
+    echo "Cloning Skia [$SKIA_REPO : $SKIA_BRANCH] into $SKIA_DIR_NAME."
+    git clone $SKIA_REPO $SKIA_DIR_NAME
+}
+
+_removeSkia(){
+    echo "Removing skia folder."
+    rm -rf $SKIA_DIR_NAME
+}
+
+getSkia () {
+    # -----------------------------
+    # Get Skia:
+    # -----------------------------
+    if _skiaExists; then 
+        if _skiaHasExpectedRemote; then 
+            _updateSkia
+        else 
+            _removeSkia
+            _cloneSkia
+        fi 
+    else 
+        _removeSkia
+        _cloneSkia
+    fi 
+
+    pushd $SKIA_DIR_NAME
+
+    echo "Checking out branch $SKIA_BRANCH"
+    git checkout $SKIA_BRANCH
+    # Remove piet-gpu from dependencies because repository does not exist anymore
+    sed -i.bak -r 's/.*piet.*//g' "$PWD/DEPS"
+
+
+    python tools/git-sync-deps
+
+    popd 
+}
\ No newline at end of file
diff --git a/skia/dependencies/make_dependencies.sh b/skia/dependencies/make_dependencies.sh
new file mode 100755
index 0000000..cede629
--- /dev/null
+++ b/skia/dependencies/make_dependencies.sh
@@ -0,0 +1,2 @@
+./make_skia.sh
+./make_glfw.sh
\ No newline at end of file
diff --git a/skia/dependencies/make_ffmpeg.sh b/skia/dependencies/make_ffmpeg.sh
new file mode 100755
index 0000000..d26b698
--- /dev/null
+++ b/skia/dependencies/make_ffmpeg.sh
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+set -e
+
+# Based on http://www.alamtechstuffs.com/ffmpegcompile_with_x264/
+
+FFMPEG_REPO=https://github.com/FFmpeg/FFmpeg
+
+# -----------------------------
+# Get & Build Skia
+# -----------------------------
+if [ ! -d FFmpeg ]; then
+	echo "Cloning FFmpeg."
+    git clone $FFMPEG_REPO
+else
+    echo "Already have FFmpeg, update it."
+    cd FFmpeg && git checkout master && git fetch && git pull
+    cd ..
+fi
+
+cd FFmpeg
+
+git checkout n4.3.1
+
+./configure  --disable-debug --enable-gpl --enable-libx264 --enable-pthreads --enable-static --extra-cflags=-I../x264/include --extra-ldflags=-L../x264/lib --extra-libs=-ldl
+make -j8
+cd ..
\ No newline at end of file
diff --git a/skia/dependencies/make_gl3w.sh b/skia/dependencies/make_gl3w.sh
new file mode 100755
index 0000000..cbcf32b
--- /dev/null
+++ b/skia/dependencies/make_gl3w.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+
+set -e
+
+# GL3W requires CMake
+
+GL3W_REPO=https://github.com/skaslev/gl3w
+GL3W_STABLE_BRANCH=master
+
+if [ ! -d gl3w ]; then
+	echo "Cloning gl3w."
+    git clone $GL3W_REPO
+fi
+
+cd gl3w && git checkout $GL3W_STABLE_BRANCH && git fetch && git pull
+
+mkdir -p build
+cd build
+cmake ../
+make
\ No newline at end of file
diff --git a/skia/dependencies/make_glfw.sh b/skia/dependencies/make_glfw.sh
new file mode 100755
index 0000000..30b1f04
--- /dev/null
+++ b/skia/dependencies/make_glfw.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+set -e
+
+# GLFW requires CMake
+
+GLFW_REPO=https://github.com/glfw/glfw
+
+# -----------------------------
+# Get & Build GLFW
+# -----------------------------
+if [ ! -d glfw ]; then
+    echo "Cloning GLFW."
+    git clone $GLFW_REPO
+else
+    echo "Already have GLFW, update it."
+    cd glfw && git fetch && git merge master
+    cd ..
+fi
+
+mkdir -p glfw_build
+cd glfw_build
+# On Windows, match Skia's /MT flag for link compatibility.
+cmake ../glfw -DBUILD_SHARED_LIBS=OFF -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded -DGLFW_BUILD_WAYLAND=OFF
+cmake --build . --parallel --config Release
diff --git a/skia/dependencies/make_imgui.sh b/skia/dependencies/make_imgui.sh
new file mode 100755
index 0000000..944b91a
--- /dev/null
+++ b/skia/dependencies/make_imgui.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+set -e
+
+IMGUI_REPO=https://github.com/ocornut/imgui
+IMGUI_STABLE_BRANCH=master
+
+if [ ! -d imgui ]; then
+	echo "Cloning ImGui."
+    git clone $IMGUI_REPO
+fi
+
+cd imgui && git checkout $IMGUI_STABLE_BRANCH && git fetch && git pull
diff --git a/skia/dependencies/make_libzip.sh b/skia/dependencies/make_libzip.sh
new file mode 100755
index 0000000..dfd3e1b
--- /dev/null
+++ b/skia/dependencies/make_libzip.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+set -e
+
+# libzip requires CMake
+
+LIBZIP_REPO=https://github.com/nih-at/libzip/
+LIBZIP_RELEASE=v1.7.3
+
+# -----------------------------
+# Get & Build libzip
+# -----------------------------
+if [ ! -d libzip ]; then
+	echo "Cloning libzip."
+    git clone $LIBZIP_REPO --branch $LIBZIP_RELEASE
+else
+    echo "Already have libzip, update it."
+    # cd libzip && git fetch && git merge master
+    cd libzip
+    LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)
+    git checkout $LATEST_TAG
+    cd ..
+fi
+
+mkdir -p libzip_build
+cd libzip_build
+cmake ../libzip -DBUILD_SHARED_LIBS=OFF
+make
\ No newline at end of file
diff --git a/skia/dependencies/make_skia.sh b/skia/dependencies/make_skia.sh
new file mode 100755
index 0000000..d1f1008
--- /dev/null
+++ b/skia/dependencies/make_skia.sh
@@ -0,0 +1,265 @@
+#!/bin/sh
+
+set -e
+
+export MAKE_SKIA_FILE="$0"
+./get_skia.sh
+
+# use Rive optimized/stripped Skia for iOS static libs.
+cd skia_rive_optimized
+
+python tools/git-sync-deps
+bin/gn gen out/ios64 --type=static_library --args=" \
+    target_os=\"ios\" \
+    target_cpu=\"arm64\" \
+    extra_cflags=[\"-fno-rtti\", \"-fembed-bitcode\", \"-mios-version-min=10.0\", \"-flto=full\", \"-DSK_DISABLE_SKPICTURE\", \"-DSK_DISABLE_TEXT\", \"-DRIVE_OPTIMIZED\", \"-DSK_DISABLE_LEGACY_SHADERCONTEXT\", \"-DSK_DISABLE_LOWP_RASTER_PIPELINE\", \"-DSK_FORCE_RASTER_PIPELINE_BLITTER\", \"-DSK_DISABLE_AAA\", \"-DSK_DISABLE_EFFECT_DESERIALIZATION\"] \
+
+    is_official_build=true \
+    skia_use_freetype=true \
+    skia_use_metal=true \
+    skia_use_zlib=true \
+    skia_enable_gpu=true \
+    skia_use_libpng_encode=true \
+    skia_use_libpng_decode=true \
+    
+    skia_use_angle=false \
+    skia_use_dng_sdk=false \
+    skia_use_egl=false \
+    skia_use_expat=false \
+    skia_use_fontconfig=false \
+    skia_use_system_freetype2=false \
+    skia_use_icu=false \
+    skia_use_libheif=false \
+    skia_use_system_libpng=false \
+    skia_use_system_libjpeg_turbo=false \
+    skia_use_libjpeg_turbo_encode=false \
+    skia_use_libjpeg_turbo_decode=true \
+    skia_use_libwebp_encode=false \
+    skia_use_libwebp_decode=true \
+    skia_use_system_libwebp=false \
+    skia_use_lua=false \
+    skia_use_piex=false \
+    skia_use_vulkan=false \
+    skia_use_gl=false \
+    skia_use_system_zlib=false \
+    skia_enable_fontmgr_empty=false \
+    skia_enable_spirv_validation=false \
+    skia_enable_pdf=false \
+    skia_enable_skottie=false \
+    "
+ninja -C out/ios64
+
+bin/gn gen out/ios32 --type=static_library --args=" \
+    target_os=\"ios\" \
+    target_cpu=\"arm\" \
+    extra_cflags=[\"-fno-rtti\", \"-fembed-bitcode\", \"-mios-version-min=10.0\", \"-flto=full\", \"-DSK_DISABLE_SKPICTURE\", \"-DSK_DISABLE_TEXT\", \"-DRIVE_OPTIMIZED\", \"-DSK_DISABLE_LEGACY_SHADERCONTEXT\", \"-DSK_DISABLE_LOWP_RASTER_PIPELINE\", \"-DSK_FORCE_RASTER_PIPELINE_BLITTER\", \"-DSK_DISABLE_AAA\", \"-DSK_DISABLE_EFFECT_DESERIALIZATION\"] \
+
+    is_official_build=true \
+    skia_use_freetype=true \
+    skia_use_metal=true \
+    skia_use_zlib=true \
+    skia_enable_gpu=true \
+    skia_use_libpng_encode=true \
+    skia_use_libpng_decode=true \
+    skia_skip_codesign=true \
+    
+    skia_use_angle=false \
+    skia_use_dng_sdk=false \
+    skia_use_egl=false \
+    skia_use_expat=false \
+    skia_use_fontconfig=false \
+    skia_use_system_freetype2=false \
+    skia_use_icu=false \
+    skia_use_libheif=false \
+    skia_use_system_libpng=false \
+    skia_use_system_libjpeg_turbo=false \
+    skia_use_libjpeg_turbo_encode=false \
+    skia_use_libjpeg_turbo_decode=true \
+    skia_use_libwebp_encode=false \
+    skia_use_libwebp_decode=true \
+    skia_use_system_libwebp=false \
+    skia_use_lua=false \
+    skia_use_piex=false \
+    skia_use_vulkan=false \
+    skia_use_gl=false \
+    skia_use_system_zlib=false \
+    skia_enable_fontmgr_empty=false \
+    skia_enable_spirv_validation=false \
+    skia_enable_pdf=false \
+    skia_enable_skottie=false \
+    "
+ninja -C out/ios32
+
+bin/gn gen out/iossim_x86 --type=static_library --args=" \
+    target_os=\"ios\" \
+    target_cpu=\"x86\" \
+    extra_cflags=[\"-fno-rtti\", \"-fembed-bitcode\", \"-mios-version-min=10.0\", \"-flto=full\", \"-DSK_DISABLE_SKPICTURE\", \"-DSK_DISABLE_TEXT\", \"-DRIVE_OPTIMIZED\", \"-DSK_DISABLE_LEGACY_SHADERCONTEXT\", \"-DSK_DISABLE_LOWP_RASTER_PIPELINE\", \"-DSK_FORCE_RASTER_PIPELINE_BLITTER\", \"-DSK_DISABLE_AAA\", \"-DSK_DISABLE_EFFECT_DESERIALIZATION\"] \
+
+    is_official_build=true \
+    skia_use_freetype=true \
+    skia_use_metal=true \
+    skia_use_zlib=true \
+    skia_enable_gpu=true \
+    skia_use_libpng_encode=true \
+    skia_use_libpng_decode=true \
+    skia_skip_codesign=true \
+
+    skia_use_angle=false \
+    skia_use_dng_sdk=false \
+    skia_use_egl=false \
+    skia_use_expat=false \
+    skia_use_fontconfig=false \
+    skia_use_system_freetype2=false \
+    skia_use_icu=false \
+    skia_use_libheif=false \
+    skia_use_system_libpng=false \
+    skia_use_system_libjpeg_turbo=false \
+    skia_use_libjpeg_turbo_encode=false \
+    skia_use_libjpeg_turbo_decode=true \
+    skia_use_libwebp_encode=false \
+    skia_use_libwebp_decode=true \
+    skia_use_system_libwebp=false \
+    skia_use_lua=false \
+    skia_use_piex=false \
+    skia_use_vulkan=false \
+    skia_use_gl=false \
+    skia_use_system_zlib=false \
+    skia_enable_fontmgr_empty=false \
+    skia_enable_spirv_validation=false \
+    skia_enable_pdf=false \
+    skia_enable_skottie=false \
+    "
+ninja -C out/iossim_x86
+
+bin/gn gen out/iossim_x64 --type=static_library --args=" \
+    target_os=\"ios\" \
+    target_cpu=\"x64\" \
+    extra_cflags=[\"-fno-rtti\", \"-fembed-bitcode\", \"-mios-version-min=10.0\", \"-flto=full\", \"-DSK_DISABLE_SKPICTURE\", \"-DSK_DISABLE_TEXT\", \"-DRIVE_OPTIMIZED\", \"-DSK_DISABLE_LEGACY_SHADERCONTEXT\", \"-DSK_DISABLE_LOWP_RASTER_PIPELINE\", \"-DSK_FORCE_RASTER_PIPELINE_BLITTER\", \"-DSK_DISABLE_AAA\", \"-DSK_DISABLE_EFFECT_DESERIALIZATION\"] \
+
+    is_official_build=true \
+    skia_use_freetype=true \
+    skia_use_metal=true \
+    skia_use_zlib=true \
+    skia_enable_gpu=true \
+    skia_use_libpng_encode=true \
+    skia_use_libpng_decode=true \
+    skia_skip_codesign=true \
+
+    skia_use_angle=false \
+    skia_use_dng_sdk=false \
+    skia_use_egl=false \
+    skia_use_expat=false \
+    skia_use_fontconfig=false \
+    skia_use_system_freetype2=false \
+    skia_use_icu=false \
+    skia_use_libheif=false \
+    skia_use_system_libpng=false \
+    skia_use_system_libjpeg_turbo=false \
+    skia_use_libjpeg_turbo_encode=false \
+    skia_use_libjpeg_turbo_decode=true \
+    skia_use_libwebp_encode=false \
+    skia_use_libwebp_decode=true \
+    skia_use_system_libwebp=false \
+    skia_use_lua=false \
+    skia_use_piex=false \
+    skia_use_vulkan=false \
+    skia_use_gl=false \
+    skia_use_system_zlib=false \
+    skia_enable_fontmgr_empty=false \
+    skia_enable_spirv_validation=false \
+    skia_enable_pdf=false \
+    skia_enable_skottie=false \
+    "
+ninja -C out/iossim_x64
+
+bin/gn gen out/iossim_arm64 --type=static_library --args=" \
+    target_os=\"ios\" \
+    target_cpu=\"arm64\" \
+    extra_cflags=[\"-fno-rtti\", \"-fembed-bitcode\", \"-flto=full\", \"-DSK_DISABLE_SKPICTURE\", \"-DSK_DISABLE_TEXT\", \"-DRIVE_OPTIMIZED\", \"-DSK_DISABLE_LEGACY_SHADERCONTEXT\", \"-DSK_DISABLE_LOWP_RASTER_PIPELINE\", \"-DSK_FORCE_RASTER_PIPELINE_BLITTER\", \"-DSK_DISABLE_AAA\", \"-DSK_DISABLE_EFFECT_DESERIALIZATION\", \"--target=arm64-apple-ios13.0.0-simulator\"] \
+    extra_ldflags=[\"--target=arm64-apple-ios13.0.0-simulator\"] \
+    is_official_build=true \
+    skia_use_angle=false \
+    skia_use_dng_sdk=false \
+    skia_use_egl=false \
+    skia_use_expat=false \
+    skia_use_fontconfig=false \
+    skia_use_freetype=true \
+    skia_use_system_freetype2=false \
+    skia_use_icu=false \
+    skia_use_libheif=false \
+    skia_use_system_libpng=false \
+    skia_use_system_libjpeg_turbo=false \
+    skia_use_libjpeg_turbo_encode=false \
+    skia_use_libjpeg_turbo_decode=true \
+    skia_use_libwebp_encode=false \
+    skia_use_libwebp_decode=true \
+    skia_use_system_libwebp=false \
+    skia_use_lua=false \
+    skia_use_piex=false \
+    skia_use_vulkan=false \
+    skia_use_metal=true \
+    skia_use_gl=false \
+    skia_use_zlib=true \
+    skia_use_system_zlib=false \
+    skia_enable_gpu=true \
+    skia_enable_fontmgr_empty=false \
+    skia_enable_spirv_validation=false \
+    skia_enable_pdf=false \
+    skia_use_libpng_encode = true \
+    skia_use_libpng_decode = true \
+    skia_enable_skottie = false \
+    skia_skip_codesign = true \
+    "
+ninja -C out/iossim_arm64
+
+# make fat library, note that the ios64 library is already fat with arm64 and arm64e so we don't specify arch there.
+xcrun -sdk iphoneos lipo -create -arch armv7 out/ios32/libskia.a out/ios64/libskia.a -output out/libskia_ios.a
+xcrun -sdk iphoneos lipo -create -arch x86_64 out/iossim_x64/libskia.a -arch i386 out/iossim_x86/libskia.a out/iossim_arm64/libskia.a -output out/libskia_ios_sim.a
+
+# build regular/full skia
+cd ../skia
+
+# build static for host
+bin/gn gen out/static --type=static_library --args=" \
+    extra_cflags=[\"-fno-rtti\", \"-flto=full\", \"-DSK_DISABLE_SKPICTURE\", \"-DSK_DISABLE_TEXT\", \"-DRIVE_OPTIMIZED\", \"-DSK_DISABLE_LEGACY_SHADERCONTEXT\", \"-DSK_DISABLE_LOWP_RASTER_PIPELINE\", \"-DSK_FORCE_RASTER_PIPELINE_BLITTER\", \"-DSK_DISABLE_AAA\", \"-DSK_DISABLE_EFFECT_DESERIALIZATION\"] \
+
+    is_official_build=true \
+    skia_use_gl=true \
+    skia_use_zlib=true \
+    skia_enable_gpu=true \
+    skia_enable_fontmgr_empty=false \
+    skia_use_libpng_encode=true \
+    skia_use_libpng_decode=true \
+    skia_enable_skgpu_v1=true \
+
+    skia_use_dng_sdk=false \
+    skia_use_egl=false \
+    skia_use_expat=false \
+    skia_use_fontconfig=false \
+    skia_use_freetype=false \
+    skia_use_icu=false \
+    skia_use_libheif=false \
+    skia_use_system_libpng=false \
+    skia_use_system_libjpeg_turbo=false \
+    skia_use_libjpeg_turbo_encode=false \
+    skia_use_libjpeg_turbo_decode=true \
+    skia_use_libwebp_encode=false \
+    skia_use_libwebp_decode=true \
+    skia_use_system_libwebp=false \
+    skia_use_lua=false \
+    skia_use_piex=false \
+    skia_use_vulkan=false \
+    skia_use_metal=false \
+    skia_use_angle=false \
+    skia_use_system_zlib=false \
+    skia_enable_spirv_validation=false \
+    skia_enable_pdf=false \
+    skia_enable_skottie=false \
+    skia_enable_tools=false \
+    skia_enable_skgpu_v2=false \
+    skia_gl_standard = \"\" \
+    "
+ninja -C out/static
+du -hs out/static/libskia.a
+
+cd ..
diff --git a/skia/dependencies/make_skia_android.sh b/skia/dependencies/make_skia_android.sh
new file mode 100755
index 0000000..039a455
--- /dev/null
+++ b/skia/dependencies/make_skia_android.sh
@@ -0,0 +1,114 @@
+#!/bin/bash
+
+set -ex
+
+export MAKE_SKIA_FILE="$0"
+source ./get_skia2.sh
+source ./cache_helper.sh
+
+ARCH=$1
+CONFIG=$2
+
+build_skia_android() {
+    cd "$SKIA_DIR_NAME"
+
+    if [ "$ARCH" != "x86" ] &&
+        [ "$ARCH" != "x64" ] &&
+        [ "$ARCH" != "arm" ] &&
+        [ "$ARCH" != "arm64" ]; then
+        printf "Invalid architecture: '%s'. Choose one between 'x86', \
+            'x64', \
+            'arm', \
+            or 'arm64'" "$ARCH"
+        exit 1
+    fi
+
+    BUILD_FLAGS=
+    EXTRA_CFLAGS=
+    if [ "$CONFIG" = "debug" ]; then
+        BUILD_FLAGS="is_official_build=false is_debug=true skia_enable_tools=false"
+    else # release
+        BUILD_FLAGS="is_official_build=true is_debug=false"
+        EXTRA_CFLAGS="\
+    \"-fno-rtti\",                          \
+    \"-flto=full\",                         \
+    \"-fembed-bitcode\",                    \
+    \"-DRIVE_OPTIMIZED\",                   \
+    \"-DSK_DISABLE_SKPICTURE\",             \
+    \"-DSK_DISABLE_TEXT\",                  \
+    \"-DSK_DISABLE_LEGACY_SHADERCONTEXT\",  \
+    \"-DSK_DISABLE_LOWP_RASTER_PIPELINE\",  \
+    \"-DSK_FORCE_RASTER_PIPELINE_BLITTER\", \
+    \"-DSK_DISABLE_AAA\",                   \
+    \"-DSK_DISABLE_EFFECT_DESERIALIZATION\" \
+    "
+    fi
+
+    # Useful for debugging:
+    # bin/gn args --list out/${ARCH}
+
+    bin/gn gen out/"${CONFIG}"/"${ARCH}" --args=" \
+        ndk=\"${NDK_PATH}\" \
+        target_cpu=\"${ARCH}\" \
+        extra_cflags=[                              \
+            ${EXTRA_CFLAGS}                         \
+            ]                                       \
+        \
+        ${BUILD_FLAGS} \
+
+        skia_gl_standard=\"gles\" 
+        skia_use_zlib=true \
+        skia_use_egl=true \
+        skia_use_gl=true \
+        skia_enable_gpu=true \
+        skia_use_libpng_decode=false \
+        skia_use_libpng_encode=false \
+
+        skia_use_angle=false \
+        skia_use_dng_sdk=false \
+        skia_use_freetype=false \
+        
+        skia_use_expat=false \
+        skia_use_fontconfig=false \
+        skia_use_system_freetype2=false \
+        skia_use_icu=false \
+        skia_use_libheif=false \
+        skia_use_system_libpng=false \
+        skia_use_system_libjpeg_turbo=false \
+        skia_use_system_libwebp=false \
+        skia_use_libjpeg_turbo_encode=false \
+        skia_use_libjpeg_turbo_decode=false \
+        skia_use_libwebp_encode=false \
+        skia_use_libwebp_decode=false \
+        skia_use_lua=false \
+        skia_use_piex=false \
+        skia_use_vulkan=false \
+        
+        skia_use_system_zlib=false \
+        skia_enable_fontmgr_empty=false \
+        skia_enable_spirv_validation=false \
+        skia_enable_pdf=false \
+        skia_enable_skottie=false \
+        
+        skia_enable_skshaper=false \
+        "
+
+    ninja -C out/"${CONFIG}"/"${ARCH}"
+    cd ..
+}
+
+if is_build_cached_locally; then
+    echo "Build is cached, nothing to do."
+else
+    if is_build_cached_remotely; then
+        pull_cache
+    else
+        getSkia
+        build_skia_android
+        # (umberto) commenting this out for now for CMake builds.
+        # hmm not the appiest with this guy
+        # if [ "$CONFIG" != "debug" ]; then
+        OUTPUT_CACHE=out/"${CONFIG}"/$ARCH upload_cache
+        # fi
+    fi
+fi
diff --git a/skia/dependencies/make_skia_ios.sh b/skia/dependencies/make_skia_ios.sh
new file mode 100755
index 0000000..bc10d49
--- /dev/null
+++ b/skia/dependencies/make_skia_ios.sh
@@ -0,0 +1,119 @@
+#!/bin/bash
+
+set -ex
+
+export MAKE_SKIA_FILE="$0"
+source ./get_skia2.sh
+source ./cache_helper.sh
+
+build_skia_ios() {
+    cd $SKIA_DIR_NAME
+
+    BASE=out/ios
+    case $1 in
+    arm64)
+        ARCH=arm64
+        FOLDER=$BASE/arm64
+        ;;
+    arm)
+        ARCH=arm
+        FOLDER=$BASE/arm
+        ;;
+    x86)
+        ARCH=x86
+        FOLDER=$BASE/x86
+        ;;
+    x64)
+        ARCH=x64
+        FOLDER=$BASE/x64
+        ;;
+    iossim_arm64)
+        ARCH=arm64
+        FOLDER=$BASE/iossim_arm64
+        EXTRA_CFLAGS=", \"--target=arm64-apple-ios13.0.0-simulator\""
+        EXTRA_LDLAGS="\"--target=arm64-apple-ios13.0.0-simulator\""
+        ;;
+    *)
+        echo "Do not know build configuration for $1"
+        exit 1
+        ;;
+    esac
+
+    # use Rive optimized/stripped Skia for iOS static libs.
+    bin/gn gen $FOLDER --type=static_library --args="   \
+        target_os=\"ios\"                                   \
+        target_cpu=\"$ARCH\"                                \
+        extra_cflags=[                                      \
+            \"-fno-rtti\",                                  \
+            \"-fembed-bitcode\",                            \
+            \"-mios-version-min=10.0\",                     \
+            \"-flto=full\",                                 \
+            \"-DSK_DISABLE_SKPICTURE\",                     \
+            \"-DSK_DISABLE_TEXT\",                          \
+            \"-DRIVE_OPTIMIZED\",                           \
+            \"-DSK_DISABLE_LEGACY_SHADERCONTEXT\",          \
+            \"-DSK_DISABLE_LOWP_RASTER_PIPELINE\",          \
+            \"-DSK_FORCE_RASTER_PIPELINE_BLITTER\",         \
+            \"-DSK_DISABLE_AAA\",                           \
+            \"-DSK_DISABLE_EFFECT_DESERIALIZATION\"         \
+            ${EXTRA_CFLAGS}
+        ]                                                   \
+
+        extra_ldflags=[                                     \
+            ${EXTRA_LDLAGS}                                 \
+        ]                                                   \
+
+        is_official_build=true \
+        skia_use_freetype=false \
+        skia_use_metal=true \
+        skia_use_zlib=true \
+        skia_enable_gpu=true \
+        skia_use_libpng_encode=true \
+        skia_use_libpng_decode=true \
+        skia_skip_codesign=true \
+        
+        skia_use_angle=false \
+        skia_use_dng_sdk=false \
+        skia_use_egl=false \
+        skia_use_expat=false \
+        skia_use_fontconfig=false \
+        skia_use_system_freetype2=false \
+        skia_use_icu=false \
+        skia_use_libheif=false \
+        skia_use_system_libpng=false \
+        skia_use_system_libjpeg_turbo=false \
+        skia_use_libjpeg_turbo_encode=false \
+        skia_use_libjpeg_turbo_decode=true \
+        skia_use_libwebp_encode=false \
+        skia_use_libwebp_decode=true \
+        skia_use_system_libwebp=false \
+        skia_use_lua=false \
+        skia_use_piex=false \
+        skia_use_vulkan=false \
+        skia_use_gl=false \
+        skia_use_system_zlib=false \
+        skia_enable_fontmgr_empty=false \
+        skia_enable_spirv_validation=false \
+        skia_enable_pdf=false \
+        skia_enable_skottie=false \
+        skia_enable_tools=false \
+        $OVERRIDES
+        "
+    ninja -C $FOLDER
+    cd ..
+}
+
+if is_build_cached_locally; then
+    echo "Build is cached, nothing to do."
+else
+    if is_build_cached_remotely; then
+        pull_cache
+    else
+        getSkia
+        build_skia_ios $1
+        # hmm not the appiest with this guy
+        OUTPUT_CACHE=$FOLDER upload_cache
+    fi
+fi
+
+cd ..
diff --git a/skia/dependencies/make_skia_macos.sh b/skia/dependencies/make_skia_macos.sh
new file mode 100755
index 0000000..5303a99
--- /dev/null
+++ b/skia/dependencies/make_skia_macos.sh
@@ -0,0 +1,126 @@
+#!/bin/bash
+
+set -ex
+
+export MAKE_SKIA_FILE="$0"
+source ./get_skia2.sh
+source ./cache_helper.sh
+
+build_skia_macos(){
+    cd $SKIA_DIR_NAME
+
+    local SHARED_EXTRA_CFLAGS="\
+            \"-fno-rtti\",                                  \
+            \"-fembed-bitcode\",                            \
+            \"-flto=full\",                                 \
+            \"-DSK_DISABLE_SKPICTURE\",                     \
+            \"-DSK_DISABLE_TEXT\",                          \
+            \"-DRIVE_OPTIMIZED\",                           \
+            \"-DSK_DISABLE_LEGACY_SHADERCONTEXT\",          \
+            \"-DSK_DISABLE_LOWP_RASTER_PIPELINE\",          \
+            \"-DSK_FORCE_RASTER_PIPELINE_BLITTER\",         \
+            \"-DSK_DISABLE_AAA\",                           \
+            \"-DSK_DISABLE_EFFECT_DESERIALIZATION\"         \
+    "
+
+    local SHARED_ARGS="\
+        is_official_build=true \
+        skia_use_freetype=false \
+        skia_use_metal=true \
+        skia_use_zlib=true \
+        skia_enable_gpu=true \
+        skia_use_libpng_encode=true \
+        skia_use_libpng_decode=true \
+        skia_skip_codesign=true \
+        
+        skia_use_angle=false \
+        skia_use_dng_sdk=false \
+        skia_use_egl=false \
+        skia_use_expat=false \
+        skia_use_fontconfig=false \
+        skia_use_system_freetype2=false \
+        skia_use_icu=false \
+        skia_use_libheif=false \
+        skia_use_system_libpng=false \
+        skia_use_system_libjpeg_turbo=false \
+        skia_use_libjpeg_turbo_encode=false \
+        skia_use_libjpeg_turbo_decode=true \
+        skia_use_libwebp_encode=false \
+        skia_use_libwebp_decode=true \
+        skia_use_system_libwebp=false \
+        skia_use_lua=false \
+        skia_use_piex=false \
+        skia_use_vulkan=false \
+        skia_use_gl=false \
+        skia_use_system_zlib=false \
+        skia_enable_fontmgr_empty=false \
+        skia_enable_spirv_validation=false \
+        skia_enable_pdf=false \
+        skia_enable_skottie=false \
+        skia_enable_tools=false \
+    "
+    BASE=out/macosx
+    case $1 in 
+        arm64) 
+            ARCH=arm64
+            FOLDER=$BASE/arm64
+
+            # use Rive optimized/stripped Skia for macOS static libs.
+            bin/gn gen $FOLDER --type=static_library --args="   \
+            target_os=\"mac\"                                       \
+            target_cpu=\"arm64\"                                    \
+            extra_cflags=[                                          \
+                ${SHARED_EXTRA_CFLAGS}
+            ]                                                       \
+            ${SHARED_ARGS}
+            "
+            ;;
+        x64) 
+            ARCH=x64
+            FOLDER=$BASE/x64
+
+            bin/gn gen $FOLDER --type=static_library --args="   \
+            target_os=\"mac\"                                       \
+            target_cpu=\"x64\"                                      \
+            extra_cflags=[                                          \
+                \"--target=x86_64-apple-macos10.12\",               \
+                ${SHARED_EXTRA_CFLAGS}
+            ]                                                       \
+            extra_asmflags = [                                      \
+                \"--target=x86_64-apple-macos10.12\"                \
+            ]                                                       \
+            extra_cflags_c = [                                      \
+                \"--target=x86_64-apple-macos10.12\",               \
+                \"-Wno-error\"                                      \
+            ]                                                       \
+            extra_ldflags=[                                         \
+                \"--target=x86_64-apple-macos10.12\"                \
+            ]                                                       \
+            cc = \"clang\"                                          \
+            cxx = \"clang++\"                                       \
+            ${SHARED_ARGS}
+            "
+            ;;
+        *) 
+            echo "Do not know build configuration for $1"
+            exit 1
+    esac
+    
+    ninja -C $FOLDER
+    cd ..
+}
+
+if is_build_cached_locally; then 
+    echo "Build is cached, nothing to do."
+else
+    if is_build_cached_remotely; then 
+        pull_cache
+    else 
+        getSkia
+        build_skia_macos $1
+        # hmm not the happiest with this guy
+        OUTPUT_CACHE=$FOLDER upload_cache
+    fi 
+fi
+
+cd ..
\ No newline at end of file
diff --git a/skia/dependencies/make_skia_recorder.sh b/skia/dependencies/make_skia_recorder.sh
new file mode 100755
index 0000000..0037be6
--- /dev/null
+++ b/skia/dependencies/make_skia_recorder.sh
@@ -0,0 +1,81 @@
+#!/bin/bash
+set -ex
+
+export MAKE_SKIA_FILE="$0"
+source ./get_skia2.sh
+source ./cache_helper.sh
+
+build_skia_recorder() {
+    cd $SKIA_DIR_NAME
+
+    # This header is required so Skia can continue to use its internal copy of libpng while we link
+    # with our own.
+    cp ../../pngprefix/pngprefix.h third_party/externals/libpng/
+    echo '#include <pngprefix.h>' >> third_party/libpng/pnglibconf.h
+    
+    bin/gn gen out/static --args=" \
+        is_official_build=true \
+        extra_cflags=[
+        \"-fno-rtti\",\
+        \"-DSK_DISABLE_SKPICTURE\",\
+        \"-DSK_DISABLE_TEXT\",\
+        \"-DSK_DISABLE_LEGACY_SHADERCONTEXT\",\
+        \"-DSK_DISABLE_LOWP_RASTER_PIPELINE\",\
+        \"-DSK_FORCE_RASTER_PIPELINE_BLITTER\",\
+        \"-DSK_DISABLE_AAA\",\
+        \"-DSK_DISABLE_EFFECT_DESERIALIZATION\",\
+        \"-DPNG_PREFIX=sk_\"\
+        ]\
+        rive_use_picture=true \
+        skia_use_angle=false \
+        skia_use_dng_sdk=false \
+        skia_use_egl=false \
+        skia_use_expat=true \
+        skia_use_system_expat=false \
+        skia_enable_svg=true \
+        skia_use_fontconfig=false \
+        skia_use_freetype=false \
+        skia_use_icu=false \
+        skia_use_libheif=false \
+        skia_use_system_libpng=false \
+        skia_use_system_libjpeg_turbo=false \
+        skia_use_libjpeg_turbo_encode=false \
+        skia_use_libjpeg_turbo_decode=true \
+        skia_use_libwebp_encode=false \
+        skia_use_libwebp_decode=true \
+        skia_use_system_libwebp=false \
+        skia_use_lua=false \
+        skia_use_piex=false \
+        skia_use_vulkan=false \
+        skia_use_metal=false \
+        skia_use_gl=true \
+        skia_use_zlib=true \
+        skia_use_system_zlib=false \
+        skia_enable_gpu=true \
+        skia_enable_fontmgr_empty=true \
+        skia_enable_spirv_validation=false \
+        skia_enable_pdf=false \
+        skia_use_libpng_encode = true \
+        skia_use_libpng_decode = true \
+        skia_enable_skottie = false \
+        skia_enable_tools = false \
+        skia_enable_skgpu_v1 = true \
+        skia_enable_skgpu_v2 = false \
+        skia_gl_standard = \"\" \
+        "
+    ninja -C out/static
+
+    cd ..
+}
+
+if is_build_cached_locally; then 
+    echo "Build is cached, nothing to do."
+else
+    if is_build_cached_remotely; then 
+        pull_cache
+    else 
+        getSkia
+        build_skia_recorder
+        OUTPUT_CACHE=out upload_cache
+    fi 
+fi
diff --git a/skia/dependencies/make_skia_wasm.sh b/skia/dependencies/make_skia_wasm.sh
new file mode 100755
index 0000000..81b47c5
--- /dev/null
+++ b/skia/dependencies/make_skia_wasm.sh
@@ -0,0 +1,67 @@
+#!/bin/sh
+
+set -e
+
+# Requires depot_tools and git:
+#   https://skia.org/user/download
+# Build notes:
+#   https://skia.org/user/build
+# GLFW requires CMake
+
+if [ -z "${PAT_GITHUB}" ]; then
+    SKIA_URL=git@github.com:rive-app/skia.git
+else
+    SKIA_URL=https://${PAT_GITHUB}@github.com/rive-app/skia.git
+fi
+./get_skia.sh ${SKIA_URL} rive skia_rive_optimized
+
+cd skia_rive_optimized
+
+# build static for host
+bin/gn gen out/wasm --type=static_library --args=" \
+    cc=\"emcc\" \
+    cxx=\"em++\" \
+    ar=\"emar\" \
+    is_official_build=true \
+    target_cpu=\"wasm\" \
+    skia_use_angle=false \
+    skia_use_webgl=true \
+    extra_cflags=[\"-s\", \"-fno-rtti\", \"-flto=full\", \"-DSK_RELEASE\", \"-DSK_DISABLE_SKPICTURE\", \"-DSK_DISABLE_TEXT\", \"-DRIVE_OPTIMIZED\", \"-DSK_DISABLE_LEGACY_SHADERCONTEXT\", \"-DSK_DISABLE_LOWP_RASTER_PIPELINE\", \"-DSK_FORCE_RASTER_PIPELINE_BLITTER\", \"-DSK_DISABLE_AAA\", \"-DSK_DISABLE_EFFECT_DESERIALIZATION\", \"-DSK_SUPPORT_GPU=1\", \"-DSK_GL\", \"-DSK_DISABLE_TRACING\", \"-DSK_NO_FONTS\", \"-DSK_FORCE_8_BYTE_ALIGNMENT\"] \
+    skia_enable_fontmgr_custom_embedded=false \
+    skia_enable_fontmgr_custom_empty=false \
+    skia_use_dng_sdk=false \
+    skia_use_egl=false \
+    skia_use_expat=false \
+    skia_use_fontconfig=false \
+    skia_use_freetype=false \
+    skia_use_icu=false \
+    skia_use_libheif=false \
+    skia_use_system_libpng=false \
+    skia_use_system_libjpeg_turbo=false \
+    skia_use_libjpeg_turbo_encode=false \
+    skia_use_libjpeg_turbo_decode=true \
+    skia_use_libwebp_encode=false \
+    skia_use_libwebp_decode=false \
+    skia_use_lua=false \
+    skia_use_piex=false \
+    skia_use_vulkan=false \
+    skia_use_metal=false \
+    skia_use_gl=true \
+    skia_use_zlib=true \
+    skia_use_system_zlib=false \
+    skia_enable_gpu=true \
+    skia_gl_standard=\"webgl\" \
+    skia_enable_fontmgr_empty=true \
+    skia_enable_spirv_validation=false \
+    skia_enable_pdf=false \
+    skia_use_libpng_encode = true \
+    skia_use_libpng_decode = true \
+    skia_enable_skottie = false \
+    skia_enable_tools = false \
+    skia_enable_skgpu_v1 = true \
+    "
+
+ninja -C out/wasm libskia.a
+du -hs out/wasm/libskia.a
+
+cd ..
diff --git a/skia/dependencies/make_viewer_dependencies.sh b/skia/dependencies/make_viewer_dependencies.sh
new file mode 100755
index 0000000..4ccf061
--- /dev/null
+++ b/skia/dependencies/make_viewer_dependencies.sh
@@ -0,0 +1,4 @@
+./make_skia.sh
+./make_glfw.sh
+./make_gl3w.sh
+./make_imgui.sh
\ No newline at end of file
diff --git a/skia/dependencies/make_x264.sh b/skia/dependencies/make_x264.sh
new file mode 100755
index 0000000..bbd7611
--- /dev/null
+++ b/skia/dependencies/make_x264.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+set -e
+
+# Based on http://www.alamtechstuffs.com/ffmpegcompile_with_x264/
+# x264 has move here: https://code.videolan.org/videolan/x264
+X264_REPO=https://github.com/mirror/x264
+
+# -----------------------------
+# Get & Build Skia
+# -----------------------------
+if [ ! -d x264 ]; then
+	echo "Cloning x264."
+    git clone $X264_REPO
+else
+    echo "Already have x264, update it."
+    cd x264 && git checkout master && git fetch && git pull
+    cd ..
+fi
+
+cd x264
+
+./configure --enable-static --enable-pic --prefix=.
+make;make install
+cd ..
\ No newline at end of file
diff --git a/skia/font_converter/build.sh b/skia/font_converter/build.sh
new file mode 100755
index 0000000..02e73c8
--- /dev/null
+++ b/skia/font_converter/build.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+cd build
+
+OPTION=$1
+
+if [ "$OPTION" = 'help' ]; then
+    echo build.sh - build debug library
+    echo build.sh clean - clean the build
+    echo build.sh release - build release library
+elif [ "$OPTION" = "clean" ]; then
+    echo Cleaning project ...
+    # TODO: fix premake5 clean to bubble the clean command to dependent projects
+    premake5 gmake && make clean
+elif [ "$OPTION" = "release" ]; then
+    premake5 gmake && make config=release -j7
+else
+    premake5 gmake && make -j7
+fi
diff --git a/skia/font_converter/build/premake5.lua b/skia/font_converter/build/premake5.lua
new file mode 100644
index 0000000..3c50826
--- /dev/null
+++ b/skia/font_converter/build/premake5.lua
@@ -0,0 +1,63 @@
+workspace('rive')
+configurations({ 'debug', 'release' })
+
+project('font_converter')
+kind('ConsoleApp')
+language('C++')
+cppdialect('C++17')
+targetdir('%{cfg.system}/bin/%{cfg.buildcfg}')
+objdir('%{cfg.system}/obj/%{cfg.buildcfg}')
+includedirs({
+    '../include',
+    '../../../include',
+    '../../renderer/include',
+    '../../dependencies/skia',
+    '/usr/local/include',
+    '/usr/include',
+})
+
+if os.host() == 'macosx' then
+    links({
+        'Cocoa.framework',
+        'CoreFoundation.framework',
+        'IOKit.framework',
+        'Security.framework',
+        'bz2',
+        'iconv',
+        'lzma',
+        'skia',
+        'z', -- lib av format
+    })
+else
+    links({ 'm', 'skia', 'z', 'dl', 'fontconfig' })
+end
+
+libdirs({ '../../dependencies/skia/out/static', '/usr/local/lib', '/usr/lib' })
+
+files({ '../src/**.cpp', '../src/**.c' })
+
+buildoptions({ '-Wall', '-fno-rtti' })
+
+filter('configurations:debug')
+defines({ 'DEBUG' })
+symbols('On')
+
+filter('configurations:release')
+defines({ 'RELEASE' })
+defines({ 'NDEBUG' })
+optimize('On')
+
+-- Clean Function --
+newaction({
+    trigger = 'clean',
+    description = 'clean the build',
+    execute = function()
+        print('clean the build...')
+        os.rmdir('./bin')
+        os.rmdir('./obj')
+        os.remove('Makefile')
+        -- no wildcards in os.remove, so use shell
+        os.execute('rm *.make')
+        print('build cleaned')
+    end,
+})
diff --git a/skia/font_converter/include/args.hxx b/skia/font_converter/include/args.hxx
new file mode 100644
index 0000000..bbc521d
--- /dev/null
+++ b/skia/font_converter/include/args.hxx
@@ -0,0 +1,4260 @@
+/* Copyright (c) 2016-2017 Taylor C. Richberger <taywee@gmx.com> and Pavel
+ * Belikov
+ * 
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ * 
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+/** \file args.hxx
+ * \brief this single-header lets you use all of the args functionality
+ *
+ * The important stuff is done inside the args namespace
+ */
+
+#ifndef ARGS_HXX
+#define ARGS_HXX
+
+#include <algorithm>
+#include <iterator>
+#include <exception>
+#include <functional>
+#include <sstream>
+#include <string>
+#include <tuple>
+#include <vector>
+#include <unordered_map>
+#include <unordered_set>
+#include <type_traits>
+
+#ifdef ARGS_TESTNAMESPACE
+namespace argstest
+{
+#else
+
+/** \namespace args
+ * \brief contains all the functionality of the args library
+ */
+namespace args
+{
+#endif
+    /** Getter to grab the value from the argument type.
+     *
+     * If the Get() function of the type returns a reference, so does this, and
+     * the value will be modifiable.
+     */
+    template <typename Option>
+    auto get(Option &option_) -> decltype(option_.Get())
+    {
+        return option_.Get();
+    }
+
+    /** (INTERNAL) Count UTF-8 glyphs
+     *
+     * This is not reliable, and will fail for combinatory glyphs, but it's
+     * good enough here for now.
+     *
+     * \param string The string to count glyphs from
+     * \return The UTF-8 glyphs in the string
+     */
+    inline std::string::size_type Glyphs(const std::string &string_)
+    {
+        std::string::size_type length = 0;
+        for (const char c: string_)
+        {
+            if ((c & 0xc0) != 0x80)
+            {
+                ++length;
+            }
+        }
+        return length;
+    }
+
+    /** (INTERNAL) Wrap a vector of words into a vector of lines
+     *
+     * Empty words are skipped. Word "\n" forces wrapping.
+     *
+     * \param begin The begin iterator
+     * \param end The end iterator
+     * \param width The width of the body
+     * \param firstlinewidth the width of the first line, defaults to the width of the body
+     * \param firstlineindent the indent of the first line, defaults to 0
+     * \return the vector of lines
+     */
+    template <typename It>
+    inline std::vector<std::string> Wrap(It begin,
+                                         It end,
+                                         const std::string::size_type width,
+                                         std::string::size_type firstlinewidth = 0,
+                                         std::string::size_type firstlineindent = 0)
+    {
+        std::vector<std::string> output;
+        std::string line(firstlineindent, ' ');
+        bool empty = true;
+
+        if (firstlinewidth == 0)
+        {
+            firstlinewidth = width;
+        }
+
+        auto currentwidth = firstlinewidth;
+
+        for (auto it = begin; it != end; ++it)
+        {
+            if (it->empty())
+            {
+                continue;
+            }
+
+            if (*it == "\n")
+            {
+                if (!empty)
+                {
+                    output.push_back(line);
+                    line.clear();
+                    empty = true;
+                    currentwidth = width;
+                }
+
+                continue;
+            }
+
+            auto itemsize = Glyphs(*it);
+            if ((line.length() + 1 + itemsize) > currentwidth)
+            {
+                if (!empty)
+                {
+                    output.push_back(line);
+                    line.clear();
+                    empty = true;
+                    currentwidth = width;
+                }
+            }
+
+            if (itemsize > 0)
+            {
+                if (!empty)
+                {
+                    line += ' ';
+                }
+
+                line += *it;
+                empty = false;
+            }
+        }
+
+        if (!empty)
+        {
+            output.push_back(line);
+        }
+
+        return output;
+    }
+
+    namespace detail
+    {
+        template <typename T>
+        std::string Join(const T& array, const std::string &delimiter)
+        {
+            std::string res;
+            for (auto &element : array)
+            {
+                if (!res.empty())
+                {
+                    res += delimiter;
+                }
+
+                res += element;
+            }
+
+            return res;
+        }
+    }
+
+    /** (INTERNAL) Wrap a string into a vector of lines
+     *
+     * This is quick and hacky, but works well enough.  You can specify a
+     * different width for the first line
+     *
+     * \param width The width of the body
+     * \param firstlinewid the width of the first line, defaults to the width of the body
+     * \return the vector of lines
+     */
+    inline std::vector<std::string> Wrap(const std::string &in, const std::string::size_type width, std::string::size_type firstlinewidth = 0)
+    {
+        // Preserve existing line breaks
+        const auto newlineloc = in.find('\n');
+        if (newlineloc != in.npos)
+        {
+            auto first = Wrap(std::string(in, 0, newlineloc), width);
+            auto second = Wrap(std::string(in, newlineloc + 1), width);
+            first.insert(
+                std::end(first),
+                std::make_move_iterator(std::begin(second)),
+                std::make_move_iterator(std::end(second)));
+            return first;
+        }
+
+        std::istringstream stream(in);
+        std::string::size_type indent = 0;
+
+        for (char c : in)
+        {
+            if (!isspace(c))
+            {
+                break;
+            }
+            ++indent;
+        }
+
+        return Wrap(std::istream_iterator<std::string>(stream), std::istream_iterator<std::string>(),
+                    width, firstlinewidth, indent);
+    }
+
+#ifdef ARGS_NOEXCEPT
+    /// Error class, for when ARGS_NOEXCEPT is defined
+    enum class Error
+    {
+        None,
+        Usage,
+        Parse,
+        Validation,
+        Required,
+        Map,
+        Extra,
+        Help,
+        Subparser,
+        Completion,
+    };
+#else
+    /** Base error class
+     */
+    class Error : public std::runtime_error
+    {
+        public:
+            Error(const std::string &problem) : std::runtime_error(problem) {}
+            virtual ~Error() {}
+    };
+
+    /** Errors that occur during usage
+     */
+    class UsageError : public Error
+    {
+        public:
+            UsageError(const std::string &problem) : Error(problem) {}
+            virtual ~UsageError() {}
+    };
+
+    /** Errors that occur during regular parsing
+     */
+    class ParseError : public Error
+    {
+        public:
+            ParseError(const std::string &problem) : Error(problem) {}
+            virtual ~ParseError() {}
+    };
+
+    /** Errors that are detected from group validation after parsing finishes
+     */
+    class ValidationError : public Error
+    {
+        public:
+            ValidationError(const std::string &problem) : Error(problem) {}
+            virtual ~ValidationError() {}
+    };
+
+    /** Errors that when a required flag is omitted
+     */
+    class RequiredError : public ValidationError
+    {
+        public:
+            RequiredError(const std::string &problem) : ValidationError(problem) {}
+            virtual ~RequiredError() {}
+    };
+
+    /** Errors in map lookups
+     */
+    class MapError : public ParseError
+    {
+        public:
+            MapError(const std::string &problem) : ParseError(problem) {}
+            virtual ~MapError() {}
+    };
+
+    /** Error that occurs when a singular flag is specified multiple times
+     */
+    class ExtraError : public ParseError
+    {
+        public:
+            ExtraError(const std::string &problem) : ParseError(problem) {}
+            virtual ~ExtraError() {}
+    };
+
+    /** An exception that indicates that the user has requested help
+     */
+    class Help : public Error
+    {
+        public:
+            Help(const std::string &flag) : Error(flag) {}
+            virtual ~Help() {}
+    };
+
+    /** (INTERNAL) An exception that emulates coroutine-like control flow for subparsers.
+     */
+    class SubparserError : public Error
+    {
+        public:
+            SubparserError() : Error("") {}
+            virtual ~SubparserError() {}
+    };
+
+    /** An exception that contains autocompletion reply
+     */
+    class Completion : public Error
+    {
+        public:
+            Completion(const std::string &flag) : Error(flag) {}
+            virtual ~Completion() {}
+    };
+#endif
+
+    /** A simple unified option type for unified initializer lists for the Matcher class.
+     */
+    struct EitherFlag
+    {
+        const bool isShort;
+        const char shortFlag;
+        const std::string longFlag;
+        EitherFlag(const std::string &flag) : isShort(false), shortFlag(), longFlag(flag) {}
+        EitherFlag(const char *flag) : isShort(false), shortFlag(), longFlag(flag) {}
+        EitherFlag(const char flag) : isShort(true), shortFlag(flag), longFlag() {}
+
+        /** Get just the long flags from an initializer list of EitherFlags
+         */
+        static std::unordered_set<std::string> GetLong(std::initializer_list<EitherFlag> flags)
+        {
+            std::unordered_set<std::string>  longFlags;
+            for (const EitherFlag &flag: flags)
+            {
+                if (!flag.isShort)
+                {
+                    longFlags.insert(flag.longFlag);
+                }
+            }
+            return longFlags;
+        }
+
+        /** Get just the short flags from an initializer list of EitherFlags
+         */
+        static std::unordered_set<char> GetShort(std::initializer_list<EitherFlag> flags)
+        {
+            std::unordered_set<char>  shortFlags;
+            for (const EitherFlag &flag: flags)
+            {
+                if (flag.isShort)
+                {
+                    shortFlags.insert(flag.shortFlag);
+                }
+            }
+            return shortFlags;
+        }
+
+        std::string str() const
+        {
+            return isShort ? std::string(1, shortFlag) : longFlag;
+        }
+
+        std::string str(const std::string &shortPrefix, const std::string &longPrefix) const
+        {
+            return isShort ? shortPrefix + std::string(1, shortFlag) : longPrefix + longFlag;
+        }
+    };
+
+
+
+    /** A class of "matchers", specifying short and flags that can possibly be
+     * matched.
+     *
+     * This is supposed to be constructed and then passed in, not used directly
+     * from user code.
+     */
+    class Matcher
+    {
+        private:
+            const std::unordered_set<char> shortFlags;
+            const std::unordered_set<std::string> longFlags;
+
+        public:
+            /** Specify short and long flags separately as iterators
+             *
+             * ex: `args::Matcher(shortFlags.begin(), shortFlags.end(), longFlags.begin(), longFlags.end())`
+             */
+            template <typename ShortIt, typename LongIt>
+            Matcher(ShortIt shortFlagsStart, ShortIt shortFlagsEnd, LongIt longFlagsStart, LongIt longFlagsEnd) :
+                shortFlags(shortFlagsStart, shortFlagsEnd),
+                longFlags(longFlagsStart, longFlagsEnd)
+            {
+                if (shortFlags.empty() && longFlags.empty())
+                {
+#ifndef ARGS_NOEXCEPT
+                    throw UsageError("empty Matcher");
+#endif
+                }
+            }
+
+#ifdef ARGS_NOEXCEPT
+            /// Only for ARGS_NOEXCEPT
+            Error GetError() const noexcept
+            {
+                return shortFlags.empty() && longFlags.empty() ? Error::Usage : Error::None;
+            }
+#endif
+
+            /** Specify short and long flags separately as iterables
+             *
+             * ex: `args::Matcher(shortFlags, longFlags)`
+             */
+            template <typename Short, typename Long>
+            Matcher(Short &&shortIn, Long &&longIn) :
+                Matcher(std::begin(shortIn), std::end(shortIn), std::begin(longIn), std::end(longIn))
+            {}
+
+            /** Specify a mixed single initializer-list of both short and long flags
+             *
+             * This is the fancy one.  It takes a single initializer list of
+             * any number of any mixed kinds of flags.  Chars are
+             * automatically interpreted as short flags, and strings are
+             * automatically interpreted as long flags:
+             *
+             *     args::Matcher{'a'}
+             *     args::Matcher{"foo"}
+             *     args::Matcher{'h', "help"}
+             *     args::Matcher{"foo", 'f', 'F', "FoO"}
+             */
+            Matcher(std::initializer_list<EitherFlag> in) :
+                Matcher(EitherFlag::GetShort(in), EitherFlag::GetLong(in)) {}
+
+            Matcher(Matcher &&other) : shortFlags(std::move(other.shortFlags)), longFlags(std::move(other.longFlags))
+            {}
+
+            ~Matcher() {}
+
+            /** (INTERNAL) Check if there is a match of a short flag
+             */
+            bool Match(const char flag) const
+            {
+                return shortFlags.find(flag) != shortFlags.end();
+            }
+
+            /** (INTERNAL) Check if there is a match of a long flag
+             */
+            bool Match(const std::string &flag) const
+            {
+                return longFlags.find(flag) != longFlags.end();
+            }
+
+            /** (INTERNAL) Check if there is a match of a flag
+             */
+            bool Match(const EitherFlag &flag) const
+            {
+                return flag.isShort ? Match(flag.shortFlag) : Match(flag.longFlag);
+            }
+
+            /** (INTERNAL) Get all flag strings as a vector, with the prefixes embedded
+             */
+            std::vector<EitherFlag> GetFlagStrings() const
+            {
+                std::vector<EitherFlag> flagStrings;
+                flagStrings.reserve(shortFlags.size() + longFlags.size());
+                for (const char flag: shortFlags)
+                {
+                    flagStrings.emplace_back(flag);
+                }
+                for (const std::string &flag: longFlags)
+                {
+                    flagStrings.emplace_back(flag);
+                }
+                return flagStrings;
+            }
+
+            /** (INTERNAL) Get long flag if it exists or any short flag
+             */
+            EitherFlag GetLongOrAny() const
+            {
+                if (!longFlags.empty())
+                {
+                    return *longFlags.begin();
+                }
+
+                if (!shortFlags.empty())
+                {
+                    return *shortFlags.begin();
+                }
+
+                // should be unreachable
+                return ' ';
+            }
+
+            /** (INTERNAL) Get short flag if it exists or any long flag
+             */
+            EitherFlag GetShortOrAny() const
+            {
+                if (!shortFlags.empty())
+                {
+                    return *shortFlags.begin();
+                }
+
+                if (!longFlags.empty())
+                {
+                    return *longFlags.begin();
+                }
+
+                // should be unreachable
+                return ' ';
+            }
+    };
+
+    /** Attributes for flags.
+     */
+    enum class Options
+    {
+        /** Default options.
+         */
+        None = 0x0,
+
+        /** Flag can't be passed multiple times.
+         */
+        Single = 0x01,
+
+        /** Flag can't be omitted.
+         */
+        Required = 0x02,
+
+        /** Flag is excluded from usage line.
+         */
+        HiddenFromUsage = 0x04,
+
+        /** Flag is excluded from options help.
+         */
+        HiddenFromDescription = 0x08,
+
+        /** Flag is global and can be used in any subcommand.
+         */
+        Global = 0x10,
+
+        /** Flag stops a parser.
+         */
+        KickOut = 0x20,
+
+        /** Flag is excluded from auto completion.
+         */
+        HiddenFromCompletion = 0x40,
+
+        /** Flag is excluded from options help and usage line
+         */
+        Hidden = HiddenFromUsage | HiddenFromDescription | HiddenFromCompletion,
+    };
+
+    inline Options operator | (Options lhs, Options rhs)
+    {
+        return static_cast<Options>(static_cast<int>(lhs) | static_cast<int>(rhs));
+    }
+
+    inline Options operator & (Options lhs, Options rhs)
+    {
+        return static_cast<Options>(static_cast<int>(lhs) & static_cast<int>(rhs));
+    }
+
+    class FlagBase;
+    class PositionalBase;
+    class Command;
+    class ArgumentParser;
+
+    /** A simple structure of parameters for easy user-modifyable help menus
+     */
+    struct HelpParams
+    {
+        /** The width of the help menu
+         */
+        unsigned int width = 80;
+        /** The indent of the program line
+         */
+        unsigned int progindent = 2;
+        /** The indent of the program trailing lines for long parameters
+         */
+        unsigned int progtailindent = 4;
+        /** The indent of the description and epilogs
+         */
+        unsigned int descriptionindent = 4;
+        /** The indent of the flags
+         */
+        unsigned int flagindent = 6;
+        /** The indent of the flag descriptions
+         */
+        unsigned int helpindent = 40;
+        /** The additional indent each group adds
+         */
+        unsigned int eachgroupindent = 2;
+
+        /** The minimum gutter between each flag and its help
+         */
+        unsigned int gutter = 1;
+
+        /** Show the terminator when both options and positional parameters are present
+         */
+        bool showTerminator = true;
+
+        /** Show the {OPTIONS} on the prog line when this is true
+         */
+        bool showProglineOptions = true;
+
+        /** Show the positionals on the prog line when this is true
+         */
+        bool showProglinePositionals = true;
+
+        /** The prefix for short flags
+         */
+        std::string shortPrefix;
+
+        /** The prefix for long flags
+         */
+        std::string longPrefix;
+
+        /** The separator for short flags
+         */
+        std::string shortSeparator;
+
+        /** The separator for long flags
+         */
+        std::string longSeparator;
+
+        /** The program name for help generation
+         */
+        std::string programName;
+
+        /** Show command's flags
+         */
+        bool showCommandChildren = false;
+
+        /** Show command's descriptions and epilog
+         */
+        bool showCommandFullHelp = false;
+
+        /** The postfix for progline when showProglineOptions is true and command has any flags
+         */
+        std::string proglineOptions = "{OPTIONS}";
+
+        /** The prefix for progline when command has any subcommands
+         */
+        std::string proglineCommand = "COMMAND";
+
+        /** The prefix for progline value
+         */
+        std::string proglineValueOpen = " <";
+
+        /** The postfix for progline value
+         */
+        std::string proglineValueClose = ">";
+
+        /** The prefix for progline required argument
+         */
+        std::string proglineRequiredOpen = "";
+
+        /** The postfix for progline required argument
+         */
+        std::string proglineRequiredClose = "";
+
+        /** The prefix for progline non-required argument
+         */
+        std::string proglineNonrequiredOpen = "[";
+
+        /** The postfix for progline non-required argument
+         */
+        std::string proglineNonrequiredClose = "]";
+
+        /** Show flags in program line
+         */
+        bool proglineShowFlags = false;
+
+        /** Use short flags in program lines when possible
+         */
+        bool proglinePreferShortFlags = false;
+
+        /** Program line prefix
+         */
+        std::string usageString;
+
+        /** String shown in help before flags descriptions
+         */
+        std::string optionsString = "OPTIONS:";
+
+        /** Display value name after all the long and short flags
+         */
+        bool useValueNameOnce = false;
+
+        /** Show value name
+         */
+        bool showValueName = true;
+
+        /** Add newline before flag description
+         */
+        bool addNewlineBeforeDescription = false;
+
+        /** The prefix for option value
+         */
+        std::string valueOpen = "[";
+
+        /** The postfix for option value
+         */
+        std::string valueClose = "]";
+
+        /** Add choices to argument description
+         */
+        bool addChoices = false;
+
+        /** The prefix for choices
+         */
+        std::string choiceString = "\nOne of: ";
+
+        /** Add default values to argument description
+         */
+        bool addDefault = false;
+
+        /** The prefix for default values
+         */
+        std::string defaultString = "\nDefault: ";
+    };
+
+    /** A number of arguments which can be consumed by an option.
+     *
+     * Represents a closed interval [min, max].
+     */
+    struct Nargs
+    {
+        const size_t min;
+        const size_t max;
+
+        Nargs(size_t min_, size_t max_) : min{min_}, max{max_}
+        {
+#ifndef ARGS_NOEXCEPT
+            if (max < min)
+            {
+                throw UsageError("Nargs: max > min");
+            }
+#endif
+        }
+
+        Nargs(size_t num_) : min{num_}, max{num_}
+        {
+        }
+
+        friend bool operator == (const Nargs &lhs, const Nargs &rhs)
+        {
+            return lhs.min == rhs.min && lhs.max == rhs.max;
+        }
+
+        friend bool operator != (const Nargs &lhs, const Nargs &rhs)
+        {
+            return !(lhs == rhs);
+        }
+    };
+
+    /** Base class for all match types
+     */
+    class Base
+    {
+        private:
+            Options options = {};
+
+        protected:
+            bool matched = false;
+            const std::string help;
+#ifdef ARGS_NOEXCEPT
+            /// Only for ARGS_NOEXCEPT
+            mutable Error error = Error::None;
+            mutable std::string errorMsg;
+#endif
+
+        public:
+            Base(const std::string &help_, Options options_ = {}) : options(options_), help(help_) {}
+            virtual ~Base() {}
+
+            Options GetOptions() const noexcept
+            {
+                return options;
+            }
+
+            bool IsRequired() const noexcept
+            {
+                return (GetOptions() & Options::Required) != Options::None;
+            }
+
+            virtual bool Matched() const noexcept
+            {
+                return matched;
+            }
+
+            virtual void Validate(const std::string &, const std::string &) const
+            {
+            }
+
+            operator bool() const noexcept
+            {
+                return Matched();
+            }
+
+            virtual std::vector<std::tuple<std::string, std::string, unsigned>> GetDescription(const HelpParams &, const unsigned indentLevel) const
+            {
+                std::tuple<std::string, std::string, unsigned> description;
+                std::get<1>(description) = help;
+                std::get<2>(description) = indentLevel;
+                return { std::move(description) };
+            }
+
+            virtual std::vector<Command*> GetCommands()
+            {
+                return {};
+            }
+
+            virtual bool IsGroup() const
+            {
+                return false;
+            }
+
+            virtual FlagBase *Match(const EitherFlag &)
+            {
+                return nullptr;
+            }
+
+            virtual PositionalBase *GetNextPositional()
+            {
+                return nullptr;
+            }
+
+            virtual std::vector<FlagBase*> GetAllFlags()
+            {
+                return {};
+            }
+
+            virtual bool HasFlag() const
+            {
+                return false;
+            }
+
+            virtual bool HasPositional() const
+            {
+                return false;
+            }
+
+            virtual bool HasCommand() const
+            {
+                return false;
+            }
+
+            virtual std::vector<std::string> GetProgramLine(const HelpParams &) const
+            {
+                return {};
+            }
+
+            /// Sets a kick-out value for building subparsers
+            void KickOut(bool kickout_) noexcept
+            {
+                if (kickout_)
+                {
+                    options = options | Options::KickOut;
+                }
+                else
+                {
+                    options = static_cast<Options>(static_cast<int>(options) & ~static_cast<int>(Options::KickOut));
+                }
+            }
+
+            /// Gets the kick-out value for building subparsers
+            bool KickOut() const noexcept
+            {
+                return (options & Options::KickOut) != Options::None;
+            }
+
+            virtual void Reset() noexcept
+            {
+                matched = false;
+#ifdef ARGS_NOEXCEPT
+                error = Error::None;
+                errorMsg.clear();
+#endif
+            }
+
+#ifdef ARGS_NOEXCEPT
+            /// Only for ARGS_NOEXCEPT
+            virtual Error GetError() const
+            {
+                return error;
+            }
+
+            /// Only for ARGS_NOEXCEPT
+            std::string GetErrorMsg() const
+            {
+                return errorMsg;
+            }
+#endif
+    };
+
+    /** Base class for all match types that have a name
+     */
+    class NamedBase : public Base
+    {
+        protected:
+            const std::string name;
+            bool kickout = false;
+            std::string defaultString;
+            bool defaultStringManual = false;
+            std::vector<std::string> choicesStrings;
+            bool choicesStringManual = false;
+
+            virtual std::string GetDefaultString(const HelpParams&) const { return {}; }
+
+            virtual std::vector<std::string> GetChoicesStrings(const HelpParams&) const { return {}; }
+
+            virtual std::string GetNameString(const HelpParams&) const { return Name(); }
+
+            void AddDescriptionPostfix(std::string &dest, const bool isManual, const std::string &manual, bool isGenerated, const std::string &generated, const std::string &str) const
+            {
+                if (isManual && !manual.empty())
+                {
+                    dest += str;
+                    dest += manual;
+                }
+                else if (!isManual && isGenerated && !generated.empty())
+                {
+                    dest += str;
+                    dest += generated;
+                }
+            }
+
+        public:
+            NamedBase(const std::string &name_, const std::string &help_, Options options_ = {}) : Base(help_, options_), name(name_) {}
+            virtual ~NamedBase() {}
+
+            /** Sets default value string that will be added to argument description.
+             *  Use empty string to disable it for this argument.
+             */
+            void HelpDefault(const std::string &str)
+            {
+                defaultStringManual = true;
+                defaultString = str;
+            }
+
+            /** Gets default value string that will be added to argument description.
+             */
+            std::string HelpDefault(const HelpParams &params) const
+            {
+                return defaultStringManual ? defaultString : GetDefaultString(params);
+            }
+
+            /** Sets choices strings that will be added to argument description.
+             *  Use empty vector to disable it for this argument.
+             */
+            void HelpChoices(const std::vector<std::string> &array)
+            {
+                choicesStringManual = true;
+                choicesStrings = array;
+            }
+
+            /** Gets choices strings that will be added to argument description.
+             */
+            std::vector<std::string> HelpChoices(const HelpParams &params) const
+            {
+                return choicesStringManual ? choicesStrings : GetChoicesStrings(params);
+            }
+
+            virtual std::vector<std::tuple<std::string, std::string, unsigned>> GetDescription(const HelpParams &params, const unsigned indentLevel) const override
+            {
+                std::tuple<std::string, std::string, unsigned> description;
+                std::get<0>(description) = GetNameString(params);
+                std::get<1>(description) = help;
+                std::get<2>(description) = indentLevel;
+
+                AddDescriptionPostfix(std::get<1>(description), choicesStringManual, detail::Join(choicesStrings, ", "), params.addChoices, detail::Join(GetChoicesStrings(params), ", "), params.choiceString);
+                AddDescriptionPostfix(std::get<1>(description), defaultStringManual, defaultString, params.addDefault, GetDefaultString(params), params.defaultString);
+
+                return { std::move(description) };
+            }
+
+            virtual std::string Name() const
+            {
+                return name;
+            }
+    };
+
+    namespace detail
+    {
+        template <typename T, typename = int>
+        struct IsConvertableToString : std::false_type {};
+
+        template <typename T>
+        struct IsConvertableToString<T, decltype(std::declval<std::ostringstream&>() << std::declval<T>(), int())> : std::true_type {};
+
+        template <typename T>
+        typename std::enable_if<IsConvertableToString<T>::value, std::string>::type
+        ToString(const T &value)
+        {
+            std::ostringstream s;
+            s << value;
+            return s.str();
+        }
+
+        template <typename T>
+        typename std::enable_if<!IsConvertableToString<T>::value, std::string>::type
+        ToString(const T &)
+        {
+            return {};
+        }
+
+        template <typename T>
+        std::vector<std::string> MapKeysToStrings(const T &map)
+        {
+            std::vector<std::string> res;
+            using K = typename std::decay<decltype(std::begin(map)->first)>::type;
+            if (IsConvertableToString<K>::value)
+            {
+                for (const auto &p : map)
+                {
+                    res.push_back(detail::ToString(p.first));
+                }
+
+                std::sort(res.begin(), res.end());
+            }
+            return res;
+        }
+    }
+
+    /** Base class for all flag options
+     */
+    class FlagBase : public NamedBase
+    {
+        protected:
+            const Matcher matcher;
+
+            virtual std::string GetNameString(const HelpParams &params) const override
+            {
+                const std::string postfix = !params.showValueName || NumberOfArguments() == 0 ? std::string() : Name();
+                std::string flags;
+                const auto flagStrings = matcher.GetFlagStrings();
+                const bool useValueNameOnce = flagStrings.size() == 1 ? false : params.useValueNameOnce;
+                for (auto it = flagStrings.begin(); it != flagStrings.end(); ++it)
+                {
+                    auto &flag = *it;
+                    if (it != flagStrings.begin())
+                    {
+                        flags += ", ";
+                    }
+
+                    flags += flag.isShort ? params.shortPrefix : params.longPrefix;
+                    flags += flag.str();
+
+                    if (!postfix.empty() && (!useValueNameOnce || it + 1 == flagStrings.end()))
+                    {
+                        flags += flag.isShort ? params.shortSeparator : params.longSeparator;
+                        flags += params.valueOpen + postfix + params.valueClose;
+                    }
+                }
+
+                return flags;
+            }
+
+        public:
+            FlagBase(const std::string &name_, const std::string &help_, Matcher &&matcher_, const bool extraError_ = false) : NamedBase(name_, help_, extraError_ ? Options::Single : Options()), matcher(std::move(matcher_)) {}
+
+            FlagBase(const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_) : NamedBase(name_, help_, options_), matcher(std::move(matcher_)) {}
+
+            virtual ~FlagBase() {}
+
+            virtual FlagBase *Match(const EitherFlag &flag) override
+            {
+                if (matcher.Match(flag))
+                {
+                    if ((GetOptions() & Options::Single) != Options::None && matched)
+                    {
+                        std::ostringstream problem;
+                        problem << "Flag '" << flag.str() << "' was passed multiple times, but is only allowed to be passed once";
+#ifdef ARGS_NOEXCEPT
+                        error = Error::Extra;
+                        errorMsg = problem.str();
+#else
+                        throw ExtraError(problem.str());
+#endif
+                    }
+                    matched = true;
+                    return this;
+                }
+                return nullptr;
+            }
+
+            virtual std::vector<FlagBase*> GetAllFlags() override
+            {
+                return { this };
+            }
+
+            const Matcher &GetMatcher() const
+            {
+                return matcher;
+            }
+
+            virtual void Validate(const std::string &shortPrefix, const std::string &longPrefix) const override
+            {
+                if (!Matched() && IsRequired())
+                {
+                        std::ostringstream problem;
+                        problem << "Flag '" << matcher.GetLongOrAny().str(shortPrefix, longPrefix) << "' is required";
+#ifdef ARGS_NOEXCEPT
+                        error = Error::Required;
+                        errorMsg = problem.str();
+#else
+                        throw RequiredError(problem.str());
+#endif
+                }
+            }
+
+            virtual std::vector<std::string> GetProgramLine(const HelpParams &params) const override
+            {
+                if (!params.proglineShowFlags)
+                {
+                    return {};
+                }
+
+                const std::string postfix = NumberOfArguments() == 0 ? std::string() : Name();
+                const EitherFlag flag = params.proglinePreferShortFlags ? matcher.GetShortOrAny() : matcher.GetLongOrAny();
+                std::string res = flag.str(params.shortPrefix, params.longPrefix);
+                if (!postfix.empty())
+                {
+                    res += params.proglineValueOpen + postfix + params.proglineValueClose;
+                }
+
+                return { IsRequired() ? params.proglineRequiredOpen + res + params.proglineRequiredClose
+                                      : params.proglineNonrequiredOpen + res + params.proglineNonrequiredClose };
+            }
+
+            virtual bool HasFlag() const override
+            {
+                return true;
+            }
+
+#ifdef ARGS_NOEXCEPT
+            /// Only for ARGS_NOEXCEPT
+            virtual Error GetError() const override
+            {
+                const auto nargs = NumberOfArguments();
+                if (nargs.min > nargs.max)
+                {
+                    return Error::Usage;
+                }
+
+                const auto matcherError = matcher.GetError();
+                if (matcherError != Error::None)
+                {
+                    return matcherError;
+                }
+
+                return error;
+            }
+#endif
+
+            /** Defines how many values can be consumed by this option.
+             *
+             * \return closed interval [min, max]
+             */
+            virtual Nargs NumberOfArguments() const noexcept = 0;
+
+            /** Parse values of this option.
+             *
+             * \param value Vector of values. It's size must be in NumberOfArguments() interval.
+             */
+            virtual void ParseValue(const std::vector<std::string> &value) = 0;
+    };
+
+    /** Base class for value-accepting flag options
+     */
+    class ValueFlagBase : public FlagBase
+    {
+        public:
+            ValueFlagBase(const std::string &name_, const std::string &help_, Matcher &&matcher_, const bool extraError_ = false) : FlagBase(name_, help_, std::move(matcher_), extraError_) {}
+            ValueFlagBase(const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_) : FlagBase(name_, help_, std::move(matcher_), options_) {}
+            virtual ~ValueFlagBase() {}
+
+            virtual Nargs NumberOfArguments() const noexcept override
+            {
+                return 1;
+            }
+    };
+
+    class CompletionFlag : public ValueFlagBase
+    {
+        public:
+            std::vector<std::string> reply;
+            size_t cword = 0;
+            std::string syntax;
+
+            template <typename GroupClass>
+            CompletionFlag(GroupClass &group_, Matcher &&matcher_): ValueFlagBase("completion", "completion flag", std::move(matcher_), Options::Hidden)
+            {
+                group_.AddCompletion(*this);
+            }
+
+            virtual ~CompletionFlag() {}
+
+            virtual Nargs NumberOfArguments() const noexcept override
+            {
+                return 2;
+            }
+
+            virtual void ParseValue(const std::vector<std::string> &value_) override
+            {
+                syntax = value_.at(0);
+                std::istringstream(value_.at(1)) >> cword;
+            }
+
+            /** Get the completion reply
+             */
+            std::string Get() noexcept
+            {
+                return detail::Join(reply, "\n");
+            }
+
+            virtual void Reset() noexcept override
+            {
+                ValueFlagBase::Reset();
+                cword = 0;
+                syntax.clear();
+                reply.clear();
+            }
+    };
+
+
+    /** Base class for positional options
+     */
+    class PositionalBase : public NamedBase
+    {
+        protected:
+            bool ready;
+
+        public:
+            PositionalBase(const std::string &name_, const std::string &help_, Options options_ = {}) : NamedBase(name_, help_, options_), ready(true) {}
+            virtual ~PositionalBase() {}
+
+            bool Ready()
+            {
+                return ready;
+            }
+
+            virtual void ParseValue(const std::string &value_) = 0;
+
+            virtual void Reset() noexcept override
+            {
+                matched = false;
+                ready = true;
+#ifdef ARGS_NOEXCEPT
+                error = Error::None;
+                errorMsg.clear();
+#endif
+            }
+
+            virtual PositionalBase *GetNextPositional() override
+            {
+                return Ready() ? this : nullptr;
+            }
+
+            virtual bool HasPositional() const override
+            {
+                return true;
+            }
+
+            virtual std::vector<std::string> GetProgramLine(const HelpParams &params) const override
+            {
+                return { IsRequired() ? params.proglineRequiredOpen + Name() + params.proglineRequiredClose
+                                      : params.proglineNonrequiredOpen + Name() + params.proglineNonrequiredClose };
+            }
+
+            virtual void Validate(const std::string &, const std::string &) const override
+            {
+                if (IsRequired() && !Matched())
+                {
+                    std::ostringstream problem;
+                    problem << "Option '" << Name() << "' is required";
+#ifdef ARGS_NOEXCEPT
+                    error = Error::Required;
+                    errorMsg = problem.str();
+#else
+                    throw RequiredError(problem.str());
+#endif
+                }
+            }
+    };
+
+    /** Class for all kinds of validating groups, including ArgumentParser
+     */
+    class Group : public Base
+    {
+        private:
+            std::vector<Base*> children;
+            std::function<bool(const Group &)> validator;
+
+        public:
+            /** Default validators
+             */
+            struct Validators
+            {
+                static bool Xor(const Group &group)
+                {
+                    return group.MatchedChildren() == 1;
+                }
+
+                static bool AtLeastOne(const Group &group)
+                {
+                    return group.MatchedChildren() >= 1;
+                }
+
+                static bool AtMostOne(const Group &group)
+                {
+                    return group.MatchedChildren() <= 1;
+                }
+
+                static bool All(const Group &group)
+                {
+                    return group.Children().size() == group.MatchedChildren();
+                }
+
+                static bool AllOrNone(const Group &group)
+                {
+                    return (All(group) || None(group));
+                }
+
+                static bool AllChildGroups(const Group &group)
+                {
+                    return std::none_of(std::begin(group.Children()), std::end(group.Children()), [](const Base* child) -> bool {
+                            return child->IsGroup() && !child->Matched();
+                            });
+                }
+
+                static bool DontCare(const Group &)
+                {
+                    return true;
+                }
+
+                static bool CareTooMuch(const Group &)
+                {
+                    return false;
+                }
+
+                static bool None(const Group &group)
+                {
+                    return group.MatchedChildren() == 0;
+                }
+            };
+            /// If help is empty, this group will not be printed in help output
+            Group(const std::string &help_ = std::string(), const std::function<bool(const Group &)> &validator_ = Validators::DontCare, Options options_ = {}) : Base(help_, options_), validator(validator_) {}
+            /// If help is empty, this group will not be printed in help output
+            Group(Group &group_, const std::string &help_ = std::string(), const std::function<bool(const Group &)> &validator_ = Validators::DontCare, Options options_ = {}) : Base(help_, options_), validator(validator_)
+            {
+                group_.Add(*this);
+            }
+            virtual ~Group() {}
+
+            /** Append a child to this Group.
+             */
+            void Add(Base &child)
+            {
+                children.emplace_back(&child);
+            }
+
+            /** Get all this group's children
+             */
+            const std::vector<Base *> &Children() const
+            {
+                return children;
+            }
+
+            /** Return the first FlagBase that matches flag, or nullptr
+             *
+             * \param flag The flag with prefixes stripped
+             * \return the first matching FlagBase pointer, or nullptr if there is no match
+             */
+            virtual FlagBase *Match(const EitherFlag &flag) override
+            {
+                for (Base *child: Children())
+                {
+                    if (FlagBase *match = child->Match(flag))
+                    {
+                        return match;
+                    }
+                }
+                return nullptr;
+            }
+
+            virtual std::vector<FlagBase*> GetAllFlags() override
+            {
+                std::vector<FlagBase*> res;
+                for (Base *child: Children())
+                {
+                    auto childRes = child->GetAllFlags();
+                    res.insert(res.end(), childRes.begin(), childRes.end());
+                }
+                return res;
+            }
+
+            virtual void Validate(const std::string &shortPrefix, const std::string &longPrefix) const override
+            {
+                for (Base *child: Children())
+                {
+                    child->Validate(shortPrefix, longPrefix);
+                }
+            }
+
+            /** Get the next ready positional, or nullptr if there is none
+             *
+             * \return the first ready PositionalBase pointer, or nullptr if there is no match
+             */
+            virtual PositionalBase *GetNextPositional() override
+            {
+                for (Base *child: Children())
+                {
+                    if (auto next = child->GetNextPositional())
+                    {
+                        return next;
+                    }
+                }
+                return nullptr;
+            }
+
+            /** Get whether this has any FlagBase children
+             *
+             * \return Whether or not there are any FlagBase children
+             */
+            virtual bool HasFlag() const override
+            {
+                return std::any_of(Children().begin(), Children().end(), [](Base *child) { return child->HasFlag(); });
+            }
+
+            /** Get whether this has any PositionalBase children
+             *
+             * \return Whether or not there are any PositionalBase children
+             */
+            virtual bool HasPositional() const override
+            {
+                return std::any_of(Children().begin(), Children().end(), [](Base *child) { return child->HasPositional(); });
+            }
+
+            /** Get whether this has any Command children
+             *
+             * \return Whether or not there are any Command children
+             */
+            virtual bool HasCommand() const override
+            {
+                return std::any_of(Children().begin(), Children().end(), [](Base *child) { return child->HasCommand(); });
+            }
+
+            /** Count the number of matched children this group has
+             */
+            std::vector<Base *>::size_type MatchedChildren() const
+            {
+                return std::count_if(std::begin(Children()), std::end(Children()), [](const Base *child){return child->Matched();});
+            }
+
+            /** Whether or not this group matches validation
+             */
+            virtual bool Matched() const noexcept override
+            {
+                return validator(*this);
+            }
+
+            /** Get validation
+             */
+            bool Get() const
+            {
+                return Matched();
+            }
+
+            /** Get all the child descriptions for help generation
+             */
+            virtual std::vector<std::tuple<std::string, std::string, unsigned>> GetDescription(const HelpParams &params, const unsigned int indent) const override
+            {
+                std::vector<std::tuple<std::string, std::string, unsigned int>> descriptions;
+
+                // Push that group description on the back if not empty
+                unsigned addindent = 0;
+                if (!help.empty())
+                {
+                    descriptions.emplace_back(help, "", indent);
+                    addindent = 1;
+                }
+
+                for (Base *child: Children())
+                {
+                    if ((child->GetOptions() & Options::HiddenFromDescription) != Options::None)
+                    {
+                        continue;
+                    }
+
+                    auto groupDescriptions = child->GetDescription(params, indent + addindent);
+                    descriptions.insert(
+                        std::end(descriptions),
+                        std::make_move_iterator(std::begin(groupDescriptions)),
+                        std::make_move_iterator(std::end(groupDescriptions)));
+                }
+                return descriptions;
+            }
+
+            /** Get the names of positional parameters
+             */
+            virtual std::vector<std::string> GetProgramLine(const HelpParams &params) const override
+            {
+                std::vector <std::string> names;
+                for (Base *child: Children())
+                {
+                    if ((child->GetOptions() & Options::HiddenFromUsage) != Options::None)
+                    {
+                        continue;
+                    }
+
+                    auto groupNames = child->GetProgramLine(params);
+                    names.insert(
+                        std::end(names),
+                        std::make_move_iterator(std::begin(groupNames)),
+                        std::make_move_iterator(std::end(groupNames)));
+                }
+                return names;
+            }
+
+            virtual std::vector<Command*> GetCommands() override
+            {
+                std::vector<Command*> res;
+                for (const auto &child : Children())
+                {
+                    auto subparsers = child->GetCommands();
+                    res.insert(std::end(res), std::begin(subparsers), std::end(subparsers));
+                }
+                return res;
+            }
+
+            virtual bool IsGroup() const override
+            {
+                return true;
+            }
+
+            virtual void Reset() noexcept override
+            {
+                Base::Reset();
+
+                for (auto &child: Children())
+                {
+                    child->Reset();
+                }
+#ifdef ARGS_NOEXCEPT
+                error = Error::None;
+                errorMsg.clear();
+#endif
+            }
+
+#ifdef ARGS_NOEXCEPT
+            /// Only for ARGS_NOEXCEPT
+            virtual Error GetError() const override
+            {
+                if (error != Error::None)
+                {
+                    return error;
+                }
+
+                auto it = std::find_if(Children().begin(), Children().end(), [](const Base *child){return child->GetError() != Error::None;});
+                if (it == Children().end())
+                {
+                    return Error::None;
+                } else
+                {
+                    return (*it)->GetError();
+                }
+            }
+#endif
+
+    };
+
+    /** Class for using global options in ArgumentParser.
+     */
+    class GlobalOptions : public Group
+    {
+        public:
+            GlobalOptions(Group &base, Base &options_) : Group(base, {}, Group::Validators::DontCare, Options::Global)
+            {
+                Add(options_);
+            }
+    };
+
+    /** Utility class for building subparsers with coroutines/callbacks.
+     *
+     * Brief example:
+     * \code
+     * Command command(argumentParser, "command", "my command", [](args::Subparser &s)
+     * {
+     *      // your command flags/positionals
+     *      s.Parse(); //required
+     *      //your command code
+     * });
+     * \endcode
+     *
+     * For ARGS_NOEXCEPT mode don't forget to check `s.GetError()` after `s.Parse()`
+     * and return if it isn't equals to args::Error::None.
+     *
+     * \sa Command
+     */
+    class Subparser : public Group
+    {
+        private:
+            std::vector<std::string> args;
+            std::vector<std::string> kicked;
+            ArgumentParser *parser = nullptr;
+            const HelpParams &helpParams;
+            const Command &command;
+            bool isParsed = false;
+
+        public:
+            Subparser(std::vector<std::string> args_, ArgumentParser &parser_, const Command &command_, const HelpParams &helpParams_)
+                : args(std::move(args_)), parser(&parser_), helpParams(helpParams_), command(command_)
+            {
+            }
+
+            Subparser(const Command &command_, const HelpParams &helpParams_) : helpParams(helpParams_), command(command_)
+            {
+            }
+
+            Subparser(const Subparser&) = delete;
+            Subparser(Subparser&&) = delete;
+            Subparser &operator = (const Subparser&) = delete;
+            Subparser &operator = (Subparser&&) = delete;
+
+            const Command &GetCommand()
+            {
+                return command;
+            }
+
+            /** (INTERNAL) Determines whether Parse was called or not.
+             */
+            bool IsParsed() const
+            {
+                return isParsed;
+            }
+
+            /** Continue parsing arguments for new command.
+             */
+            void Parse();
+
+            /** Returns a vector of kicked out arguments.
+             *
+             * \sa Base::KickOut
+             */
+            const std::vector<std::string> &KickedOut() const noexcept
+            {
+                return kicked;
+            }
+    };
+
+    /** Main class for building subparsers.
+     *
+     * /sa Subparser
+     */
+    class Command : public Group
+    {
+        private:
+            friend class Subparser;
+
+            std::string name;
+            std::string help;
+            std::string description;
+            std::string epilog;
+            std::string proglinePostfix;
+
+            std::function<void(Subparser&)> parserCoroutine;
+            bool commandIsRequired = true;
+            Command *selectedCommand = nullptr;
+
+            mutable std::vector<std::tuple<std::string, std::string, unsigned>> subparserDescription;
+            mutable std::vector<std::string> subparserProgramLine;
+            mutable bool subparserHasFlag = false;
+            mutable bool subparserHasPositional = false;
+            mutable bool subparserHasCommand = false;
+#ifdef ARGS_NOEXCEPT
+            mutable Error subparserError = Error::None;
+#endif
+            mutable Subparser *subparser = nullptr;
+
+        protected:
+
+            class RaiiSubparser
+            {
+                public:
+                    RaiiSubparser(ArgumentParser &parser_, std::vector<std::string> args_);
+                    RaiiSubparser(const Command &command_, const HelpParams &params_);
+
+                    ~RaiiSubparser()
+                    {
+                        command.subparser = oldSubparser;
+                    }
+
+                    Subparser &Parser()
+                    {
+                        return parser;
+                    }
+
+                private:
+                    const Command &command;
+                    Subparser parser;
+                    Subparser *oldSubparser;
+            };
+
+            Command() = default;
+
+            std::function<void(Subparser&)> &GetCoroutine()
+            {
+                return selectedCommand != nullptr ? selectedCommand->GetCoroutine() : parserCoroutine;
+            }
+
+            Command &SelectedCommand()
+            {
+                Command *res = this;
+                while (res->selectedCommand != nullptr)
+                {
+                    res = res->selectedCommand;
+                }
+
+                return *res;
+            }
+
+            const Command &SelectedCommand() const
+            {
+                const Command *res = this;
+                while (res->selectedCommand != nullptr)
+                {
+                    res = res->selectedCommand;
+                }
+
+                return *res;
+            }
+
+            void UpdateSubparserHelp(const HelpParams &params) const
+            {
+                if (parserCoroutine)
+                {
+                    RaiiSubparser coro(*this, params);
+#ifndef ARGS_NOEXCEPT
+                    try
+                    {
+                        parserCoroutine(coro.Parser());
+                    }
+                    catch (args::SubparserError&)
+                    {
+                    }
+#else
+                    parserCoroutine(coro.Parser());
+#endif
+                }
+            }
+
+        public:
+            Command(Group &base_, std::string name_, std::string help_, std::function<void(Subparser&)> coroutine_ = {})
+                : name(std::move(name_)), help(std::move(help_)), parserCoroutine(std::move(coroutine_))
+            {
+                base_.Add(*this);
+            }
+
+            /** The description that appears on the prog line after options
+             */
+            const std::string &ProglinePostfix() const
+            { return proglinePostfix; }
+
+            /** The description that appears on the prog line after options
+             */
+            void ProglinePostfix(const std::string &proglinePostfix_)
+            { this->proglinePostfix = proglinePostfix_; }
+
+            /** The description that appears above options
+             */
+            const std::string &Description() const
+            { return description; }
+            /** The description that appears above options
+             */
+
+            void Description(const std::string &description_)
+            { this->description = description_; }
+
+            /** The description that appears below options
+             */
+            const std::string &Epilog() const
+            { return epilog; }
+
+            /** The description that appears below options
+             */
+            void Epilog(const std::string &epilog_)
+            { this->epilog = epilog_; }
+
+            /** The name of command
+             */
+            const std::string &Name() const
+            { return name; }
+
+            /** The description of command
+             */
+            const std::string &Help() const
+            { return help; }
+
+            /** If value is true, parser will fail if no command was parsed.
+             *
+             * Default: true.
+             */
+            void RequireCommand(bool value)
+            { commandIsRequired = value; }
+
+            virtual bool IsGroup() const override
+            { return false; }
+
+            virtual bool Matched() const noexcept override
+            { return Base::Matched(); }
+
+            operator bool() const noexcept
+            { return Matched(); }
+
+            void Match() noexcept
+            { matched = true; }
+
+            void SelectCommand(Command *c) noexcept
+            {
+                selectedCommand = c;
+
+                if (c != nullptr)
+                {
+                    c->Match();
+                }
+            }
+
+            virtual FlagBase *Match(const EitherFlag &flag) override
+            {
+                if (selectedCommand != nullptr)
+                {
+                    if (auto *res = selectedCommand->Match(flag))
+                    {
+                        return res;
+                    }
+
+                    for (auto *child: Children())
+                    {
+                        if ((child->GetOptions() & Options::Global) != Options::None)
+                        {
+                            if (auto *res = child->Match(flag))
+                            {
+                                return res;
+                            }
+                        }
+                    }
+
+                    return nullptr;
+                }
+
+                if (subparser != nullptr)
+                {
+                    return subparser->Match(flag);
+                }
+
+                return Matched() ? Group::Match(flag) : nullptr;
+            }
+
+            virtual std::vector<FlagBase*> GetAllFlags() override
+            {
+                std::vector<FlagBase*> res;
+
+                if (!Matched())
+                {
+                    return res;
+                }
+
+                for (auto *child: Children())
+                {
+                    if (selectedCommand == nullptr || (child->GetOptions() & Options::Global) != Options::None)
+                    {
+                        auto childFlags = child->GetAllFlags();
+                        res.insert(res.end(), childFlags.begin(), childFlags.end());
+                    }
+                }
+
+                if (selectedCommand != nullptr)
+                {
+                    auto childFlags = selectedCommand->GetAllFlags();
+                    res.insert(res.end(), childFlags.begin(), childFlags.end());
+                }
+
+                if (subparser != nullptr)
+                {
+                    auto childFlags = subparser->GetAllFlags();
+                    res.insert(res.end(), childFlags.begin(), childFlags.end());
+                }
+
+                return res;
+            }
+
+            virtual PositionalBase *GetNextPositional() override
+            {
+                if (selectedCommand != nullptr)
+                {
+                    if (auto *res = selectedCommand->GetNextPositional())
+                    {
+                        return res;
+                    }
+
+                    for (auto *child: Children())
+                    {
+                        if ((child->GetOptions() & Options::Global) != Options::None)
+                        {
+                            if (auto *res = child->GetNextPositional())
+                            {
+                                return res;
+                            }
+                        }
+                    }
+
+                    return nullptr;
+                }
+
+                if (subparser != nullptr)
+                {
+                    return subparser->GetNextPositional();
+                }
+
+                return Matched() ? Group::GetNextPositional() : nullptr;
+            }
+
+            virtual bool HasFlag() const override
+            {
+                return subparserHasFlag || Group::HasFlag();
+            }
+
+            virtual bool HasPositional() const override
+            {
+                return subparserHasPositional || Group::HasPositional();
+            }
+
+            virtual bool HasCommand() const override
+            {
+                return true;
+            }
+
+            std::vector<std::string> GetCommandProgramLine(const HelpParams &params) const
+            {
+                UpdateSubparserHelp(params);
+
+                auto res = Group::GetProgramLine(params);
+                res.insert(res.end(), subparserProgramLine.begin(), subparserProgramLine.end());
+
+                if (!params.proglineCommand.empty() && (Group::HasCommand() || subparserHasCommand))
+                {
+                    res.insert(res.begin(), commandIsRequired ? params.proglineCommand : "[" + params.proglineCommand + "]");
+                }
+
+                if (!Name().empty())
+                {
+                    res.insert(res.begin(), Name());
+                }
+
+                if ((subparserHasFlag || Group::HasFlag()) && params.showProglineOptions && !params.proglineShowFlags)
+                {
+                    res.push_back(params.proglineOptions);
+                }
+
+                if (!ProglinePostfix().empty())
+                {
+                    std::string line;
+                    for (char c : ProglinePostfix())
+                    {
+                        if (isspace(c))
+                        {
+                            if (!line.empty())
+                            {
+                                res.push_back(line);
+                                line.clear();
+                            }
+
+                            if (c == '\n')
+                            {
+                                res.push_back("\n");
+                            }
+                        }
+                        else
+                        {
+                            line += c;
+                        }
+                    }
+
+                    if (!line.empty())
+                    {
+                        res.push_back(line);
+                    }
+                }
+
+                return res;
+            }
+
+            virtual std::vector<std::string> GetProgramLine(const HelpParams &params) const override
+            {
+                if (!Matched())
+                {
+                    return {};
+                }
+
+                return GetCommandProgramLine(params);
+            }
+
+            virtual std::vector<Command*> GetCommands() override
+            {
+                if (selectedCommand != nullptr)
+                {
+                    return selectedCommand->GetCommands();
+                }
+
+                if (Matched())
+                {
+                    return Group::GetCommands();
+                }
+
+                return { this };
+            }
+
+            virtual std::vector<std::tuple<std::string, std::string, unsigned>> GetDescription(const HelpParams &params, const unsigned int indent) const override
+            {
+                std::vector<std::tuple<std::string, std::string, unsigned>> descriptions;
+                unsigned addindent = 0;
+
+                UpdateSubparserHelp(params);
+
+                if (!Matched())
+                {
+                    if (params.showCommandFullHelp)
+                    {
+                        std::ostringstream s;
+                        bool empty = true;
+                        for (const auto &progline: GetCommandProgramLine(params))
+                        {
+                            if (!empty)
+                            {
+                                s << ' ';
+                            }
+                            else
+                            {
+                                empty = false;
+                            }
+
+                            s << progline;
+                        }
+
+                        descriptions.emplace_back(s.str(), "", indent);
+                    }
+                    else
+                    {
+                        descriptions.emplace_back(Name(), help, indent);
+                    }
+
+                    if (!params.showCommandChildren && !params.showCommandFullHelp)
+                    {
+                        return descriptions;
+                    }
+
+                    addindent = 1;
+                }
+
+                if (params.showCommandFullHelp && !Matched())
+                {
+                    descriptions.emplace_back("", "", indent + addindent);
+                    descriptions.emplace_back(Description().empty() ? Help() : Description(), "", indent + addindent);
+                    descriptions.emplace_back("", "", indent + addindent);
+                }
+
+                for (Base *child: Children())
+                {
+                    if ((child->GetOptions() & Options::HiddenFromDescription) != Options::None)
+                    {
+                        continue;
+                    }
+
+                    auto groupDescriptions = child->GetDescription(params, indent + addindent);
+                    descriptions.insert(
+                                        std::end(descriptions),
+                                        std::make_move_iterator(std::begin(groupDescriptions)),
+                                        std::make_move_iterator(std::end(groupDescriptions)));
+                }
+
+                for (auto childDescription: subparserDescription)
+                {
+                    std::get<2>(childDescription) += indent + addindent;
+                    descriptions.push_back(std::move(childDescription));
+                }
+
+                if (params.showCommandFullHelp && !Matched())
+                {
+                    descriptions.emplace_back("", "", indent + addindent);
+                    if (!Epilog().empty())
+                    {
+                        descriptions.emplace_back(Epilog(), "", indent + addindent);
+                        descriptions.emplace_back("", "", indent + addindent);
+                    }
+                }
+
+                return descriptions;
+            }
+
+            virtual void Validate(const std::string &shortprefix, const std::string &longprefix) const override
+            {
+                if (!Matched())
+                {
+                    return;
+                }
+
+                for (Base *child: Children())
+                {
+                    if (child->IsGroup() && !child->Matched())
+                    {
+                        std::ostringstream problem;
+                        problem << "Group validation failed somewhere!";
+#ifdef ARGS_NOEXCEPT
+                        error = Error::Validation;
+                        errorMsg = problem.str();
+#else
+                        throw ValidationError(problem.str());
+#endif
+                    }
+
+                    child->Validate(shortprefix, longprefix);
+                }
+
+                if (subparser != nullptr)
+                {
+                    subparser->Validate(shortprefix, longprefix);
+                }
+
+                if (selectedCommand == nullptr && commandIsRequired && (Group::HasCommand() || subparserHasCommand))
+                {
+                    std::ostringstream problem;
+                    problem << "Command is required";
+#ifdef ARGS_NOEXCEPT
+                    error = Error::Validation;
+                    errorMsg = problem.str();
+#else
+                    throw ValidationError(problem.str());
+#endif
+                }
+            }
+
+            virtual void Reset() noexcept override
+            {
+                Group::Reset();
+                selectedCommand = nullptr;
+                subparserProgramLine.clear();
+                subparserDescription.clear();
+                subparserHasFlag = false;
+                subparserHasPositional = false;
+                subparserHasCommand = false;
+#ifdef ARGS_NOEXCEPT
+                subparserError = Error::None;
+#endif
+            }
+
+#ifdef ARGS_NOEXCEPT
+            /// Only for ARGS_NOEXCEPT
+            virtual Error GetError() const override
+            {
+                if (!Matched())
+                {
+                    return Error::None;
+                }
+
+                if (error != Error::None)
+                {
+                    return error;
+                }
+
+                if (subparserError != Error::None)
+                {
+                    return subparserError;
+                }
+
+                return Group::GetError();
+            }
+#endif
+    };
+
+    /** The main user facing command line argument parser class
+     */
+    class ArgumentParser : public Command
+    {
+        friend class Subparser;
+
+        private:
+            std::string longprefix;
+            std::string shortprefix;
+
+            std::string longseparator;
+
+            std::string terminator;
+
+            bool allowJoinedShortValue = true;
+            bool allowJoinedLongValue = true;
+            bool allowSeparateShortValue = true;
+            bool allowSeparateLongValue = true;
+
+            CompletionFlag *completion = nullptr;
+            bool readCompletion = false;
+
+        protected:
+            enum class OptionType
+            {
+                LongFlag,
+                ShortFlag,
+                Positional
+            };
+
+            OptionType ParseOption(const std::string &s, bool allowEmpty = false)
+            {
+                if (s.find(longprefix) == 0 && (allowEmpty || s.length() > longprefix.length()))
+                {
+                    return OptionType::LongFlag;
+                }
+
+                if (s.find(shortprefix) == 0 && (allowEmpty || s.length() > shortprefix.length()))
+                {
+                    return OptionType::ShortFlag;
+                }
+
+                return OptionType::Positional;
+            }
+
+            template <typename It>
+            bool Complete(FlagBase &flag, It it, It end)
+            {
+                auto nextIt = it;
+                if (!readCompletion || (++nextIt != end))
+                {
+                    return false;
+                }
+
+                const auto &chunk = *it;
+                for (auto &choice : flag.HelpChoices(helpParams))
+                {
+                    AddCompletionReply(chunk, choice);
+                }
+
+#ifndef ARGS_NOEXCEPT
+                throw Completion(completion->Get());
+#else
+                return true;
+#endif
+            }
+
+            /** (INTERNAL) Parse flag's values
+             *
+             * \param arg The string to display in error message as a flag name
+             * \param[in, out] it The iterator to first value. It will point to the last value
+             * \param end The end iterator
+             * \param joinedArg Joined value (e.g. bar in --foo=bar)
+             * \param canDiscardJoined If true joined value can be parsed as flag not as a value (as in -abcd)
+             * \param[out] values The vector to store parsed arg's values
+             */
+            template <typename It>
+            std::string ParseArgsValues(FlagBase &flag, const std::string &arg, It &it, It end,
+                                        const bool allowSeparate, const bool allowJoined,
+                                        const bool hasJoined, const std::string &joinedArg,
+                                        const bool canDiscardJoined, std::vector<std::string> &values)
+            {
+                values.clear();
+
+                Nargs nargs = flag.NumberOfArguments();
+
+                if (hasJoined && !allowJoined && nargs.min != 0)
+                {
+                    return "Flag '" + arg + "' was passed a joined argument, but these are disallowed";
+                }
+
+                if (hasJoined)
+                {
+                    if (!canDiscardJoined || nargs.max != 0)
+                    {
+                        values.push_back(joinedArg);
+                    }
+                } else if (!allowSeparate)
+                {
+                    if (nargs.min != 0)
+                    {
+                        return "Flag '" + arg + "' was passed a separate argument, but these are disallowed";
+                    }
+                } else
+                {
+                    auto valueIt = it;
+                    ++valueIt;
+
+                    while (valueIt != end &&
+                           values.size() < nargs.max &&
+                           (nargs.min == nargs.max || ParseOption(*valueIt) == OptionType::Positional))
+                    {
+                        if (Complete(flag, valueIt, end))
+                        {
+                            it = end;
+                            return "";
+                        }
+
+                        values.push_back(*valueIt);
+                        ++it;
+                        ++valueIt;
+                    }
+                }
+
+                if (values.size() > nargs.max)
+                {
+                    return "Passed an argument into a non-argument flag: " + arg;
+                } else if (values.size() < nargs.min)
+                {
+                    if (nargs.min == 1 && nargs.max == 1)
+                    {
+                        return "Flag '" + arg + "' requires an argument but received none";
+                    } else if (nargs.min == 1)
+                    {
+                        return "Flag '" + arg + "' requires at least one argument but received none";
+                    } else if (nargs.min != nargs.max)
+                    {
+                        return "Flag '" + arg + "' requires at least " + std::to_string(nargs.min) +
+                               " arguments but received " + std::to_string(values.size());
+                    } else
+                    {
+                        return "Flag '" + arg + "' requires " + std::to_string(nargs.min) +
+                               " arguments but received " + std::to_string(values.size());
+                    }
+                }
+
+                return {};
+            }
+
+            template <typename It>
+            bool ParseLong(It &it, It end)
+            {
+                const auto &chunk = *it;
+                const auto argchunk = chunk.substr(longprefix.size());
+                // Try to separate it, in case of a separator:
+                const auto separator = longseparator.empty() ? argchunk.npos : argchunk.find(longseparator);
+                // If the separator is in the argument, separate it.
+                const auto arg = (separator != argchunk.npos ?
+                    std::string(argchunk, 0, separator)
+                    : argchunk);
+                const auto joined = (separator != argchunk.npos ?
+                    argchunk.substr(separator + longseparator.size())
+                    : std::string());
+
+                if (auto flag = Match(arg))
+                {
+                    std::vector<std::string> values;
+                    const std::string errorMessage = ParseArgsValues(*flag, arg, it, end, allowSeparateLongValue, allowJoinedLongValue,
+                                                                     separator != argchunk.npos, joined, false, values);
+                    if (!errorMessage.empty())
+                    {
+#ifndef ARGS_NOEXCEPT
+                        throw ParseError(errorMessage);
+#else
+                        error = Error::Parse;
+                        errorMsg = errorMessage;
+                        return false;
+#endif
+                    }
+
+                    if (!readCompletion)
+                    {
+                        flag->ParseValue(values);
+                    }
+
+                    if (flag->KickOut())
+                    {
+                        ++it;
+                        return false;
+                    }
+                } else
+                {
+                    const std::string errorMessage("Flag could not be matched: " + arg);
+#ifndef ARGS_NOEXCEPT
+                    throw ParseError(errorMessage);
+#else
+                    error = Error::Parse;
+                    errorMsg = errorMessage;
+                    return false;
+#endif
+                }
+
+                return true;
+            }
+
+            template <typename It>
+            bool ParseShort(It &it, It end)
+            {
+                const auto &chunk = *it;
+                const auto argchunk = chunk.substr(shortprefix.size());
+                for (auto argit = std::begin(argchunk); argit != std::end(argchunk); ++argit)
+                {
+                    const auto arg = *argit;
+
+                    if (auto flag = Match(arg))
+                    {
+                        const std::string value(argit + 1, std::end(argchunk));
+                        std::vector<std::string> values;
+                        const std::string errorMessage = ParseArgsValues(*flag, std::string(1, arg), it, end,
+                                                                         allowSeparateShortValue, allowJoinedShortValue,
+                                                                         !value.empty(), value, !value.empty(), values);
+
+                        if (!errorMessage.empty())
+                        {
+#ifndef ARGS_NOEXCEPT
+                            throw ParseError(errorMessage);
+#else
+                            error = Error::Parse;
+                            errorMsg = errorMessage;
+                            return false;
+#endif
+                        }
+
+                        if (!readCompletion)
+                        {
+                            flag->ParseValue(values);
+                        }
+
+                        if (flag->KickOut())
+                        {
+                            ++it;
+                            return false;
+                        }
+
+                        if (!values.empty())
+                        {
+                            break;
+                        }
+                    } else
+                    {
+                        const std::string errorMessage("Flag could not be matched: '" + std::string(1, arg) + "'");
+#ifndef ARGS_NOEXCEPT
+                        throw ParseError(errorMessage);
+#else
+                        error = Error::Parse;
+                        errorMsg = errorMessage;
+                        return false;
+#endif
+                    }
+                }
+
+                return true;
+            }
+
+            bool AddCompletionReply(const std::string &cur, const std::string &choice)
+            {
+                if (cur.empty() || choice.find(cur) == 0)
+                {
+                    if (completion->syntax == "bash" && ParseOption(choice) == OptionType::LongFlag && choice.find(longseparator) != std::string::npos)
+                    {
+                        completion->reply.push_back(choice.substr(choice.find(longseparator) + 1));
+                    } else
+                    {
+                        completion->reply.push_back(choice);
+                    }
+                    return true;
+                }
+
+                return false;
+            }
+
+            template <typename It>
+            bool Complete(It it, It end)
+            {
+                auto nextIt = it;
+                if (!readCompletion || (++nextIt != end))
+                {
+                    return false;
+                }
+
+                const auto &chunk = *it;
+                auto pos = GetNextPositional();
+                std::vector<Command *> commands = GetCommands();
+                const auto optionType = ParseOption(chunk, true);
+
+                if (!commands.empty() && (chunk.empty() || optionType == OptionType::Positional))
+                {
+                    for (auto &cmd : commands)
+                    {
+                        if ((cmd->GetOptions() & Options::HiddenFromCompletion) == Options::None)
+                        {
+                            AddCompletionReply(chunk, cmd->Name());
+                        }
+                    }
+                } else
+                {
+                    bool hasPositionalCompletion = true;
+
+                    if (!commands.empty())
+                    {
+                        for (auto &cmd : commands)
+                        {
+                            if ((cmd->GetOptions() & Options::HiddenFromCompletion) == Options::None)
+                            {
+                                AddCompletionReply(chunk, cmd->Name());
+                            }
+                        }
+                    } else if (pos)
+                    {
+                        if ((pos->GetOptions() & Options::HiddenFromCompletion) == Options::None)
+                        {
+                            auto choices = pos->HelpChoices(helpParams);
+                            hasPositionalCompletion = !choices.empty() || optionType != OptionType::Positional;
+                            for (auto &choice : choices)
+                            {
+                                AddCompletionReply(chunk, choice);
+                            }
+                        }
+                    }
+
+                    if (hasPositionalCompletion)
+                    {
+                        auto flags = GetAllFlags();
+                        for (auto flag : flags)
+                        {
+                            if ((flag->GetOptions() & Options::HiddenFromCompletion) != Options::None)
+                            {
+                                continue;
+                            }
+
+                            auto &matcher = flag->GetMatcher();
+                            if (!AddCompletionReply(chunk, matcher.GetShortOrAny().str(shortprefix, longprefix)))
+                            {
+                                for (auto &flagName : matcher.GetFlagStrings())
+                                {
+                                    if (AddCompletionReply(chunk, flagName.str(shortprefix, longprefix)))
+                                    {
+                                        break;
+                                    }
+                                }
+                            }
+                        }
+
+                        if (optionType == OptionType::LongFlag && allowJoinedLongValue)
+                        {
+                            const auto separator = longseparator.empty() ? chunk.npos : chunk.find(longseparator);
+                            if (separator != chunk.npos)
+                            {
+                                std::string arg(chunk, 0, separator);
+                                if (auto flag = this->Match(arg.substr(longprefix.size())))
+                                {
+                                    for (auto &choice : flag->HelpChoices(helpParams))
+                                    {
+                                        AddCompletionReply(chunk, arg + longseparator + choice);
+                                    }
+                                }
+                            }
+                        } else if (optionType == OptionType::ShortFlag && allowJoinedShortValue)
+                        {
+                            if (chunk.size() > shortprefix.size() + 1)
+                            {
+                                auto arg = chunk.at(shortprefix.size());
+                                //TODO: support -abcVALUE where a and b take no value
+                                if (auto flag = this->Match(arg))
+                                {
+                                    for (auto &choice : flag->HelpChoices(helpParams))
+                                    {
+                                        AddCompletionReply(chunk, shortprefix + arg + choice);
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+
+#ifndef ARGS_NOEXCEPT
+                throw Completion(completion->Get());
+#else
+                return true;
+#endif
+            }
+
+            template <typename It>
+            It Parse(It begin, It end)
+            {
+                bool terminated = false;
+                std::vector<Command *> commands = GetCommands();
+
+                // Check all arg chunks
+                for (auto it = begin; it != end; ++it)
+                {
+                    if (Complete(it, end))
+                    {
+                        return end;
+                    }
+
+                    const auto &chunk = *it;
+
+                    if (!terminated && chunk == terminator)
+                    {
+                        terminated = true;
+                    } else if (!terminated && ParseOption(chunk) == OptionType::LongFlag)
+                    {
+                        if (!ParseLong(it, end))
+                        {
+                            return it;
+                        }
+                    } else if (!terminated && ParseOption(chunk) == OptionType::ShortFlag)
+                    {
+                        if (!ParseShort(it, end))
+                        {
+                            return it;
+                        }
+                    } else if (!terminated && !commands.empty())
+                    {
+                        auto itCommand = std::find_if(commands.begin(), commands.end(), [&chunk](Command *c) { return c->Name() == chunk; });
+                        if (itCommand == commands.end())
+                        {
+                            const std::string errorMessage("Unknown command: " + chunk);
+#ifndef ARGS_NOEXCEPT
+                            throw ParseError(errorMessage);
+#else
+                            error = Error::Parse;
+                            errorMsg = errorMessage;
+                            return it;
+#endif
+                        }
+
+                        SelectCommand(*itCommand);
+
+                        if (const auto &coroutine = GetCoroutine())
+                        {
+                            ++it;
+                            RaiiSubparser coro(*this, std::vector<std::string>(it, end));
+                            coroutine(coro.Parser());
+#ifdef ARGS_NOEXCEPT
+                            error = GetError();
+                            if (error != Error::None)
+                            {
+                                return end;
+                            }
+
+                            if (!coro.Parser().IsParsed())
+                            {
+                                error = Error::Usage;
+                                return end;
+                            }
+#else
+                            if (!coro.Parser().IsParsed())
+                            {
+                                throw UsageError("Subparser::Parse was not called");
+                            }
+#endif
+
+                            break;
+                        }
+
+                        commands = GetCommands();
+                    } else
+                    {
+                        auto pos = GetNextPositional();
+                        if (pos)
+                        {
+                            pos->ParseValue(chunk);
+
+                            if (pos->KickOut())
+                            {
+                                return ++it;
+                            }
+                        } else
+                        {
+                            const std::string errorMessage("Passed in argument, but no positional arguments were ready to receive it: " + chunk);
+#ifndef ARGS_NOEXCEPT
+                            throw ParseError(errorMessage);
+#else
+                            error = Error::Parse;
+                            errorMsg = errorMessage;
+                            return it;
+#endif
+                        }
+                    }
+
+                    if (!readCompletion && completion != nullptr && completion->Matched())
+                    {
+#ifdef ARGS_NOEXCEPT
+                        error = Error::Completion;
+#endif
+                        readCompletion = true;
+                        ++it;
+                        size_t argsLeft = std::distance(it, end);
+                        if (completion->cword == 0 || argsLeft <= 1 || completion->cword >= argsLeft)
+                        {
+#ifndef ARGS_NOEXCEPT
+                            throw Completion("");
+#endif
+                        }
+
+                        std::vector<std::string> curArgs(++it, end);
+                        curArgs.resize(completion->cword);
+
+                        if (completion->syntax == "bash")
+                        {
+                            // bash tokenizes --flag=value as --flag=value
+                            for (size_t idx = 0; idx < curArgs.size(); )
+                            {
+                                if (idx > 0 && curArgs[idx] == "=")
+                                {
+                                    curArgs[idx - 1] += "=";
+                                    if (idx + 1 < curArgs.size())
+                                    {
+                                        curArgs[idx - 1] += curArgs[idx + 1];
+                                        curArgs.erase(curArgs.begin() + idx, curArgs.begin() + idx + 2);
+                                    } else
+                                    {
+                                        curArgs.erase(curArgs.begin() + idx);
+                                    }
+                                } else
+                                {
+                                    ++idx;
+                                }
+                            }
+
+                        }
+#ifndef ARGS_NOEXCEPT
+                        try
+                        {
+                            Parse(curArgs.begin(), curArgs.end());
+                            throw Completion("");
+                        }
+                        catch (Completion &)
+                        {
+                            throw;
+                        }
+                        catch (args::Error&)
+                        {
+                            throw Completion("");
+                        }
+#else
+                        return Parse(curArgs.begin(), curArgs.end());
+#endif
+                    }
+                }
+
+                Validate(shortprefix, longprefix);
+                return end;
+            }
+
+        public:
+            HelpParams helpParams;
+
+            ArgumentParser(const std::string &description_, const std::string &epilog_ = std::string())
+            {
+                Description(description_);
+                Epilog(epilog_);
+                LongPrefix("--");
+                ShortPrefix("-");
+                LongSeparator("=");
+                Terminator("--");
+                SetArgumentSeparations(true, true, true, true);
+                matched = true;
+            }
+
+            void AddCompletion(CompletionFlag &completionFlag)
+            {
+                completion = &completionFlag;
+                Add(completionFlag);
+            }
+
+            /** The program name for help generation
+             */
+            const std::string &Prog() const
+            { return helpParams.programName; }
+            /** The program name for help generation
+             */
+            void Prog(const std::string &prog_)
+            { this->helpParams.programName = prog_; }
+
+            /** The prefix for long flags
+             */
+            const std::string &LongPrefix() const
+            { return longprefix; }
+            /** The prefix for long flags
+             */
+            void LongPrefix(const std::string &longprefix_)
+            {
+                this->longprefix = longprefix_;
+                this->helpParams.longPrefix = longprefix_;
+            }
+
+            /** The prefix for short flags
+             */
+            const std::string &ShortPrefix() const
+            { return shortprefix; }
+            /** The prefix for short flags
+             */
+            void ShortPrefix(const std::string &shortprefix_)
+            {
+                this->shortprefix = shortprefix_;
+                this->helpParams.shortPrefix = shortprefix_;
+            }
+
+            /** The separator for long flags
+             */
+            const std::string &LongSeparator() const
+            { return longseparator; }
+            /** The separator for long flags
+             */
+            void LongSeparator(const std::string &longseparator_)
+            {
+                if (longseparator_.empty())
+                {
+                    const std::string errorMessage("longseparator can not be set to empty");
+#ifdef ARGS_NOEXCEPT
+                    error = Error::Usage;
+                    errorMsg = errorMessage;
+#else
+                    throw UsageError(errorMessage);
+#endif
+                } else
+                {
+                    this->longseparator = longseparator_;
+                    this->helpParams.longSeparator = allowJoinedLongValue ? longseparator_ : " ";
+                }
+            }
+
+            /** The terminator that forcibly separates flags from positionals
+             */
+            const std::string &Terminator() const
+            { return terminator; }
+            /** The terminator that forcibly separates flags from positionals
+             */
+            void Terminator(const std::string &terminator_)
+            { this->terminator = terminator_; }
+
+            /** Get the current argument separation parameters.
+             *
+             * See SetArgumentSeparations for details on what each one means.
+             */
+            void GetArgumentSeparations(
+                bool &allowJoinedShortValue_,
+                bool &allowJoinedLongValue_,
+                bool &allowSeparateShortValue_,
+                bool &allowSeparateLongValue_) const
+            {
+                allowJoinedShortValue_ = this->allowJoinedShortValue;
+                allowJoinedLongValue_ = this->allowJoinedLongValue;
+                allowSeparateShortValue_ = this->allowSeparateShortValue;
+                allowSeparateLongValue_ = this->allowSeparateLongValue;
+            }
+
+            /** Change allowed option separation.
+             *
+             * \param allowJoinedShortValue_ Allow a short flag that accepts an argument to be passed its argument immediately next to it (ie. in the same argv field)
+             * \param allowJoinedLongValue_ Allow a long flag that accepts an argument to be passed its argument separated by the longseparator (ie. in the same argv field)
+             * \param allowSeparateShortValue_ Allow a short flag that accepts an argument to be passed its argument separated by whitespace (ie. in the next argv field)
+             * \param allowSeparateLongValue_ Allow a long flag that accepts an argument to be passed its argument separated by whitespace (ie. in the next argv field)
+             */
+            void SetArgumentSeparations(
+                const bool allowJoinedShortValue_,
+                const bool allowJoinedLongValue_,
+                const bool allowSeparateShortValue_,
+                const bool allowSeparateLongValue_)
+            {
+                this->allowJoinedShortValue = allowJoinedShortValue_;
+                this->allowJoinedLongValue = allowJoinedLongValue_;
+                this->allowSeparateShortValue = allowSeparateShortValue_;
+                this->allowSeparateLongValue = allowSeparateLongValue_;
+
+                this->helpParams.longSeparator = allowJoinedLongValue ? longseparator : " ";
+                this->helpParams.shortSeparator = allowJoinedShortValue ? "" : " ";
+            }
+
+            /** Pass the help menu into an ostream
+             */
+            void Help(std::ostream &help_) const
+            {
+                auto &command = SelectedCommand();
+                const auto &commandDescription = command.Description().empty() ? command.Help() : command.Description();
+                const auto description_text = Wrap(commandDescription, helpParams.width - helpParams.descriptionindent);
+                const auto epilog_text = Wrap(command.Epilog(), helpParams.width - helpParams.descriptionindent);
+
+                const bool hasoptions = command.HasFlag();
+                const bool hasarguments = command.HasPositional();
+
+                std::vector<std::string> prognameline;
+                prognameline.push_back(helpParams.usageString);
+                prognameline.push_back(Prog());
+                auto commandProgLine = command.GetProgramLine(helpParams);
+                prognameline.insert(prognameline.end(), commandProgLine.begin(), commandProgLine.end());
+
+                const auto proglines = Wrap(prognameline.begin(), prognameline.end(),
+                                            helpParams.width - (helpParams.progindent + helpParams.progtailindent),
+                                            helpParams.width - helpParams.progindent);
+                auto progit = std::begin(proglines);
+                if (progit != std::end(proglines))
+                {
+                    help_ << std::string(helpParams.progindent, ' ') << *progit << '\n';
+                    ++progit;
+                }
+                for (; progit != std::end(proglines); ++progit)
+                {
+                    help_ << std::string(helpParams.progtailindent, ' ') << *progit << '\n';
+                }
+
+                help_ << '\n';
+
+                if (!description_text.empty())
+                {
+                    for (const auto &line: description_text)
+                    {
+                        help_ << std::string(helpParams.descriptionindent, ' ') << line << "\n";
+                    }
+                    help_ << "\n";
+                }
+
+                bool lastDescriptionIsNewline = false;
+
+                if (!helpParams.optionsString.empty())
+                {
+                    help_ << std::string(helpParams.progindent, ' ') << helpParams.optionsString << "\n\n";
+                }
+
+                for (const auto &desc: command.GetDescription(helpParams, 0))
+                {
+                    lastDescriptionIsNewline = std::get<0>(desc).empty() && std::get<1>(desc).empty();
+                    const auto groupindent = std::get<2>(desc) * helpParams.eachgroupindent;
+                    const auto flags = Wrap(std::get<0>(desc), helpParams.width - (helpParams.flagindent + helpParams.helpindent + helpParams.gutter));
+                    const auto info = Wrap(std::get<1>(desc), helpParams.width - (helpParams.helpindent + groupindent));
+
+                    std::string::size_type flagssize = 0;
+                    for (auto flagsit = std::begin(flags); flagsit != std::end(flags); ++flagsit)
+                    {
+                        if (flagsit != std::begin(flags))
+                        {
+                            help_ << '\n';
+                        }
+                        help_ << std::string(groupindent + helpParams.flagindent, ' ') << *flagsit;
+                        flagssize = Glyphs(*flagsit);
+                    }
+
+                    auto infoit = std::begin(info);
+                    // groupindent is on both sides of this inequality, and therefore can be removed
+                    if ((helpParams.flagindent + flagssize + helpParams.gutter) > helpParams.helpindent || infoit == std::end(info) || helpParams.addNewlineBeforeDescription)
+                    {
+                        help_ << '\n';
+                    } else
+                    {
+                        // groupindent is on both sides of the minus sign, and therefore doesn't actually need to be in here
+                        help_ << std::string(helpParams.helpindent - (helpParams.flagindent + flagssize), ' ') << *infoit << '\n';
+                        ++infoit;
+                    }
+                    for (; infoit != std::end(info); ++infoit)
+                    {
+                        help_ << std::string(groupindent + helpParams.helpindent, ' ') << *infoit << '\n';
+                    }
+                }
+                if (hasoptions && hasarguments && helpParams.showTerminator)
+                {
+                    lastDescriptionIsNewline = false;
+                    for (const auto &item: Wrap(std::string("\"") + terminator + "\" can be used to terminate flag options and force all following arguments to be treated as positional options", helpParams.width - helpParams.flagindent))
+                    {
+                        help_ << std::string(helpParams.flagindent, ' ') << item << '\n';
+                    }
+                }
+
+                if (!lastDescriptionIsNewline)
+                {
+                    help_ << "\n";
+                }
+
+                for (const auto &line: epilog_text)
+                {
+                    help_ << std::string(helpParams.descriptionindent, ' ') << line << "\n";
+                }
+            }
+
+            /** Generate a help menu as a string.
+             *
+             * \return the help text as a single string
+             */
+            std::string Help() const
+            {
+                std::ostringstream help_;
+                Help(help_);
+                return help_.str();
+            }
+
+            virtual void Reset() noexcept override
+            {
+                Command::Reset();
+                matched = true;
+                readCompletion = false;
+            }
+
+            /** Parse all arguments.
+             *
+             * \param begin an iterator to the beginning of the argument list
+             * \param end an iterator to the past-the-end element of the argument list
+             * \return the iterator after the last parsed value.  Only useful for kick-out
+             */
+            template <typename It>
+            It ParseArgs(It begin, It end)
+            {
+                // Reset all Matched statuses and errors
+                Reset();
+#ifdef ARGS_NOEXCEPT
+                error = GetError();
+                if (error != Error::None)
+                {
+                    return end;
+                }
+#endif
+                return Parse(begin, end);
+            }
+
+            /** Parse all arguments.
+             *
+             * \param args an iterable of the arguments
+             * \return the iterator after the last parsed value.  Only useful for kick-out
+             */
+            template <typename T>
+            auto ParseArgs(const T &args) -> decltype(std::begin(args))
+            {
+                return ParseArgs(std::begin(args), std::end(args));
+            }
+
+            /** Convenience function to parse the CLI from argc and argv
+             *
+             * Just assigns the program name and vectorizes arguments for passing into ParseArgs()
+             *
+             * \return whether or not all arguments were parsed.  This works for detecting kick-out, but is generally useless as it can't do anything with it.
+             */
+            bool ParseCLI(const int argc, const char * const * argv)
+            {
+                if (Prog().empty())
+                {
+                    Prog(argv[0]);
+                }
+                const std::vector<std::string> args(argv + 1, argv + argc);
+                return ParseArgs(args) == std::end(args);
+            }
+            
+            template <typename T>
+            bool ParseCLI(const T &args)
+            {
+                return ParseArgs(args) == std::end(args);
+            }
+    };
+
+    inline Command::RaiiSubparser::RaiiSubparser(ArgumentParser &parser_, std::vector<std::string> args_)
+        : command(parser_.SelectedCommand()), parser(std::move(args_), parser_, command, parser_.helpParams), oldSubparser(command.subparser)
+    {
+        command.subparser = &parser;
+    }
+
+    inline Command::RaiiSubparser::RaiiSubparser(const Command &command_, const HelpParams &params_): command(command_), parser(command, params_), oldSubparser(command.subparser)
+    {
+        command.subparser = &parser;
+    }
+
+    inline void Subparser::Parse()
+    {
+        isParsed = true;
+        Reset();
+        command.subparserDescription = GetDescription(helpParams, 0);
+        command.subparserHasFlag = HasFlag();
+        command.subparserHasPositional = HasPositional();
+        command.subparserHasCommand = HasCommand();
+        command.subparserProgramLine = GetProgramLine(helpParams);
+        if (parser == nullptr)
+        {
+#ifndef ARGS_NOEXCEPT
+            throw args::SubparserError();
+#else
+            error = Error::Subparser;
+            return;
+#endif
+        }
+
+        auto it = parser->Parse(args.begin(), args.end());
+        command.Validate(parser->ShortPrefix(), parser->LongPrefix());
+        kicked.assign(it, args.end());
+
+#ifdef ARGS_NOEXCEPT
+        command.subparserError = GetError();
+#endif
+    }
+
+    inline std::ostream &operator<<(std::ostream &os, const ArgumentParser &parser)
+    {
+        parser.Help(os);
+        return os;
+    }
+
+    /** Boolean argument matcher
+     */
+    class Flag : public FlagBase
+    {
+        public:
+            Flag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_): FlagBase(name_, help_, std::move(matcher_), options_)
+            {
+                group_.Add(*this);
+            }
+
+            Flag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const bool extraError_ = false): Flag(group_, name_, help_, std::move(matcher_), extraError_ ? Options::Single : Options::None)
+            {
+            }
+
+            virtual ~Flag() {}
+
+            /** Get whether this was matched
+             */
+            bool Get() const
+            {
+                return Matched();
+            }
+
+            virtual Nargs NumberOfArguments() const noexcept override
+            {
+                return 0;
+            }
+
+            virtual void ParseValue(const std::vector<std::string>&) override
+            {
+            }
+    };
+
+    /** Help flag class
+     *
+     * Works like a regular flag, but throws an instance of Help when it is matched
+     */
+    class HelpFlag : public Flag
+    {
+        public:
+            HelpFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_ = {}): Flag(group_, name_, help_, std::move(matcher_), options_) {}
+
+            virtual ~HelpFlag() {}
+
+            virtual void ParseValue(const std::vector<std::string> &)
+            {
+#ifdef ARGS_NOEXCEPT
+                    error = Error::Help;
+                    errorMsg = Name();
+#else
+                    throw Help(Name());
+#endif
+            }
+
+            /** Get whether this was matched
+             */
+            bool Get() const noexcept
+            {
+                return Matched();
+            }
+    };
+
+    /** A flag class that simply counts the number of times it's matched
+     */
+    class CounterFlag : public Flag
+    {
+        private:
+            const int startcount;
+            int count;
+
+        public:
+            CounterFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const int startcount_ = 0, Options options_ = {}):
+                Flag(group_, name_, help_, std::move(matcher_), options_), startcount(startcount_), count(startcount_) {}
+
+            virtual ~CounterFlag() {}
+
+            virtual FlagBase *Match(const EitherFlag &arg) override
+            {
+                auto me = FlagBase::Match(arg);
+                if (me)
+                {
+                    ++count;
+                }
+                return me;
+            }
+
+            /** Get the count
+             */
+            int &Get() noexcept
+            {
+                return count;
+            }
+
+            virtual void Reset() noexcept override
+            {
+                FlagBase::Reset();
+                count = startcount;
+            }
+    };
+
+    /** A flag class that calls a function when it's matched
+     */
+    class ActionFlag : public FlagBase
+    {
+        private:
+            std::function<void(const std::vector<std::string> &)> action;
+            Nargs nargs;
+
+        public:
+            ActionFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Nargs nargs_, std::function<void(const std::vector<std::string> &)> action_, Options options_ = {}):
+                FlagBase(name_, help_, std::move(matcher_), options_), action(std::move(action_)), nargs(nargs_)
+            {
+                group_.Add(*this);
+            }
+
+            ActionFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, std::function<void(const std::string &)> action_, Options options_ = {}):
+                FlagBase(name_, help_, std::move(matcher_), options_), nargs(1)
+            {
+                group_.Add(*this);
+                action = [action_](const std::vector<std::string> &a) { return action_(a.at(0)); };
+            }
+
+            ActionFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, std::function<void()> action_, Options options_ = {}):
+                FlagBase(name_, help_, std::move(matcher_), options_), nargs(0)
+            {
+                group_.Add(*this);
+                action = [action_](const std::vector<std::string> &) { return action_(); };
+            }
+
+            virtual Nargs NumberOfArguments() const noexcept override
+            { return nargs; }
+
+            virtual void ParseValue(const std::vector<std::string> &value) override
+            { action(value); }
+    };
+
+    /** A default Reader class for argument classes
+     *
+     * If destination type is assignable to std::string it uses an assignment to std::string.
+     * Otherwise ValueReader simply uses a std::istringstream to read into the destination type, and
+     * raises a ParseError if there are any characters left.
+     */
+    struct ValueReader
+    {
+        template <typename T>
+        typename std::enable_if<!std::is_assignable<T, std::string>::value, bool>::type
+        operator ()(const std::string &name, const std::string &value, T &destination)
+        {
+            std::istringstream ss(value);
+            ss >> destination >> std::ws;
+
+            if (ss.rdbuf()->in_avail() > 0)
+            {
+#ifdef ARGS_NOEXCEPT
+                (void)name;
+                return false;
+#else
+                std::ostringstream problem;
+                problem << "Argument '" << name << "' received invalid value type '" << value << "'";
+                throw ParseError(problem.str());
+#endif
+            }
+            return true;
+        }
+
+        template <typename T>
+        typename std::enable_if<std::is_assignable<T, std::string>::value, bool>::type
+        operator()(const std::string &, const std::string &value, T &destination)
+        {
+            destination = value;
+            return true;
+        }
+    };
+
+    /** An argument-accepting flag class
+     * 
+     * \tparam T the type to extract the argument as
+     * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined)
+     */
+    template <
+        typename T,
+        typename Reader = ValueReader>
+    class ValueFlag : public ValueFlagBase
+    {
+        protected:
+            T value;
+            T defaultValue;
+
+            virtual std::string GetDefaultString(const HelpParams&) const override
+            {
+                return detail::ToString(defaultValue);
+            }
+
+        private:
+            Reader reader;
+
+        public:
+
+            ValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const T &defaultValue_, Options options_): ValueFlagBase(name_, help_, std::move(matcher_), options_), value(defaultValue_), defaultValue(defaultValue_)
+            {
+                group_.Add(*this);
+            }
+
+            ValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const T &defaultValue_ = T(), const bool extraError_ = false): ValueFlag(group_, name_, help_, std::move(matcher_), defaultValue_, extraError_ ? Options::Single : Options::None)
+            {
+            }
+
+            ValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_): ValueFlag(group_, name_, help_, std::move(matcher_), T(), options_)
+            {
+            }
+
+            virtual ~ValueFlag() {}
+
+            virtual void ParseValue(const std::vector<std::string> &values_) override
+            {
+                const std::string &value_ = values_.at(0);
+
+#ifdef ARGS_NOEXCEPT
+                if (!reader(name, value_, this->value))
+                {
+                    error = Error::Parse;
+                }
+#else
+                reader(name, value_, this->value);
+#endif
+            }
+
+            virtual void Reset() noexcept override
+            {
+                ValueFlagBase::Reset();
+                value = defaultValue;
+            }
+
+            /** Get the value
+             */
+            T &Get() noexcept
+            {
+                return value;
+            }
+
+            /** Get the default value
+             */
+            const T &GetDefault() noexcept
+            {
+                return defaultValue;
+            }
+    };
+
+    /** An optional argument-accepting flag class
+     *
+     * \tparam T the type to extract the argument as
+     * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined)
+     */
+    template <
+        typename T,
+        typename Reader = ValueReader>
+    class ImplicitValueFlag : public ValueFlag<T, Reader>
+    {
+        protected:
+            T implicitValue;
+
+        public:
+
+            ImplicitValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const T &implicitValue_, const T &defaultValue_ = T(), Options options_ = {})
+                : ValueFlag<T, Reader>(group_, name_, help_, std::move(matcher_), defaultValue_, options_), implicitValue(implicitValue_)
+            {
+            }
+
+            ImplicitValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const T &defaultValue_ = T(), Options options_ = {})
+                : ValueFlag<T, Reader>(group_, name_, help_, std::move(matcher_), defaultValue_, options_), implicitValue(defaultValue_)
+            {
+            }
+
+            ImplicitValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_)
+                : ValueFlag<T, Reader>(group_, name_, help_, std::move(matcher_), {}, options_), implicitValue()
+            {
+            }
+
+            virtual ~ImplicitValueFlag() {}
+
+            virtual Nargs NumberOfArguments() const noexcept override
+            {
+                return {0, 1};
+            }
+
+            virtual void ParseValue(const std::vector<std::string> &value_) override
+            {
+                if (value_.empty())
+                {
+                    this->value = implicitValue;
+                } else
+                {
+                    ValueFlag<T, Reader>::ParseValue(value_);
+                }
+            }
+    };
+
+    /** A variadic arguments accepting flag class
+     *
+     * \tparam T the type to extract the argument as
+     * \tparam List the list type that houses the values
+     * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined)
+     */
+    template <
+        typename T,
+        template <typename...> class List = std::vector,
+        typename Reader = ValueReader>
+    class NargsValueFlag : public FlagBase
+    {
+        protected:
+
+            List<T> values;
+            const List<T> defaultValues;
+            Nargs nargs;
+            Reader reader;
+
+        public:
+
+            typedef List<T> Container;
+            typedef T value_type;
+            typedef typename Container::allocator_type allocator_type;
+            typedef typename Container::pointer pointer;
+            typedef typename Container::const_pointer const_pointer;
+            typedef T& reference;
+            typedef const T& const_reference;
+            typedef typename Container::size_type size_type;
+            typedef typename Container::difference_type difference_type;
+            typedef typename Container::iterator iterator;
+            typedef typename Container::const_iterator const_iterator;
+            typedef std::reverse_iterator<iterator> reverse_iterator;
+            typedef std::reverse_iterator<const_iterator> const_reverse_iterator;
+
+            NargsValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Nargs nargs_, const List<T> &defaultValues_ = {}, Options options_ = {})
+                : FlagBase(name_, help_, std::move(matcher_), options_), values(defaultValues_), defaultValues(defaultValues_),nargs(nargs_)
+            {
+                group_.Add(*this);
+            }
+
+            virtual ~NargsValueFlag() {}
+
+            virtual Nargs NumberOfArguments() const noexcept override
+            {
+                return nargs;
+            }
+
+            virtual void ParseValue(const std::vector<std::string> &values_) override
+            {
+                values.clear();
+
+                for (const std::string &value : values_)
+                {
+                    T v;
+#ifdef ARGS_NOEXCEPT
+                    if (!reader(name, value, v))
+                    {
+                        error = Error::Parse;
+                    }
+#else
+                    reader(name, value, v);
+#endif
+                    values.insert(std::end(values), v);
+                }
+            }
+
+            List<T> &Get() noexcept
+            {
+                return values;
+            }
+
+            iterator begin() noexcept
+            {
+                return values.begin();
+            }
+
+            const_iterator begin() const noexcept
+            {
+                return values.begin();
+            }
+
+            const_iterator cbegin() const noexcept
+            {
+                return values.cbegin();
+            }
+
+            iterator end() noexcept
+            {
+                return values.end();
+            }
+
+            const_iterator end() const noexcept 
+            {
+                return values.end();
+            }
+
+            const_iterator cend() const noexcept
+            {
+                return values.cend();
+            }
+
+            virtual void Reset() noexcept override
+            {
+                FlagBase::Reset();
+                values = defaultValues;
+            }
+
+            virtual FlagBase *Match(const EitherFlag &arg) override
+            {
+                const bool wasMatched = Matched();
+                auto me = FlagBase::Match(arg);
+                if (me && !wasMatched)
+                {
+                    values.clear();
+                }
+                return me;
+            }
+    };
+
+    /** An argument-accepting flag class that pushes the found values into a list
+     * 
+     * \tparam T the type to extract the argument as
+     * \tparam List the list type that houses the values
+     * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined)
+     */
+    template <
+        typename T,
+        template <typename...> class List = std::vector,
+        typename Reader = ValueReader>
+    class ValueFlagList : public ValueFlagBase
+    {
+        private:
+            using Container = List<T>;
+            Container values;
+            const Container defaultValues;
+            Reader reader;
+
+        public:
+
+            typedef T value_type;
+            typedef typename Container::allocator_type allocator_type;
+            typedef typename Container::pointer pointer;
+            typedef typename Container::const_pointer const_pointer;
+            typedef T& reference;
+            typedef const T& const_reference;
+            typedef typename Container::size_type size_type;
+            typedef typename Container::difference_type difference_type;
+            typedef typename Container::iterator iterator;
+            typedef typename Container::const_iterator const_iterator;
+            typedef std::reverse_iterator<iterator> reverse_iterator;
+            typedef std::reverse_iterator<const_iterator> const_reverse_iterator;
+
+            ValueFlagList(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const Container &defaultValues_ = Container(), Options options_ = {}):
+                ValueFlagBase(name_, help_, std::move(matcher_), options_), values(defaultValues_), defaultValues(defaultValues_)
+            {
+                group_.Add(*this);
+            }
+
+            virtual ~ValueFlagList() {}
+
+            virtual void ParseValue(const std::vector<std::string> &values_) override
+            {
+                const std::string &value_ = values_.at(0);
+
+                T v;
+#ifdef ARGS_NOEXCEPT
+                if (!reader(name, value_, v))
+                {
+                    error = Error::Parse;
+                }
+#else
+                reader(name, value_, v);
+#endif
+                values.insert(std::end(values), v);
+            }
+
+            /** Get the values
+             */
+            Container &Get() noexcept
+            {
+                return values;
+            }
+
+            virtual std::string Name() const override
+            {
+                return name + std::string("...");
+            }
+
+            virtual void Reset() noexcept override
+            {
+                ValueFlagBase::Reset();
+                values = defaultValues;
+            }
+
+            virtual FlagBase *Match(const EitherFlag &arg) override
+            {
+                const bool wasMatched = Matched();
+                auto me = FlagBase::Match(arg);
+                if (me && !wasMatched)
+                {
+                    values.clear();
+                }
+                return me;
+            }
+
+            iterator begin() noexcept
+            {
+                return values.begin();
+            }
+
+            const_iterator begin() const noexcept
+            {
+                return values.begin();
+            }
+
+            const_iterator cbegin() const noexcept
+            {
+                return values.cbegin();
+            }
+
+            iterator end() noexcept
+            {
+                return values.end();
+            }
+
+            const_iterator end() const noexcept 
+            {
+                return values.end();
+            }
+
+            const_iterator cend() const noexcept
+            {
+                return values.cend();
+            }
+    };
+
+    /** A mapping value flag class
+     * 
+     * \tparam K the type to extract the argument as
+     * \tparam T the type to store the result as
+     * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined)
+     * \tparam Map The Map type.  Should operate like std::map or std::unordered_map
+     */
+    template <
+        typename K,
+        typename T,
+        typename Reader = ValueReader,
+        template <typename...> class Map = std::unordered_map>
+    class MapFlag : public ValueFlagBase
+    {
+        private:
+            const Map<K, T> map;
+            T value;
+            const T defaultValue;
+            Reader reader;
+
+        protected:
+            virtual std::vector<std::string> GetChoicesStrings(const HelpParams &) const override
+            {
+                return detail::MapKeysToStrings(map);
+            }
+
+        public:
+
+            MapFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const Map<K, T> &map_, const T &defaultValue_, Options options_): ValueFlagBase(name_, help_, std::move(matcher_), options_), map(map_), value(defaultValue_), defaultValue(defaultValue_)
+            {
+                group_.Add(*this);
+            }
+
+            MapFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const Map<K, T> &map_, const T &defaultValue_ = T(), const bool extraError_ = false): MapFlag(group_, name_, help_, std::move(matcher_), map_, defaultValue_, extraError_ ? Options::Single : Options::None)
+            {
+            }
+
+            MapFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const Map<K, T> &map_, Options options_): MapFlag(group_, name_, help_, std::move(matcher_), map_, T(), options_)
+            {
+            }
+
+            virtual ~MapFlag() {}
+
+            virtual void ParseValue(const std::vector<std::string> &values_) override
+            {
+                const std::string &value_ = values_.at(0);
+
+                K key;
+#ifdef ARGS_NOEXCEPT
+                if (!reader(name, value_, key))
+                {
+                    error = Error::Parse;
+                }
+#else
+                reader(name, value_, key);
+#endif
+                auto it = map.find(key);
+                if (it == std::end(map))
+                {
+                    std::ostringstream problem;
+                    problem << "Could not find key '" << key << "' in map for arg '" << name << "'";
+#ifdef ARGS_NOEXCEPT
+                    error = Error::Map;
+                    errorMsg = problem.str();
+#else
+                    throw MapError(problem.str());
+#endif
+                } else
+                {
+                    this->value = it->second;
+                }
+            }
+
+            /** Get the value
+             */
+            T &Get() noexcept
+            {
+                return value;
+            }
+
+            virtual void Reset() noexcept override
+            {
+                ValueFlagBase::Reset();
+                value = defaultValue;
+            }
+    };
+
+    /** A mapping value flag list class
+     * 
+     * \tparam K the type to extract the argument as
+     * \tparam T the type to store the result as
+     * \tparam List the list type that houses the values
+     * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined)
+     * \tparam Map The Map type.  Should operate like std::map or std::unordered_map
+     */
+    template <
+        typename K,
+        typename T,
+        template <typename...> class List = std::vector,
+        typename Reader = ValueReader,
+        template <typename...> class Map = std::unordered_map>
+    class MapFlagList : public ValueFlagBase
+    {
+        private:
+            using Container = List<T>;
+            const Map<K, T> map;
+            Container values;
+            const Container defaultValues;
+            Reader reader;
+
+        protected:
+            virtual std::vector<std::string> GetChoicesStrings(const HelpParams &) const override
+            {
+                return detail::MapKeysToStrings(map);
+            }
+
+        public:
+            typedef T value_type;
+            typedef typename Container::allocator_type allocator_type;
+            typedef typename Container::pointer pointer;
+            typedef typename Container::const_pointer const_pointer;
+            typedef T& reference;
+            typedef const T& const_reference;
+            typedef typename Container::size_type size_type;
+            typedef typename Container::difference_type difference_type;
+            typedef typename Container::iterator iterator;
+            typedef typename Container::const_iterator const_iterator;
+            typedef std::reverse_iterator<iterator> reverse_iterator;
+            typedef std::reverse_iterator<const_iterator> const_reverse_iterator;
+
+            MapFlagList(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const Map<K, T> &map_, const Container &defaultValues_ = Container()): ValueFlagBase(name_, help_, std::move(matcher_)), map(map_), values(defaultValues_), defaultValues(defaultValues_)
+            {
+                group_.Add(*this);
+            }
+
+            virtual ~MapFlagList() {}
+
+            virtual void ParseValue(const std::vector<std::string> &values_) override
+            {
+                const std::string &value = values_.at(0);
+
+                K key;
+#ifdef ARGS_NOEXCEPT
+                if (!reader(name, value, key))
+                {
+                    error = Error::Parse;
+                }
+#else
+                reader(name, value, key);
+#endif
+                auto it = map.find(key);
+                if (it == std::end(map))
+                {
+                    std::ostringstream problem;
+                    problem << "Could not find key '" << key << "' in map for arg '" << name << "'";
+#ifdef ARGS_NOEXCEPT
+                    error = Error::Map;
+                    errorMsg = problem.str();
+#else
+                    throw MapError(problem.str());
+#endif
+                } else
+                {
+                    this->values.emplace_back(it->second);
+                }
+            }
+
+            /** Get the value
+             */
+            Container &Get() noexcept
+            {
+                return values;
+            }
+
+            virtual std::string Name() const override
+            {
+                return name + std::string("...");
+            }
+
+            virtual void Reset() noexcept override
+            {
+                ValueFlagBase::Reset();
+                values = defaultValues;
+            }
+
+            virtual FlagBase *Match(const EitherFlag &arg) override
+            {
+                const bool wasMatched = Matched();
+                auto me = FlagBase::Match(arg);
+                if (me && !wasMatched)
+                {
+                    values.clear();
+                }
+                return me;
+            }
+
+            iterator begin() noexcept
+            {
+                return values.begin();
+            }
+
+            const_iterator begin() const noexcept
+            {
+                return values.begin();
+            }
+
+            const_iterator cbegin() const noexcept
+            {
+                return values.cbegin();
+            }
+
+            iterator end() noexcept
+            {
+                return values.end();
+            }
+
+            const_iterator end() const noexcept 
+            {
+                return values.end();
+            }
+
+            const_iterator cend() const noexcept
+            {
+                return values.cend();
+            }
+    };
+
+    /** A positional argument class
+     *
+     * \tparam T the type to extract the argument as
+     * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined)
+     */
+    template <
+        typename T,
+        typename Reader = ValueReader>
+    class Positional : public PositionalBase
+    {
+        private:
+            T value;
+            const T defaultValue;
+            Reader reader;
+        public:
+            Positional(Group &group_, const std::string &name_, const std::string &help_, const T &defaultValue_ = T(), Options options_ = {}): PositionalBase(name_, help_, options_), value(defaultValue_), defaultValue(defaultValue_)
+            {
+                group_.Add(*this);
+            }
+
+            Positional(Group &group_, const std::string &name_, const std::string &help_, Options options_): Positional(group_, name_, help_, T(), options_)
+            {
+            }
+
+            virtual ~Positional() {}
+
+            virtual void ParseValue(const std::string &value_) override
+            {
+#ifdef ARGS_NOEXCEPT
+                if (!reader(name, value_, this->value))
+                {
+                    error = Error::Parse;
+                }
+#else
+                reader(name, value_, this->value);
+#endif
+                ready = false;
+                matched = true;
+            }
+
+            /** Get the value
+             */
+            T &Get() noexcept
+            {
+                return value;
+            }
+
+            virtual void Reset() noexcept override
+            {
+                PositionalBase::Reset();
+                value = defaultValue;
+            }
+    };
+
+    /** A positional argument class that pushes the found values into a list
+     * 
+     * \tparam T the type to extract the argument as
+     * \tparam List the list type that houses the values
+     * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined)
+     */
+    template <
+        typename T,
+        template <typename...> class List = std::vector,
+        typename Reader = ValueReader>
+    class PositionalList : public PositionalBase
+    {
+        private:
+            using Container = List<T>;
+            Container values;
+            const Container defaultValues;
+            Reader reader;
+
+        public:
+            typedef T value_type;
+            typedef typename Container::allocator_type allocator_type;
+            typedef typename Container::pointer pointer;
+            typedef typename Container::const_pointer const_pointer;
+            typedef T& reference;
+            typedef const T& const_reference;
+            typedef typename Container::size_type size_type;
+            typedef typename Container::difference_type difference_type;
+            typedef typename Container::iterator iterator;
+            typedef typename Container::const_iterator const_iterator;
+            typedef std::reverse_iterator<iterator> reverse_iterator;
+            typedef std::reverse_iterator<const_iterator> const_reverse_iterator;
+
+            PositionalList(Group &group_, const std::string &name_, const std::string &help_, const Container &defaultValues_ = Container(), Options options_ = {}): PositionalBase(name_, help_, options_), values(defaultValues_), defaultValues(defaultValues_)
+            {
+                group_.Add(*this);
+            }
+
+            PositionalList(Group &group_, const std::string &name_, const std::string &help_, Options options_): PositionalList(group_, name_, help_, {}, options_)
+            {
+            }
+
+            virtual ~PositionalList() {}
+
+            virtual void ParseValue(const std::string &value_) override
+            {
+                T v;
+#ifdef ARGS_NOEXCEPT
+                if (!reader(name, value_, v))
+                {
+                    error = Error::Parse;
+                }
+#else
+                reader(name, value_, v);
+#endif
+                values.insert(std::end(values), v);
+                matched = true;
+            }
+
+            virtual std::string Name() const override
+            {
+                return name + std::string("...");
+            }
+
+            /** Get the values
+             */
+            Container &Get() noexcept
+            {
+                return values;
+            }
+
+            virtual void Reset() noexcept override
+            {
+                PositionalBase::Reset();
+                values = defaultValues;
+            }
+
+            virtual PositionalBase *GetNextPositional() override
+            {
+                const bool wasMatched = Matched();
+                auto me = PositionalBase::GetNextPositional();
+                if (me && !wasMatched)
+                {
+                    values.clear();
+                }
+                return me;
+            }
+
+            iterator begin() noexcept
+            {
+                return values.begin();
+            }
+
+            const_iterator begin() const noexcept
+            {
+                return values.begin();
+            }
+
+            const_iterator cbegin() const noexcept
+            {
+                return values.cbegin();
+            }
+
+            iterator end() noexcept
+            {
+                return values.end();
+            }
+
+            const_iterator end() const noexcept 
+            {
+                return values.end();
+            }
+
+            const_iterator cend() const noexcept
+            {
+                return values.cend();
+            }
+    };
+
+    /** A positional argument mapping class
+     * 
+     * \tparam K the type to extract the argument as
+     * \tparam T the type to store the result as
+     * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined)
+     * \tparam Map The Map type.  Should operate like std::map or std::unordered_map
+     */
+    template <
+        typename K,
+        typename T,
+        typename Reader = ValueReader,
+        template <typename...> class Map = std::unordered_map>
+    class MapPositional : public PositionalBase
+    {
+        private:
+            const Map<K, T> map;
+            T value;
+            const T defaultValue;
+            Reader reader;
+
+        protected:
+            virtual std::vector<std::string> GetChoicesStrings(const HelpParams &) const override
+            {
+                return detail::MapKeysToStrings(map);
+            }
+
+        public:
+
+            MapPositional(Group &group_, const std::string &name_, const std::string &help_, const Map<K, T> &map_, const T &defaultValue_ = T(), Options options_ = {}):
+                PositionalBase(name_, help_, options_), map(map_), value(defaultValue_), defaultValue(defaultValue_)
+            {
+                group_.Add(*this);
+            }
+
+            virtual ~MapPositional() {}
+
+            virtual void ParseValue(const std::string &value_) override
+            {
+                K key;
+#ifdef ARGS_NOEXCEPT
+                if (!reader(name, value_, key))
+                {
+                    error = Error::Parse;
+                }
+#else
+                reader(name, value_, key);
+#endif
+                auto it = map.find(key);
+                if (it == std::end(map))
+                {
+                    std::ostringstream problem;
+                    problem << "Could not find key '" << key << "' in map for arg '" << name << "'";
+#ifdef ARGS_NOEXCEPT
+                    error = Error::Map;
+                    errorMsg = problem.str();
+#else
+                    throw MapError(problem.str());
+#endif
+                } else
+                {
+                    this->value = it->second;
+                    ready = false;
+                    matched = true;
+                }
+            }
+
+            /** Get the value
+             */
+            T &Get() noexcept
+            {
+                return value;
+            }
+
+            virtual void Reset() noexcept override
+            {
+                PositionalBase::Reset();
+                value = defaultValue;
+            }
+    };
+
+    /** A positional argument mapping list class
+     * 
+     * \tparam K the type to extract the argument as
+     * \tparam T the type to store the result as
+     * \tparam List the list type that houses the values
+     * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined)
+     * \tparam Map The Map type.  Should operate like std::map or std::unordered_map
+     */
+    template <
+        typename K,
+        typename T,
+        template <typename...> class List = std::vector,
+        typename Reader = ValueReader,
+        template <typename...> class Map = std::unordered_map>
+    class MapPositionalList : public PositionalBase
+    {
+        private:
+            using Container = List<T>;
+
+            const Map<K, T> map;
+            Container values;
+            const Container defaultValues;
+            Reader reader;
+
+        protected:
+            virtual std::vector<std::string> GetChoicesStrings(const HelpParams &) const override
+            {
+                return detail::MapKeysToStrings(map);
+            }
+
+        public:
+            typedef T value_type;
+            typedef typename Container::allocator_type allocator_type;
+            typedef typename Container::pointer pointer;
+            typedef typename Container::const_pointer const_pointer;
+            typedef T& reference;
+            typedef const T& const_reference;
+            typedef typename Container::size_type size_type;
+            typedef typename Container::difference_type difference_type;
+            typedef typename Container::iterator iterator;
+            typedef typename Container::const_iterator const_iterator;
+            typedef std::reverse_iterator<iterator> reverse_iterator;
+            typedef std::reverse_iterator<const_iterator> const_reverse_iterator;
+
+            MapPositionalList(Group &group_, const std::string &name_, const std::string &help_, const Map<K, T> &map_, const Container &defaultValues_ = Container(), Options options_ = {}):
+                PositionalBase(name_, help_, options_), map(map_), values(defaultValues_), defaultValues(defaultValues_)
+            {
+                group_.Add(*this);
+            }
+
+            virtual ~MapPositionalList() {}
+
+            virtual void ParseValue(const std::string &value_) override
+            {
+                K key;
+#ifdef ARGS_NOEXCEPT
+                if (!reader(name, value_, key))
+                {
+                    error = Error::Parse;
+                }
+#else
+                reader(name, value_, key);
+#endif
+                auto it = map.find(key);
+                if (it == std::end(map))
+                {
+                    std::ostringstream problem;
+                    problem << "Could not find key '" << key << "' in map for arg '" << name << "'";
+#ifdef ARGS_NOEXCEPT
+                    error = Error::Map;
+                    errorMsg = problem.str();
+#else
+                    throw MapError(problem.str());
+#endif
+                } else
+                {
+                    this->values.emplace_back(it->second);
+                    matched = true;
+                }
+            }
+
+            /** Get the value
+             */
+            Container &Get() noexcept
+            {
+                return values;
+            }
+
+            virtual std::string Name() const override
+            {
+                return name + std::string("...");
+            }
+
+            virtual void Reset() noexcept override
+            {
+                PositionalBase::Reset();
+                values = defaultValues;
+            }
+
+            virtual PositionalBase *GetNextPositional() override
+            {
+                const bool wasMatched = Matched();
+                auto me = PositionalBase::GetNextPositional();
+                if (me && !wasMatched)
+                {
+                    values.clear();
+                }
+                return me;
+            }
+
+            iterator begin() noexcept
+            {
+                return values.begin();
+            }
+
+            const_iterator begin() const noexcept
+            {
+                return values.begin();
+            }
+
+            const_iterator cbegin() const noexcept
+            {
+                return values.cbegin();
+            }
+
+            iterator end() noexcept
+            {
+                return values.end();
+            }
+
+            const_iterator end() const noexcept 
+            {
+                return values.end();
+            }
+
+            const_iterator cend() const noexcept
+            {
+                return values.cend();
+            }
+    };
+}
+
+#endif
diff --git a/skia/font_converter/include/util.hpp b/skia/font_converter/include/util.hpp
new file mode 100644
index 0000000..80cad97
--- /dev/null
+++ b/skia/font_converter/include/util.hpp
@@ -0,0 +1,6 @@
+#ifndef UTIL_HPP
+#define UTIL_HPP
+
+int nextMultipleOf(float number, int multiple_of);
+
+#endif
\ No newline at end of file
diff --git a/skia/font_converter/include/util.hxx b/skia/font_converter/include/util.hxx
new file mode 100644
index 0000000..d1c86e8
--- /dev/null
+++ b/skia/font_converter/include/util.hxx
@@ -0,0 +1,48 @@
+#ifndef UTIL_HXX
+#define UTIL_HXX
+
+#include <string>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <stdexcept>
+#include <memory>
+
+template <typename... Args>
+std::string string_format(const std::string& format, Args... args)
+{
+	int size = snprintf(nullptr, 0, format.c_str(), args...) +
+	           1; // Extra space for '\0'
+	if (size <= 0)
+	{
+		throw std::runtime_error("Error during formatting.");
+	}
+	std::unique_ptr<char[]> buf(new char[size]);
+	snprintf(buf.get(), size, format.c_str(), args...);
+	return std::string(buf.get(),
+	                   buf.get() + size - 1); // We don't want the '\0' inside
+}
+
+// QUESTION: inline looks fun. so this copy pastes the code around i guess, but
+// its considered faster than referencing code?
+inline bool file_exists(const std::string& name)
+{
+	// https://stackoverflow.com/questions/12774207/fastest-way-to-check-if-a-file-exist-using-standard-c-c11-c
+	struct stat buffer;
+	return (stat(name.c_str(), &buffer) == 0);
+}
+
+inline int valueOrDefault(int value, int default_value)
+{
+	return value <= 0 ? default_value : value;
+}
+
+inline void scale(int* value, int targetValue, int* otherValue)
+{
+	if (*value != targetValue)
+	{
+		*otherValue = *otherValue * targetValue / *value;
+		*value = targetValue;
+	}
+}
+
+#endif
\ No newline at end of file
diff --git a/skia/font_converter/run.sh b/skia/font_converter/run.sh
new file mode 100755
index 0000000..3e0c5e3
--- /dev/null
+++ b/skia/font_converter/run.sh
@@ -0,0 +1 @@
+./build/macosx/bin/debug/font_converter
\ No newline at end of file
diff --git a/skia/font_converter/src/font.cpp b/skia/font_converter/src/font.cpp
new file mode 100644
index 0000000..80a7eca
--- /dev/null
+++ b/skia/font_converter/src/font.cpp
@@ -0,0 +1,612 @@
+#include <cassert>
+#include "font.h"
+#include "include/core/SkFont.h"
+
+uint16_t RiveFont::charToGlyph(SkUnichar u) const
+{
+    for (size_t i = 0; i < fCMap.size(); ++i)
+    {
+        if (fCMap[i].fChar == u)
+        {
+            return fCMap[i].fGlyph;
+        }
+    }
+    return 0;
+}
+
+float RiveFont::advance(uint16_t glyph) const
+{
+    assert(glyph < fGlyphs.size());
+    return fGlyphs[glyph].fAdvance;
+}
+
+const SkPath* RiveFont::path(uint16_t glyph) const
+{
+    assert(glyph < fGlyphs.size());
+    const SkPath& p = fGlyphs[glyph].fPath;
+    return p.isEmpty() ? nullptr : &p;
+}
+
+#define kSignature 0x23581321
+#define kVersion 1
+
+constexpr uint32_t Tag(unsigned a, unsigned b, unsigned c, unsigned d)
+{
+    assert((a & 0xFF) == a);
+    assert((b & 0xFF) == b);
+    assert((c & 0xFF) == c);
+    assert((d & 0xFF) == d);
+    return (a << 24) | (b << 16) | (c << 8) | d;
+}
+
+static inline void tag_to_str(uint32_t tag, char str[5])
+{
+    str[0] = (tag >> 24) & 0xFF;
+    str[1] = (tag >> 16) & 0xFF;
+    str[2] = (tag >> 8) & 0xFF;
+    str[3] = (tag >> 0) & 0xFF;
+    str[4] = 0;
+}
+
+constexpr uint32_t kOffsets_TableTag = Tag('p', 'o', 'f', 'f');
+constexpr uint32_t kPaths_TableTag = Tag('p', 'a', 't', 'h');
+constexpr uint32_t kCMap_TableTag = Tag('c', 'm', 'a', 'p');
+constexpr uint32_t kInfo_TableTag = Tag('i', 'n', 'f', 'o');
+constexpr uint32_t kAdvances_TableTag = Tag('h', 'a', 'd', 'v');
+
+struct TableDir
+{
+    uint32_t tag;
+    uint32_t offset;
+    uint32_t length;
+};
+
+struct FontHead
+{
+    uint32_t signature;
+    uint32_t version;
+    uint32_t tableCount;
+    // TableDir[dirCount]
+
+    const TableDir* dir() const { return (const TableDir*)(this + 1); }
+
+    const void* findTable(uint32_t tag) const
+    {
+        auto dir = this->dir();
+        for (unsigned i = 0; i < this->tableCount; ++i)
+        {
+            if (dir[i].tag == tag)
+            {
+                return (char*)this + dir[i].offset;
+            }
+        }
+        return nullptr;
+    }
+};
+
+struct InfoTable
+{
+    uint16_t glyphCount;
+    uint16_t upem;
+};
+
+struct GlyphOffsetTable
+{
+    //  uint32_t offsets[glyphCount+1]; // relative to the glyphdata table
+};
+
+struct GlyphRec
+{
+    // uint16_t verbCount;
+    // uint16_t pointCount; // if verbCount > 0
+    // uint8_t  verbs;       // Move, Line, Quad, Cubic, Close
+    // pad16[]
+    // uint16_t x,y x,y x,y
+};
+
+void RiveFont::load(sk_sp<SkTypeface> tf, const char str[], size_t len)
+{
+    this->clear();
+
+    SkFont font(std::move(tf), 1.0f);
+
+    uint16_t glyphIDs[len];
+    int glyphCount = font.textToGlyphs(str, len, SkTextEncoding::kUTF8, glyphIDs, len);
+    assert(glyphCount == (int)len);
+
+    struct Rec
+    {
+        uint16_t charCode;
+        uint16_t srcGlyph;
+        uint16_t dstGlyph;
+    };
+    std::vector<Rec> rec;
+    uint16_t newDstGlyphID = 1; // leave room for glyphID==0 for missing glyph
+    // build vector of unique chars
+    for (size_t i = 0; i < len; ++i)
+    {
+        uint16_t code = str[i];
+        auto iter = std::find_if(rec.begin(), rec.end(), [code](const auto& r) {
+            return r.charCode == code;
+        });
+        if (iter == rec.end())
+        {
+            // gonna add code -- now see if its glyph is unique
+            uint16_t srcGlyph = glyphIDs[i];
+            auto it2 = std::find_if(rec.begin(), rec.end(), [srcGlyph](const auto& r) {
+                return r.srcGlyph == srcGlyph;
+            });
+            uint16_t dstGlyph;
+            if (it2 == rec.end())
+            {
+                // srcGlyph is unique (or zero)
+                dstGlyph = srcGlyph ? newDstGlyphID++ : 0;
+            }
+            else
+            {
+                dstGlyph = it2->dstGlyph; // reuse prev dstGlyph
+            }
+            rec.push_back({code, srcGlyph, dstGlyph});
+        }
+    }
+
+    std::sort(rec.begin(), rec.end(), [](const Rec& a, const Rec& b) {
+        return a.charCode < b.charCode;
+    });
+    for (const auto& r : rec)
+    {
+        printf("'%c' [%d] %d -> %d\n", r.charCode, r.charCode, r.srcGlyph, r.dstGlyph);
+        fCMap.push_back({r.charCode, r.dstGlyph});
+    }
+
+    std::sort(rec.begin(), rec.end(), [](const Rec& a, const Rec& b) {
+        return a.dstGlyph < b.dstGlyph;
+    });
+
+    font.setLinearMetrics(true);
+    auto append_glyph = [&](uint16_t srcGlyph) {
+        float width;
+        font.getWidths(&srcGlyph, 1, &width);
+        SkPath path;
+        font.getPath(srcGlyph, &path); // returns false if glyph is bitmap
+
+        fGlyphs.push_back({std::move(path), width});
+    };
+
+    append_glyph(0); // missing glyph
+    for (int i = 1; i < newDstGlyphID; ++i)
+    { // walk through our glyphs
+        auto iter =
+            std::find_if(rec.begin(), rec.end(), [i](const auto& r) { return r.dstGlyph == i; });
+        assert(iter != rec.end());
+        append_glyph(iter->srcGlyph);
+    }
+}
+
+struct ByteBuilder
+{
+    std::vector<uint8_t> bytes;
+
+    uint32_t length() const { return bytes.size(); }
+
+    uint8_t add8(size_t x)
+    {
+        assert((x & 0xFF) == x);
+        uint8_t b = (uint8_t)x;
+        bytes.push_back(b);
+        return b;
+    }
+    int16_t addS16(ssize_t x)
+    {
+        int16_t s = (int16_t)x;
+        assert(s == x);
+        this->add(&s, 2);
+        return s;
+    }
+    uint16_t addU16(size_t x)
+    {
+        assert((x & 0xFFFF) == x);
+        uint16_t u = (uint16_t)x;
+        this->add(&u, 2);
+        return u;
+    }
+    uint32_t addU32(size_t x)
+    {
+        assert((x & 0xFFFFFFFF) == x);
+        uint32_t u = (uint32_t)x;
+        this->add(&u, 4);
+        return u;
+    }
+
+    void add(const void* src, size_t n)
+    {
+        size_t size = bytes.size();
+        bytes.resize(size + n);
+        memcpy(bytes.data() + size, src, n);
+    }
+    template <typename T> void add(const std::vector<T>& v)
+    {
+        this->add(v.data(), v.size() * sizeof(T));
+    }
+    void add(const ByteBuilder& bb) { this->add(bb.bytes.data(), bb.bytes.size()); }
+    void add(sk_sp<SkData> data) { this->add(data->data(), data->size()); }
+
+    void padTo16()
+    {
+        if (bytes.size() & 1)
+        {
+            this->add8(0);
+        }
+    }
+    void padTo32()
+    {
+        while (bytes.size() & 3)
+        {
+            this->add8(0);
+        }
+    }
+
+    void set32(size_t offset, uint32_t value)
+    {
+        assert(offset + 4 <= bytes.size());
+        assert((offset & 3) == 0);
+        memcpy(bytes.data() + offset, &value, 4);
+    }
+
+    sk_sp<SkData> detach()
+    {
+        auto data = SkData::MakeWithCopy(bytes.data(), bytes.size());
+        bytes.clear();
+        return data;
+    }
+};
+
+void encode_path(ByteBuilder* bb, const SkPath& path, float scale)
+{
+    std::vector<uint8_t> varray;
+    std::vector<uint16_t> parray;
+
+    auto add_point = [&](SkPoint p) {
+        parray.push_back(SkScalarRoundToInt(p.fX * scale));
+        parray.push_back(SkScalarRoundToInt(p.fY * scale));
+    };
+
+    SkPath::RawIter iter(path);
+    for (;;)
+    {
+        SkPoint pts[4];
+        auto verb = iter.next(pts);
+        if (verb == SkPath::kDone_Verb)
+        {
+            break;
+        }
+        varray.push_back(verb);
+        switch ((SkPathVerb)verb)
+        {
+            case SkPathVerb::kMove:
+                add_point(pts[0]);
+                break;
+            case SkPathVerb::kLine:
+                add_point(pts[1]);
+                break;
+            case SkPathVerb::kQuad:
+                add_point(pts[1]);
+                add_point(pts[2]);
+                break;
+            case SkPathVerb::kCubic:
+                add_point(pts[1]);
+                add_point(pts[2]);
+                add_point(pts[3]);
+                break;
+            case SkPathVerb::kClose:
+                break;
+            default:
+                assert(false); // unsupported
+        }
+    }
+    assert((int)varray.size() == path.countVerbs());
+    assert((int)parray.size() == path.countPoints() * 2);
+
+    auto no_useful_verbs = [](const std::vector<uint8_t>& verbs) {
+        for (auto v : verbs)
+        {
+            switch ((SkPathVerb)v)
+            {
+                case SkPathVerb::kLine:
+                case SkPathVerb::kQuad:
+                case SkPathVerb::kCubic:
+                    return false;
+                default:
+                    break;
+            }
+        }
+        return true;
+    };
+    if (no_useful_verbs(varray))
+    {
+        return; // we signal empty paths with the offset table
+    }
+
+    bb->addU16(varray.size());
+    assert((parray.size() & 1) == 0);
+    bb->addU16(parray.size() / 2); // #points == 2 * #values
+
+    bb->add(varray);
+    bb->padTo16();
+    bb->add(parray);
+}
+
+sk_sp<SkData> RiveFont::encode() const
+{
+    sk_sp<SkData> infoD, cmapD, offsetsD, pathsD;
+    const int upem = 2048;
+    const float scale = upem;
+
+    struct DirRec
+    {
+        uint32_t tag;
+        sk_sp<SkData> data;
+    };
+    std::vector<DirRec> dir;
+
+    {
+        InfoTable itable;
+        itable.glyphCount = fGlyphs.size();
+        itable.upem = upem;
+        dir.push_back({kInfo_TableTag, SkData::MakeWithCopy(&itable, sizeof(itable))});
+    }
+
+    {
+        ByteBuilder cmap;
+        // start with #pairs
+        cmap.addU16(fCMap.size());
+        for (const auto& cm : fCMap)
+        {
+            cmap.addU16(cm.fChar);
+            cmap.addU16(cm.fGlyph);
+        }
+        dir.push_back({kCMap_TableTag, cmap.detach()});
+    }
+
+    // todo: store offset[i] for glyph[i+1] since first offset is always 0
+    {
+        ByteBuilder paths, advances;
+        std::vector<uint32_t> offsets;
+        for (const auto& g : fGlyphs)
+        {
+            offsets.push_back(paths.length());
+            encode_path(&paths, g.fPath, scale);
+            paths.padTo16();
+            advances.addU16(SkScalarRoundToInt(g.fAdvance * scale));
+        }
+        offsets.push_back(paths.length()); // store N+1 offsets
+
+        dir.push_back(
+            {kOffsets_TableTag, SkData::MakeWithCopy(offsets.data(), offsets.size() * 4)});
+        dir.push_back({kPaths_TableTag, paths.detach()});
+        dir.push_back({kAdvances_TableTag, advances.detach()});
+    }
+
+    std::sort(dir.begin(), dir.end(), [](const DirRec& a, const DirRec& b) {
+        return a.tag < b.tag;
+    });
+
+    ByteBuilder header;
+    header.addU32(kSignature);
+    header.addU32(kVersion);
+    header.addU32(dir.size());
+    for (auto& d : dir)
+    {
+        header.addU32(d.tag);
+        header.addU32(0); // offset -- fill in later
+        header.addU32(d.data->size());
+    }
+
+    size_t offsetToDirEntry = sizeof(FontHead);
+    for (auto& d : dir)
+    {
+        // +4 to skip the tag field of the dir entry
+        header.set32(offsetToDirEntry + 4, header.length());
+        offsetToDirEntry += sizeof(TableDir);
+
+        header.add(d.data);
+        header.padTo32();
+    }
+    return header.detach();
+}
+
+struct Reader
+{
+    const char* fStart;
+    const char* fCurr;
+    const char* fStop;
+
+    Reader(const void* data, size_t length)
+    {
+        fStart = (const char*)data;
+        fCurr = fStart;
+        fStop = fStart + length;
+    }
+
+    bool isAvailable(size_t n) const { return fCurr + n <= fStop; }
+    size_t available() const { return fStop - fCurr; }
+
+    template <typename T> const T* skip(size_t size)
+    {
+        assert(this->isAvailable(size));
+        const char* p = fCurr;
+        fCurr += size;
+        return reinterpret_cast<const T*>(p);
+    }
+
+    template <typename T> const T* skip() { return this->skip<T>(sizeof(T)); }
+
+    void skip(size_t size) {}
+
+    uint8_t u8()
+    {
+        assert(this->isAvailable(1));
+        return *fCurr++;
+    }
+
+    int16_t s16()
+    {
+        assert(this->isAvailable(2));
+        int16_t s;
+        memcpy(&s, fCurr, 2);
+        fCurr += 2;
+        return s;
+    }
+    uint16_t u16()
+    {
+        assert(this->isAvailable(2));
+        uint16_t s;
+        memcpy(&s, fCurr, 2);
+        fCurr += 2;
+        return s;
+    }
+    uint32_t u32()
+    {
+        assert(this->isAvailable(4));
+        uint32_t s;
+        memcpy(&s, fCurr, 4);
+        fCurr += 4;
+        return s;
+    }
+
+    void read(void* dst, size_t size)
+    {
+        assert(this->isAvailable(size));
+        memcpy(dst, fCurr, size);
+        fCurr += size;
+    }
+
+    void skipPad16()
+    {
+        size_t amount = fCurr - fStart;
+        if (amount & 1)
+        {
+            assert(this->isAvailable(1));
+            fCurr += 1;
+        }
+    }
+};
+
+static int compute_point_count(const uint8_t verbs[], int verbCount)
+{
+    int count = 0;
+    for (int i = 0; i < verbCount; ++i)
+    {
+        switch ((SkPathVerb)verbs[i])
+        {
+            case SkPathVerb::kMove:
+                count += 1;
+                break;
+            case SkPathVerb::kLine:
+                count += 1;
+                break;
+            case SkPathVerb::kQuad:
+                count += 2;
+                break;
+            case SkPathVerb::kCubic:
+                count += 3;
+                break;
+            case SkPathVerb::kClose:
+                count += 0;
+                break;
+            default:
+                assert(false);
+                return -1;
+        }
+    }
+    return count;
+}
+
+static SkPath decode_path(const void* data, size_t length, float scale)
+{
+    Reader reader(data, length);
+
+    const int verbCount = reader.u16();
+    assert(verbCount > 0);
+    const int pointCount = reader.u16();
+
+    auto verbs = reader.skip<uint8_t>(verbCount);
+    reader.skipPad16();
+    const int computedPointCount = compute_point_count(verbs, verbCount);
+    assert(pointCount == computedPointCount);
+
+    auto pts16 = reader.skip<int16_t>(pointCount * 2 * sizeof(int16_t));
+    assert(reader.available() == 0);
+
+    SkPoint pts[pointCount];
+    for (int i = 0; i < pointCount; ++i)
+    {
+        pts[i] = {pts16[0] * scale, pts16[1] * scale};
+        pts16 += 2;
+    }
+
+    return SkPath::Make(pts, pointCount, verbs, verbCount, nullptr, 0, SkPathFillType::kWinding);
+}
+
+bool RiveFont::decode(const void* data, size_t length)
+{
+    Reader reader(data, length);
+
+    auto font = reader.skip<FontHead>();
+    assert(font->signature == kSignature);
+    assert(font->version == kVersion);
+    assert(font->tableCount >= 0);
+
+    assert(reader.isAvailable(font->tableCount * sizeof(TableDir)));
+
+    if (true)
+    {
+        auto dir = font->dir();
+        for (unsigned i = 0; i < font->tableCount; ++i)
+        {
+            char str[5];
+            tag_to_str(dir[i].tag, str);
+            printf("[%d] {%0x08 %s %10d} %d %d\n",
+                   i,
+                   dir[i].tag,
+                   str,
+                   dir[i].tag,
+                   dir[i].offset,
+                   dir[i].length);
+        }
+    }
+
+    auto info = (const InfoTable*)font->findTable(kInfo_TableTag);
+    auto cmap = (const uint16_t*)font->findTable(kCMap_TableTag);
+    auto paths = (const char*)font->findTable(kPaths_TableTag);
+    auto offsets = (const uint32_t*)font->findTable(kOffsets_TableTag);
+    auto advances = (const uint16_t*)font->findTable(kAdvances_TableTag);
+
+    const int glyphCount = info->glyphCount;
+
+    this->clear();
+
+    const int pairCount = *cmap++;
+    for (int i = 0; i < pairCount; ++i)
+    {
+        uint16_t c = *cmap++;
+        uint16_t g = *cmap++;
+        assert(g < glyphCount);
+
+        fCMap.push_back({c, g});
+    }
+
+    const float scale = 1.0f / (float)info->upem;
+
+    for (int i = 0; i < glyphCount; ++i)
+    {
+        float adv = advances[i] * scale;
+        uint32_t start = offsets[i];
+        uint32_t end = offsets[i + 1];
+        assert(start <= end);
+
+        fGlyphs.push_back(
+            {(start == end) ? SkPath() : decode_path(paths + start, end - start, scale), adv});
+    }
+    return true;
+}
diff --git a/skia/font_converter/src/font.h b/skia/font_converter/src/font.h
new file mode 100644
index 0000000..de6bd02
--- /dev/null
+++ b/skia/font_converter/src/font.h
@@ -0,0 +1,42 @@
+#ifndef RiveFont_DEFINED
+#define RiveFont_DEFINED
+
+#include "include/core/SkData.h"
+#include "include/core/SkPath.h"
+#include "include/core/SkTypeface.h"
+#include <vector>
+
+class RiveFont
+{
+    struct Pair
+    {
+        uint16_t fChar;
+        uint16_t fGlyph;
+    };
+    std::vector<Pair> fCMap;
+
+    struct Glyph
+    {
+        SkPath fPath;
+        float fAdvance;
+    };
+    std::vector<Glyph> fGlyphs;
+
+public:
+    uint16_t charToGlyph(SkUnichar) const;
+    float advance(uint16_t glyph) const;
+    const SkPath* path(uint16_t glyph) const;
+
+    void clear()
+    {
+        fCMap.clear();
+        fGlyphs.clear();
+    }
+
+    void load(sk_sp<SkTypeface>, const char text[], size_t length);
+
+    sk_sp<SkData> encode() const;
+    bool decode(const void*, size_t);
+};
+
+#endif
diff --git a/skia/font_converter/src/font_arguments.hpp b/skia/font_converter/src/font_arguments.hpp
new file mode 100644
index 0000000..c4f95f2
--- /dev/null
+++ b/skia/font_converter/src/font_arguments.hpp
@@ -0,0 +1,79 @@
+#ifndef RECORDER_ARGUMENTS_HPP
+#define RECORDER_ARGUMENTS_HPP
+
+#include <iostream>
+#include <string>
+#include <unordered_map>
+
+#include "args.hxx"
+
+class FontArguments
+{
+
+public:
+    FontArguments(int argc, const char** argv) :
+        m_Parser("Convert a font file into the rive format.", "Experimental....")
+    {
+        args::HelpFlag help(m_Parser, "help", "Display this help menu", {'h', "help"});
+        args::Group required(m_Parser, "required arguments:", args::Group::Validators::All);
+        args::Group optional(m_Parser, "optional arguments:", args::Group::Validators::DontCare);
+
+        args::ValueFlag<std::string> source(required, "path", "source filename", {'s', "source"});
+        args::ValueFlag<std::string> destination(required,
+                                                 "path",
+                                                 "destination filename",
+                                                 {'d', "destination"});
+        args::ValueFlag<std::string> charset(optional,
+                                             "path",
+                                             "charset filename",
+                                             {'c', "charset"});
+
+        args::CompletionFlag completion(m_Parser, {"complete"});
+        try
+        {
+            m_Parser.ParseCLI(argc, argv);
+        }
+        catch (const std::invalid_argument e)
+        {
+            std::cout << e.what();
+            throw;
+        }
+        catch (const args::Completion& e)
+        {
+            std::cout << e.what();
+            throw;
+        }
+        catch (const args::Help&)
+        {
+            std::cout << m_Parser;
+            throw;
+        }
+        catch (const args::ParseError& e)
+        {
+            std::cerr << e.what() << std::endl;
+            std::cerr << m_Parser;
+            throw;
+        }
+        catch (args::ValidationError e)
+        {
+            std::cerr << e.what() << std::endl;
+            std::cerr << m_Parser;
+            throw;
+        }
+
+        m_Destination = args::get(destination);
+        m_Source = args::get(source);
+        m_Charset = args::get(charset);
+    }
+
+    const std::string& destination() const { return m_Destination; }
+    const std::string& source() const { return m_Source; }
+    const std::string& charset() const { return m_Charset; }
+
+private:
+    args::ArgumentParser m_Parser;
+    std::string m_Destination;
+    std::string m_Source;
+    std::string m_Charset;
+};
+#endif
diff --git a/skia/font_converter/src/main.cpp b/skia/font_converter/src/main.cpp
new file mode 100644
index 0000000..32ed66e
--- /dev/null
+++ b/skia/font_converter/src/main.cpp
@@ -0,0 +1,113 @@
+#ifdef TESTING
+#else
+
+#include "font.h"
+#include "font_arguments.hpp"
+#include "include/core/SkData.h"
+#include "include/core/SkFontMgr.h"
+#include "include/core/SkStream.h"
+#include "include/core/SkTypeface.h"
+#include <vector>
+
+static std::vector<char> readFile(const char path[])
+{
+    FILE* fp = fopen(path, "rb");
+
+    if (fp == nullptr)
+    {
+        fclose(fp);
+        std::ostringstream errorStream;
+        errorStream << "Failed to open file " << path;
+        throw std::invalid_argument(errorStream.str());
+    }
+    fseek(fp, 0, SEEK_END);
+    size_t length = ftell(fp);
+    fseek(fp, 0, SEEK_SET);
+
+    std::vector<char> data;
+    data.resize(length);
+
+    if (fread(data.data(), 1, length, fp) != length)
+    {
+        fclose(fp);
+        std::ostringstream errorStream;
+        errorStream << "Failed to read file into bytes array " << path;
+        throw std::invalid_argument(errorStream.str());
+    }
+
+    fclose(fp);
+    return data;
+}
+
+static void writeFile(const char path[], const void* bytes, size_t length)
+{
+    FILE* pFile = fopen(path, "wb");
+    fwrite(bytes, sizeof(char), length, pFile);
+    fclose(pFile);
+}
+
+static std::vector<char> build_default_charset()
+{
+    std::vector<char> charset;
+    for (int i = 32; i < 127; ++i)
+    {
+        charset.push_back(i);
+    }
+    return charset;
+}
+
+int main(int argc, const char* argv[])
+{
+    try
+    {
+        FontArguments args(argc, argv);
+        sk_sp<SkTypeface> typeface;
+
+        auto src = readFile(args.source().c_str());
+        auto srcData = SkData::MakeWithCopy(src.data(), src.size());
+        typeface = SkTypeface::MakeFromData(srcData);
+        if (!typeface)
+        {
+            fprintf(stderr, "Failed to convert file to SkTypeface\n");
+            return 1;
+        }
+
+        std::vector<char> charset;
+        auto charsetFile = args.charset();
+        if (charsetFile.size() > 0)
+        {
+            charset = readFile(args.charset().c_str());
+        }
+        else
+        {
+            charset = build_default_charset();
+        }
+
+        RiveFont font;
+        font.load(typeface, charset.data(), charset.size());
+
+        auto dst = font.encode();
+
+        writeFile(args.destination().c_str(), dst->data(), dst->size());
+    }
+    catch (const args::Completion& e)
+    {
+        return 0;
+    }
+    catch (const args::Help&)
+    {
+        return 0;
+    }
+    catch (const args::ParseError& e)
+    {
+        return 1;
+    }
+    catch (args::ValidationError e)
+    {
+        return 1;
+    }
+
+    return 0;
+}
+
+#endif
diff --git a/skia/pngprefix/pngprefix.h b/skia/pngprefix/pngprefix.h
new file mode 100644
index 0000000..ddc1792
--- /dev/null
+++ b/skia/pngprefix/pngprefix.h
@@ -0,0 +1,465 @@
+/*
+ * Copyright 2016 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ *
+ * Copyright 2023 Rive
+ */
+#ifndef PNGPREFIX_H
+#define PNGPREFIX_H
+/*
+ * This is necessary to build multiple copies of libpng.  We need this while Skia has its own copy
+ * of libpng.
+ */
+#define png_access_version_number sk_png_access_version_number
+#define png_app_error sk_png_app_error
+#define png_app_warning sk_png_app_warning
+#define png_ascii_from_fixed sk_png_ascii_from_fixed
+#define png_ascii_from_fp sk_png_ascii_from_fp
+#define png_benign_error sk_png_benign_error
+#define png_build_gamma_table sk_png_build_gamma_table
+#define png_build_grayscale_palette sk_png_build_grayscale_palette
+#define png_calculate_crc sk_png_calculate_crc
+#define png_calloc sk_png_calloc
+#define png_check_IHDR sk_png_check_IHDR
+#define png_check_chunk_length sk_png_check_chunk_length
+#define png_check_chunk_name sk_png_check_chunk_name
+#define png_check_fp_number sk_png_check_fp_number
+#define png_check_fp_string sk_png_check_fp_string
+#define png_check_keyword sk_png_check_keyword
+#define png_chunk_benign_error sk_png_chunk_benign_error
+#define png_chunk_error sk_png_chunk_error
+#define png_chunk_report sk_png_chunk_report
+#define png_chunk_unknown_handling sk_png_chunk_unknown_handling
+#define png_chunk_warning sk_png_chunk_warning
+#define png_colorspace_set_ICC sk_png_colorspace_set_ICC
+#define png_colorspace_set_chromaticities sk_png_colorspace_set_chromaticities
+#define png_colorspace_set_endpoints sk_png_colorspace_set_endpoints
+#define png_colorspace_set_gamma sk_png_colorspace_set_gamma
+#define png_colorspace_set_rgb_coefficients sk_png_colorspace_set_rgb_coefficients
+#define png_colorspace_set_sRGB sk_png_colorspace_set_sRGB
+#define png_colorspace_sync sk_png_colorspace_sync
+#define png_colorspace_sync_info sk_png_colorspace_sync_info
+#define png_combine_row sk_png_combine_row
+#define png_compress_IDAT sk_png_compress_IDAT
+#define png_convert_from_struct_tm sk_png_convert_from_struct_tm
+#define png_convert_from_time_t sk_png_convert_from_time_t
+#define png_convert_to_rfc1123 sk_png_convert_to_rfc1123
+#define png_convert_to_rfc1123_buffer sk_png_convert_to_rfc1123_buffer
+#define png_crc_error sk_png_crc_error
+#define png_crc_finish sk_png_crc_finish
+#define png_crc_read sk_png_crc_read
+#define png_create_info_struct sk_png_create_info_struct
+#define png_create_png_struct sk_png_create_png_struct
+#define png_create_read_struct sk_png_create_read_struct
+#define png_create_read_struct_2 sk_png_create_read_struct_2
+#define png_create_write_struct sk_png_create_write_struct
+#define png_create_write_struct_2 sk_png_create_write_struct_2
+#define png_data_freer sk_png_data_freer
+#define png_default_flush sk_png_default_flush
+#define png_default_read_data sk_png_default_read_data
+#define png_default_write_data sk_png_default_write_data
+#define png_destroy_gamma_table sk_png_destroy_gamma_table
+#define png_destroy_info_struct sk_png_destroy_info_struct
+#define png_destroy_png_struct sk_png_destroy_png_struct
+#define png_destroy_read_struct sk_png_destroy_read_struct
+#define png_destroy_write_struct sk_png_destroy_write_struct
+#define png_do_bgr sk_png_do_bgr
+#define png_do_check_palette_indexes sk_png_do_check_palette_indexes
+#define png_do_chop sk_png_do_chop
+#define png_do_compose sk_png_do_compose
+#define png_do_encode_alpha sk_png_do_encode_alpha
+#define png_do_expand sk_png_do_expand
+#define png_do_expand_16 sk_png_do_expand_16
+#define png_do_expand_palette sk_png_do_expand_palette
+#define png_do_expand_palette_neon_rgb sk_png_do_expand_palette_neon_rgb
+#define png_do_expand_palette_neon_rgba sk_png_do_expand_palette_neon_rgba
+#define png_do_gamma sk_png_do_gamma
+#define png_do_gray_to_rgb sk_png_do_gray_to_rgb
+#define png_do_invert sk_png_do_invert
+#define png_do_pack sk_png_do_pack
+#define png_do_packswap sk_png_do_packswap
+#define png_do_quantize sk_png_do_quantize
+#define png_do_read_filler sk_png_do_read_filler
+#define png_do_read_interlace sk_png_do_read_interlace
+#define png_do_read_intrapixel sk_png_do_read_intrapixel
+#define png_do_read_invert_alpha sk_png_do_read_invert_alpha
+#define png_do_read_swap_alpha sk_png_do_read_swap_alpha
+#define png_do_read_transformations sk_png_do_read_transformations
+#define png_do_rgb_to_gray sk_png_do_rgb_to_gray
+#define png_do_scale_16_to_8 sk_png_do_scale_16_to_8
+#define png_do_shift sk_png_do_shift
+#define png_do_strip_channel sk_png_do_strip_channel
+#define png_do_swap sk_png_do_swap
+#define png_do_unpack sk_png_do_unpack
+#define png_do_unshift sk_png_do_unshift
+#define png_do_write_interlace sk_png_do_write_interlace
+#define png_do_write_intrapixel sk_png_do_write_intrapixel
+#define png_do_write_invert_alpha sk_png_do_write_invert_alpha
+#define png_do_write_swap_alpha sk_png_do_write_swap_alpha
+#define png_do_write_transformations sk_png_do_write_transformations
+#define png_error sk_png_error
+#define png_fixed sk_png_fixed
+#define png_fixed_error sk_png_fixed_error
+#define png_flush sk_png_flush
+#define png_format_number sk_png_format_number
+#define png_formatted_warning sk_png_formatted_warning
+#define png_free sk_png_free
+#define png_free_buffer_list sk_png_free_buffer_list
+#define png_free_data sk_png_free_data
+#define png_free_default sk_png_free_default
+#define png_free_jmpbuf sk_png_free_jmpbuf
+#define png_gamma_16bit_correct sk_png_gamma_16bit_correct
+#define png_gamma_8bit_correct sk_png_gamma_8bit_correct
+#define png_gamma_correct sk_png_gamma_correct
+#define png_gamma_significant sk_png_gamma_significant
+#define png_get_IHDR sk_png_get_IHDR
+#define png_get_PLTE sk_png_get_PLTE
+#define png_get_bKGD sk_png_get_bKGD
+#define png_get_bit_depth sk_png_get_bit_depth
+#define png_get_cHRM sk_png_get_cHRM
+#define png_get_cHRM_XYZ sk_png_get_cHRM_XYZ
+#define png_get_cHRM_XYZ_fixed sk_png_get_cHRM_XYZ_fixed
+#define png_get_cHRM_fixed sk_png_get_cHRM_fixed
+#define png_get_channels sk_png_get_channels
+#define png_get_chunk_cache_max sk_png_get_chunk_cache_max
+#define png_get_chunk_malloc_max sk_png_get_chunk_malloc_max
+#define png_get_color_type sk_png_get_color_type
+#define png_get_compression_buffer_size sk_png_get_compression_buffer_size
+#define png_get_compression_type sk_png_get_compression_type
+#define png_get_copyright sk_png_get_copyright
+#define png_get_current_pass_number sk_png_get_current_pass_number
+#define png_get_current_row_number sk_png_get_current_row_number
+#define png_get_eXIf sk_png_get_eXIf
+#define png_get_eXIf_1 sk_png_get_eXIf_1
+#define png_get_error_ptr sk_png_get_error_ptr
+#define png_get_filter_type sk_png_get_filter_type
+#define png_get_gAMA sk_png_get_gAMA
+#define png_get_gAMA_fixed sk_png_get_gAMA_fixed
+#define png_get_hIST sk_png_get_hIST
+#define png_get_header_ver sk_png_get_header_ver
+#define png_get_header_version sk_png_get_header_version
+#define png_get_iCCP sk_png_get_iCCP
+#define png_get_image_height sk_png_get_image_height
+#define png_get_image_width sk_png_get_image_width
+#define png_get_int_32 sk_png_get_int_32
+#define png_get_interlace_type sk_png_get_interlace_type
+#define png_get_io_chunk_type sk_png_get_io_chunk_type
+#define png_get_io_ptr sk_png_get_io_ptr
+#define png_get_io_state sk_png_get_io_state
+#define png_get_libpng_ver sk_png_get_libpng_ver
+#define png_get_mem_ptr sk_png_get_mem_ptr
+#define png_get_oFFs sk_png_get_oFFs
+#define png_get_pCAL sk_png_get_pCAL
+#define png_get_pHYs sk_png_get_pHYs
+#define png_get_pHYs_dpi sk_png_get_pHYs_dpi
+#define png_get_palette_max sk_png_get_palette_max
+#define png_get_pixel_aspect_ratio sk_png_get_pixel_aspect_ratio
+#define png_get_pixel_aspect_ratio_fixed sk_png_get_pixel_aspect_ratio_fixed
+#define png_get_pixels_per_inch sk_png_get_pixels_per_inch
+#define png_get_pixels_per_meter sk_png_get_pixels_per_meter
+#define png_get_progressive_ptr sk_png_get_progressive_ptr
+#define png_get_rgb_to_gray_status sk_png_get_rgb_to_gray_status
+#define png_get_rowbytes sk_png_get_rowbytes
+#define png_get_rows sk_png_get_rows
+#define png_get_sBIT sk_png_get_sBIT
+#define png_get_sCAL sk_png_get_sCAL
+#define png_get_sCAL_fixed sk_png_get_sCAL_fixed
+#define png_get_sCAL_s sk_png_get_sCAL_s
+#define png_get_sPLT sk_png_get_sPLT
+#define png_get_sRGB sk_png_get_sRGB
+#define png_get_signature sk_png_get_signature
+#define png_get_tIME sk_png_get_tIME
+#define png_get_tRNS sk_png_get_tRNS
+#define png_get_text sk_png_get_text
+#define png_get_uint_16 sk_png_get_uint_16
+#define png_get_uint_31 sk_png_get_uint_31
+#define png_get_uint_32 sk_png_get_uint_32
+#define png_get_unknown_chunks sk_png_get_unknown_chunks
+#define png_get_user_chunk_ptr sk_png_get_user_chunk_ptr
+#define png_get_user_height_max sk_png_get_user_height_max
+#define png_get_user_transform_ptr sk_png_get_user_transform_ptr
+#define png_get_user_width_max sk_png_get_user_width_max
+#define png_get_valid sk_png_get_valid
+#define png_get_x_offset_inches sk_png_get_x_offset_inches
+#define png_get_x_offset_inches_fixed sk_png_get_x_offset_inches_fixed
+#define png_get_x_offset_microns sk_png_get_x_offset_microns
+#define png_get_x_offset_pixels sk_png_get_x_offset_pixels
+#define png_get_x_pixels_per_inch sk_png_get_x_pixels_per_inch
+#define png_get_x_pixels_per_meter sk_png_get_x_pixels_per_meter
+#define png_get_y_offset_inches sk_png_get_y_offset_inches
+#define png_get_y_offset_inches_fixed sk_png_get_y_offset_inches_fixed
+#define png_get_y_offset_microns sk_png_get_y_offset_microns
+#define png_get_y_offset_pixels sk_png_get_y_offset_pixels
+#define png_get_y_pixels_per_inch sk_png_get_y_pixels_per_inch
+#define png_get_y_pixels_per_meter sk_png_get_y_pixels_per_meter
+#define png_handle_IEND sk_png_handle_IEND
+#define png_handle_IHDR sk_png_handle_IHDR
+#define png_handle_PLTE sk_png_handle_PLTE
+#define png_handle_as_unknown sk_png_handle_as_unknown
+#define png_handle_bKGD sk_png_handle_bKGD
+#define png_handle_cHRM sk_png_handle_cHRM
+#define png_handle_gAMA sk_png_handle_gAMA
+#define png_handle_hIST sk_png_handle_hIST
+#define png_handle_iCCP sk_png_handle_iCCP
+#define png_handle_iTXt sk_png_handle_iTXt
+#define png_handle_oFFs sk_png_handle_oFFs
+#define png_handle_pCAL sk_png_handle_pCAL
+#define png_handle_pHYs sk_png_handle_pHYs
+#define png_handle_sBIT sk_png_handle_sBIT
+#define png_handle_sCAL sk_png_handle_sCAL
+#define png_handle_sPLT sk_png_handle_sPLT
+#define png_handle_sRGB sk_png_handle_sRGB
+#define png_handle_tEXt sk_png_handle_tEXt
+#define png_handle_tIME sk_png_handle_tIME
+#define png_handle_tRNS sk_png_handle_tRNS
+#define png_handle_unknown sk_png_handle_unknown
+#define png_handle_zTXt sk_png_handle_zTXt
+#define png_icc_check_header sk_png_icc_check_header
+#define png_icc_check_length sk_png_icc_check_length
+#define png_icc_check_tag_table sk_png_icc_check_tag_table
+#define png_icc_set_sRGB sk_png_icc_set_sRGB
+#define png_image_begin_read_from_file sk_png_image_begin_read_from_file
+#define png_image_begin_read_from_memory sk_png_image_begin_read_from_memory
+#define png_image_begin_read_from_stdio sk_png_image_begin_read_from_stdio
+#define png_image_error sk_png_image_error
+#define png_image_finish_read sk_png_image_finish_read
+#define png_image_free sk_png_image_free
+#define png_image_write_to_file sk_png_image_write_to_file
+#define png_image_write_to_memory sk_png_image_write_to_memory
+#define png_image_write_to_stdio sk_png_image_write_to_stdio
+#define png_info_init_3 sk_png_info_init_3
+#define png_init_filter_functions_neon sk_png_init_filter_functions_neon
+#define png_init_filter_functions_sse2 sk_png_init_filter_functions_sse2
+#define png_init_io sk_png_init_io
+#define png_init_read_transformations sk_png_init_read_transformations
+#define png_longjmp sk_png_longjmp
+#define png_malloc sk_png_malloc
+#define png_malloc_array sk_png_malloc_array
+#define png_malloc_base sk_png_malloc_base
+#define png_malloc_default sk_png_malloc_default
+#define png_malloc_warn sk_png_malloc_warn
+#define png_muldiv sk_png_muldiv
+#define png_muldiv_warn sk_png_muldiv_warn
+#define png_permit_mng_features sk_png_permit_mng_features
+#define png_process_IDAT_data sk_png_process_IDAT_data
+#define png_process_data sk_png_process_data
+#define png_process_data_pause sk_png_process_data_pause
+#define png_process_data_skip sk_png_process_data_skip
+#define png_process_some_data sk_png_process_some_data
+#define png_progressive_combine_row sk_png_progressive_combine_row
+#define png_push_check_crc sk_png_push_check_crc
+#define png_push_crc_finish sk_png_push_crc_finish
+#define png_push_crc_skip sk_png_push_crc_skip
+#define png_push_fill_buffer sk_png_push_fill_buffer
+#define png_push_handle_iTXt sk_png_push_handle_iTXt
+#define png_push_handle_tEXt sk_png_push_handle_tEXt
+#define png_push_handle_unknown sk_png_push_handle_unknown
+#define png_push_handle_zTXt sk_png_push_handle_zTXt
+#define png_push_have_end sk_png_push_have_end
+#define png_push_have_info sk_png_push_have_info
+#define png_push_have_row sk_png_push_have_row
+#define png_push_process_row sk_png_push_process_row
+#define png_push_read_IDAT sk_png_push_read_IDAT
+#define png_push_read_chunk sk_png_push_read_chunk
+#define png_push_read_end sk_png_push_read_end
+#define png_push_read_iTXt sk_png_push_read_iTXt
+#define png_push_read_sig sk_png_push_read_sig
+#define png_push_read_tEXt sk_png_push_read_tEXt
+#define png_push_read_zTXt sk_png_push_read_zTXt
+#define png_push_restore_buffer sk_png_push_restore_buffer
+#define png_push_save_buffer sk_png_push_save_buffer
+#define png_read_IDAT_data sk_png_read_IDAT_data
+#define png_read_chunk_header sk_png_read_chunk_header
+#define png_read_data sk_png_read_data
+#define png_read_end sk_png_read_end
+#define png_read_filter_row sk_png_read_filter_row
+#define png_read_filter_row_avg3_neon sk_png_read_filter_row_avg3_neon
+#define png_read_filter_row_avg3_sse2 sk_png_read_filter_row_avg3_sse2
+#define png_read_filter_row_avg4_neon sk_png_read_filter_row_avg4_neon
+#define png_read_filter_row_avg4_sse2 sk_png_read_filter_row_avg4_sse2
+#define png_read_filter_row_paeth3_neon sk_png_read_filter_row_paeth3_neon
+#define png_read_filter_row_paeth3_sse2 sk_png_read_filter_row_paeth3_sse2
+#define png_read_filter_row_paeth4_neon sk_png_read_filter_row_paeth4_neon
+#define png_read_filter_row_paeth4_sse2 sk_png_read_filter_row_paeth4_sse2
+#define png_read_filter_row_sub3_neon sk_png_read_filter_row_sub3_neon
+#define png_read_filter_row_sub3_sse2 sk_png_read_filter_row_sub3_sse2
+#define png_read_filter_row_sub4_neon sk_png_read_filter_row_sub4_neon
+#define png_read_filter_row_sub4_sse2 sk_png_read_filter_row_sub4_sse2
+#define png_read_filter_row_up_neon sk_png_read_filter_row_up_neon
+#define png_read_finish_IDAT sk_png_read_finish_IDAT
+#define png_read_finish_row sk_png_read_finish_row
+#define png_read_image sk_png_read_image
+#define png_read_info sk_png_read_info
+#define png_read_png sk_png_read_png
+#define png_read_push_finish_row sk_png_read_push_finish_row
+#define png_read_row sk_png_read_row
+#define png_read_rows sk_png_read_rows
+#define png_read_sig sk_png_read_sig
+#define png_read_start_row sk_png_read_start_row
+#define png_read_transform_info sk_png_read_transform_info
+#define png_read_update_info sk_png_read_update_info
+#define png_realloc_array sk_png_realloc_array
+#define png_reciprocal sk_png_reciprocal
+#define png_reciprocal2 sk_png_reciprocal2
+#define png_reset_crc sk_png_reset_crc
+#define png_reset_zstream sk_png_reset_zstream
+#define png_riffle_palette_rgba sk_png_riffle_palette_rgba
+#define png_sRGB_base sk_png_sRGB_base
+#define png_sRGB_delta sk_png_sRGB_delta
+#define png_sRGB_table sk_png_sRGB_table
+#define png_safe_error sk_png_safe_error
+#define png_safe_execute sk_png_safe_execute
+#define png_safe_warning sk_png_safe_warning
+#define png_safecat sk_png_safecat
+#define png_save_int_32 sk_png_save_int_32
+#define png_save_uint_16 sk_png_save_uint_16
+#define png_save_uint_32 sk_png_save_uint_32
+#define png_set_IHDR sk_png_set_IHDR
+#define png_set_PLTE sk_png_set_PLTE
+#define png_set_add_alpha sk_png_set_add_alpha
+#define png_set_alpha_mode sk_png_set_alpha_mode
+#define png_set_alpha_mode_fixed sk_png_set_alpha_mode_fixed
+#define png_set_bKGD sk_png_set_bKGD
+#define png_set_background sk_png_set_background
+#define png_set_background_fixed sk_png_set_background_fixed
+#define png_set_benign_errors sk_png_set_benign_errors
+#define png_set_bgr sk_png_set_bgr
+#define png_set_cHRM sk_png_set_cHRM
+#define png_set_cHRM_XYZ sk_png_set_cHRM_XYZ
+#define png_set_cHRM_XYZ_fixed sk_png_set_cHRM_XYZ_fixed
+#define png_set_cHRM_fixed sk_png_set_cHRM_fixed
+#define png_set_check_for_invalid_index sk_png_set_check_for_invalid_index
+#define png_set_chunk_cache_max sk_png_set_chunk_cache_max
+#define png_set_chunk_malloc_max sk_png_set_chunk_malloc_max
+#define png_set_compression_buffer_size sk_png_set_compression_buffer_size
+#define png_set_compression_level sk_png_set_compression_level
+#define png_set_compression_mem_level sk_png_set_compression_mem_level
+#define png_set_compression_method sk_png_set_compression_method
+#define png_set_compression_strategy sk_png_set_compression_strategy
+#define png_set_compression_window_bits sk_png_set_compression_window_bits
+#define png_set_crc_action sk_png_set_crc_action
+#define png_set_eXIf sk_png_set_eXIf
+#define png_set_eXIf_1 sk_png_set_eXIf_1
+#define png_set_error_fn sk_png_set_error_fn
+#define png_set_expand sk_png_set_expand
+#define png_set_expand_16 sk_png_set_expand_16
+#define png_set_expand_gray_1_2_4_to_8 sk_png_set_expand_gray_1_2_4_to_8
+#define png_set_filler sk_png_set_filler
+#define png_set_filter sk_png_set_filter
+#define png_set_filter_heuristics sk_png_set_filter_heuristics
+#define png_set_filter_heuristics_fixed sk_png_set_filter_heuristics_fixed
+#define png_set_flush sk_png_set_flush
+#define png_set_gAMA sk_png_set_gAMA
+#define png_set_gAMA_fixed sk_png_set_gAMA_fixed
+#define png_set_gamma sk_png_set_gamma
+#define png_set_gamma_fixed sk_png_set_gamma_fixed
+#define png_set_gray_to_rgb sk_png_set_gray_to_rgb
+#define png_set_hIST sk_png_set_hIST
+#define png_set_iCCP sk_png_set_iCCP
+#define png_set_interlace_handling sk_png_set_interlace_handling
+#define png_set_invalid sk_png_set_invalid
+#define png_set_invert_alpha sk_png_set_invert_alpha
+#define png_set_invert_mono sk_png_set_invert_mono
+#define png_set_keep_unknown_chunks sk_png_set_keep_unknown_chunks
+#define png_set_longjmp_fn sk_png_set_longjmp_fn
+#define png_set_mem_fn sk_png_set_mem_fn
+#define png_set_oFFs sk_png_set_oFFs
+#define png_set_option sk_png_set_option
+#define png_set_pCAL sk_png_set_pCAL
+#define png_set_pHYs sk_png_set_pHYs
+#define png_set_packing sk_png_set_packing
+#define png_set_packswap sk_png_set_packswap
+#define png_set_palette_to_rgb sk_png_set_palette_to_rgb
+#define png_set_progressive_read_fn sk_png_set_progressive_read_fn
+#define png_set_quantize sk_png_set_quantize
+#define png_set_read_fn sk_png_set_read_fn
+#define png_set_read_status_fn sk_png_set_read_status_fn
+#define png_set_read_user_chunk_fn sk_png_set_read_user_chunk_fn
+#define png_set_read_user_transform_fn sk_png_set_read_user_transform_fn
+#define png_set_rgb_to_gray sk_png_set_rgb_to_gray
+#define png_set_rgb_to_gray_fixed sk_png_set_rgb_to_gray_fixed
+#define png_set_rows sk_png_set_rows
+#define png_set_sBIT sk_png_set_sBIT
+#define png_set_sCAL sk_png_set_sCAL
+#define png_set_sCAL_fixed sk_png_set_sCAL_fixed
+#define png_set_sCAL_s sk_png_set_sCAL_s
+#define png_set_sPLT sk_png_set_sPLT
+#define png_set_sRGB sk_png_set_sRGB
+#define png_set_sRGB_gAMA_and_cHRM sk_png_set_sRGB_gAMA_and_cHRM
+#define png_set_scale_16 sk_png_set_scale_16
+#define png_set_shift sk_png_set_shift
+#define png_set_sig_bytes sk_png_set_sig_bytes
+#define png_set_strip_16 sk_png_set_strip_16
+#define png_set_strip_alpha sk_png_set_strip_alpha
+#define png_set_swap sk_png_set_swap
+#define png_set_swap_alpha sk_png_set_swap_alpha
+#define png_set_tIME sk_png_set_tIME
+#define png_set_tRNS sk_png_set_tRNS
+#define png_set_tRNS_to_alpha sk_png_set_tRNS_to_alpha
+#define png_set_text sk_png_set_text
+#define png_set_text_2 sk_png_set_text_2
+#define png_set_text_compression_level sk_png_set_text_compression_level
+#define png_set_text_compression_mem_level sk_png_set_text_compression_mem_level
+#define png_set_text_compression_method sk_png_set_text_compression_method
+#define png_set_text_compression_strategy sk_png_set_text_compression_strategy
+#define png_set_text_compression_window_bits sk_png_set_text_compression_window_bits
+#define png_set_unknown_chunk_location sk_png_set_unknown_chunk_location
+#define png_set_unknown_chunks sk_png_set_unknown_chunks
+#define png_set_user_limits sk_png_set_user_limits
+#define png_set_user_transform_info sk_png_set_user_transform_info
+#define png_set_write_fn sk_png_set_write_fn
+#define png_set_write_status_fn sk_png_set_write_status_fn
+#define png_set_write_user_transform_fn sk_png_set_write_user_transform_fn
+#define png_sig_cmp sk_png_sig_cmp
+#define png_start_read_image sk_png_start_read_image
+#define png_user_version_check sk_png_user_version_check
+#define png_warning sk_png_warning
+#define png_warning_parameter sk_png_warning_parameter
+#define png_warning_parameter_signed sk_png_warning_parameter_signed
+#define png_warning_parameter_unsigned sk_png_warning_parameter_unsigned
+#define png_write_IEND sk_png_write_IEND
+#define png_write_IHDR sk_png_write_IHDR
+#define png_write_PLTE sk_png_write_PLTE
+#define png_write_bKGD sk_png_write_bKGD
+#define png_write_cHRM_fixed sk_png_write_cHRM_fixed
+#define png_write_chunk sk_png_write_chunk
+#define png_write_chunk_data sk_png_write_chunk_data
+#define png_write_chunk_end sk_png_write_chunk_end
+#define png_write_chunk_start sk_png_write_chunk_start
+#define png_write_data sk_png_write_data
+#define png_write_end sk_png_write_end
+#define png_write_find_filter sk_png_write_find_filter
+#define png_write_finish_row sk_png_write_finish_row
+#define png_write_flush sk_png_write_flush
+#define png_write_gAMA_fixed sk_png_write_gAMA_fixed
+#define png_write_hIST sk_png_write_hIST
+#define png_write_iCCP sk_png_write_iCCP
+#define png_write_iTXt sk_png_write_iTXt
+#define png_write_image sk_png_write_image
+#define png_write_info sk_png_write_info
+#define png_write_info_before_PLTE sk_png_write_info_before_PLTE
+#define png_write_oFFs sk_png_write_oFFs
+#define png_write_pCAL sk_png_write_pCAL
+#define png_write_pHYs sk_png_write_pHYs
+#define png_write_png sk_png_write_png
+#define png_write_row sk_png_write_row
+#define png_write_rows sk_png_write_rows
+#define png_write_sBIT sk_png_write_sBIT
+#define png_write_sCAL_s sk_png_write_sCAL_s
+#define png_write_sPLT sk_png_write_sPLT
+#define png_write_sRGB sk_png_write_sRGB
+#define png_write_sig sk_png_write_sig
+#define png_write_start_row sk_png_write_start_row
+#define png_write_tEXt sk_png_write_tEXt
+#define png_write_tIME sk_png_write_tIME
+#define png_write_tRNS sk_png_write_tRNS
+#define png_write_zTXt sk_png_write_zTXt
+#define png_zalloc sk_png_zalloc
+#define png_zfree sk_png_zfree
+#define png_zlib_inflate sk_png_zlib_inflate
+#define png_zstream_error sk_png_zstream_error
+#define png_write_eXIf sk_png_write_eXIf
+#define png_handle_eXIf sk_png_handle_eXIf
+#endif // PNGPREFIX_H
diff --git a/skia/renderer/build.sh b/skia/renderer/build.sh
new file mode 100755
index 0000000..474b491
--- /dev/null
+++ b/skia/renderer/build.sh
@@ -0,0 +1,97 @@
+#!/bin/bash
+set -e
+
+export SKIA_DIR="${SKIA_DIR_NAME:-skia}"
+source ../../dependencies/config_directories.sh
+
+# build main rive
+cd ../..
+./build.sh "$@"
+
+# build skia renderer
+cd skia/renderer
+
+pushd build &>/dev/null
+
+while getopts p: flag; do
+    case "${flag}" in
+    p)
+        shift 2
+        platform=${OPTARG}
+        ;;
+
+    \?) help ;;
+    esac
+done
+
+# make sure argument is lowercase
+OPTION="$(echo "$1" | tr '[A-Z]' '[a-z]')"
+
+help() {
+    echo build.sh - build debug library
+    echo build.sh clean - clean the build
+    echo build.sh release - build release library
+    echo build.sh -p ios release - build release ios library
+    echo build.sh -p ios_sim release - build release ios simulator library
+    echo build.sh -p android release - build release android library
+    exit 1
+}
+
+if [ "$OPTION" = 'help' ]; then
+    help
+else
+    build() {
+        echo "Building Rive Renderer for platform=$platform option=$OPTION"
+        PREMAKE="premake5 --scripts=../../../build gmake2 --with_rive_text --with_rive_audio=system $1"
+        eval "$PREMAKE"
+        if [ "$OPTION" = "clean" ]; then
+            make clean
+            make clean config=release
+        elif [ "$OPTION" = "release" ]; then
+            make config=release -j7
+        else
+            make -j7
+        fi
+    }
+
+    case $platform in
+    ios)
+        echo "Building for iOS"
+        export IOS_SYSROOT=$(xcrun --sdk iphoneos --show-sdk-path)
+        build "--os=ios"
+        if [ "$OPTION" = "clean" ]; then
+            exit
+        fi
+        ;;
+    ios_sim)
+        echo "Building for iOS Simulator"
+        export IOS_SYSROOT=$(xcrun --sdk iphonesimulator --show-sdk-path)
+        build "--os=ios --variant=emulator"
+        if [ "$OPTION" = "clean" ]; then
+            exit
+        fi
+        ;;
+    # Android supports ABIs via a custom platform format:
+    #   e.g. 'android.x86', 'android.x64', etc.
+    android*)
+        echo "Building for ${platform}"
+        # Extract ABI from this opt by splitting on '.' character
+        IFS="." read -ra strarr <<<"$platform"
+        ARCH=${strarr[1]}
+        build "--os=android --arch=${ARCH}"
+        ;;
+    macosx)
+        echo "Building for macos"
+        export MACOS_SYSROOT=$(xcrun --sdk macosx --show-sdk-path)
+        build "--os=macosx --variant=runtime"
+        if [ "$OPTION" = "clean" ]; then
+            exit
+        fi
+        ;;
+    *)
+        build
+        ;;
+    esac
+fi
+
+popd &>/dev/null
diff --git a/skia/renderer/build/macosx/build_skia_renderer.sh b/skia/renderer/build/macosx/build_skia_renderer.sh
new file mode 100755
index 0000000..fe9a3b4
--- /dev/null
+++ b/skia/renderer/build/macosx/build_skia_renderer.sh
@@ -0,0 +1,59 @@
+#!/bin/sh
+set -e
+
+source ../../../../dependencies/macosx/config_directories.sh
+
+CONFIG=debug
+GRAPHICS=gl
+OTHER_OPTIONS=
+
+for var in "$@"; do
+    if [[ $var = "release" ]]; then
+        CONFIG=release
+    fi
+    if [[ $var = "gl" ]]; then
+        GRAPHICS=gl
+    fi
+    if [[ $var = "d3d" ]]; then
+        GRAPHICS=d3d
+    fi
+    if [[ $var = "metal" ]]; then
+        GRAPHICS=metal
+    fi
+    if [[ $var = "text" ]]; then
+        OTHER_OPTIONS+=--with_rive_text
+    fi
+done
+
+if [[ ! -f "$DEPENDENCIES/bin/premake5" ]]; then
+    pushd $DEPENDENCIES_SCRIPTS
+    ./get_premake5.sh
+    popd
+fi
+
+if [[ ! -d "$DEPENDENCIES/sokol" ]]; then
+    pushd $DEPENDENCIES_SCRIPTS
+    ./get_sokol.sh
+    popd
+fi
+
+if [[ ! -f "$DEPENDENCIES/skia/out/$GRAPHICS/$CONFIG/libskia.a" ]]; then
+    pushd $DEPENDENCIES_SCRIPTS
+    ./make_viewer_skia.sh $GRAPHICS $CONFIG
+    popd
+fi
+
+export PREMAKE=$DEPENDENCIES/bin/premake5
+pushd ..
+$PREMAKE --scripts=../../../build --file=./premake5.lua gmake2 $OTHER_OPTIONS
+
+for var in "$@"; do
+    if [[ $var = "clean" ]]; then
+        make clean
+        make config=release clean
+    fi
+done
+
+make config=$CONFIG -j$(($(sysctl -n hw.physicalcpu) + 1))
+
+popd
diff --git a/skia/renderer/build/premake5.lua b/skia/renderer/build/premake5.lua
new file mode 100644
index 0000000..43f147b
--- /dev/null
+++ b/skia/renderer/build/premake5.lua
@@ -0,0 +1,198 @@
+workspace('rive')
+configurations({ 'debug', 'release' })
+
+require('setup_compiler')
+
+SKIA_DIR = os.getenv('SKIA_DIR')
+dependencies = os.getenv('DEPENDENCIES')
+
+if SKIA_DIR == nil and dependencies ~= nil then
+    SKIA_DIR = dependencies .. '/skia'
+else
+    if SKIA_DIR == nil then
+        SKIA_DIR = 'skia'
+    end
+    SKIA_DIR = '../../dependencies/' .. SKIA_DIR
+end
+
+project('rive_skia_renderer')
+do
+    kind('StaticLib')
+    language('C++')
+    cppdialect('C++17')
+    targetdir('%{cfg.system}/bin/%{cfg.buildcfg}')
+    objdir('%{cfg.system}/obj/%{cfg.buildcfg}')
+    includedirs({
+        '../include',
+        '../../../cg_renderer/include',
+        '../../../include',
+    })
+
+    libdirs({ '../../../build/%{cfg.system}/bin/%{cfg.buildcfg}' })
+
+    files({ '../src/**.cpp' })
+
+    flags({ 'FatalCompileWarnings' })
+
+    filter('system:windows')
+    do
+        architecture('x64')
+        defines({ '_USE_MATH_DEFINES' })
+    end
+
+    filter({ 'system:macosx' })
+    do
+        includedirs({ SKIA_DIR })
+        libdirs({ SKIA_DIR .. '/out/static' })
+        links({ 'Cocoa.framework', 'rive', 'skia' })
+    end
+
+    filter({ 'system:macosx', 'options:variant=runtime' })
+    do
+        links({})
+        buildoptions({
+            '-fembed-bitcode -arch arm64 -arch x86_64 -isysroot '
+                .. (os.getenv('MACOS_SYSROOT') or ''),
+        })
+    end
+
+    filter({ 'system:linux or windows' })
+    do
+        includedirs({ SKIA_DIR })
+        libdirs({ SKIA_DIR .. '/out/static' })
+        links({ 'rive', 'skia' })
+    end
+
+    filter({ 'system:ios' })
+    do
+        includedirs({ SKIA_DIR })
+        libdirs({ SKIA_DIR .. '/out/static' })
+    end
+
+    filter({ 'system:ios', 'options:variant=system' })
+    do
+        buildoptions({
+            '-mios-version-min=13.0 -fembed-bitcode -arch arm64 -isysroot '
+                .. (os.getenv('IOS_SYSROOT') or ''),
+        })
+    end
+
+    filter({ 'system:ios', 'options:variant=emulator' })
+    do
+        buildoptions({
+            '--target=arm64-apple-ios13.0.0-simulator -mios-version-min=13.0 -arch x86_64 -arch arm64 -isysroot '
+                .. (os.getenv('IOS_SYSROOT') or ''),
+        })
+        targetdir('%{cfg.system}_sim/bin/%{cfg.buildcfg}')
+        objdir('%{cfg.system}_sim/obj/%{cfg.buildcfg}')
+    end
+
+    -- Is there a way to pass 'arch' as a variable here?
+    filter({ 'system:android' })
+    do
+        includedirs({ SKIA_DIR })
+
+        filter({ 'system:android', 'options:arch=x86' })
+        do
+            targetdir('%{cfg.system}/x86/bin/%{cfg.buildcfg}')
+            objdir('%{cfg.system}/x86/obj/%{cfg.buildcfg}')
+            libdirs({ SKIA_DIR .. '/out/x86' })
+        end
+
+        filter({ 'system:android', 'options:arch=x64' })
+        do
+            targetdir('%{cfg.system}/x64/bin/%{cfg.buildcfg}')
+            objdir('%{cfg.system}/x64/obj/%{cfg.buildcfg}')
+            libdirs({ SKIA_DIR .. '/out/x64' })
+        end
+
+        filter({ 'system:android', 'options:arch=arm' })
+        do
+            targetdir('%{cfg.system}/arm/bin/%{cfg.buildcfg}')
+            objdir('%{cfg.system}/arm/obj/%{cfg.buildcfg}')
+            libdirs({ SKIA_DIR .. '/out/arm' })
+        end
+
+        filter({ 'system:android', 'options:arch=arm64' })
+        do
+            targetdir('%{cfg.system}/arm64/bin/%{cfg.buildcfg}')
+            objdir('%{cfg.system}/arm64/obj/%{cfg.buildcfg}')
+            libdirs({ SKIA_DIR .. '/out/arm64' })
+        end
+    end
+
+    filter({ 'configurations:release', 'system:macosx' })
+    do
+        buildoptions({ '-flto=full' })
+    end
+
+    filter({ 'configurations:release', 'system:android' })
+    do
+        buildoptions({ '-flto=full' })
+    end
+
+    filter({ 'configurations:release', 'system:ios' })
+    do
+        buildoptions({ '-flto=full' })
+    end
+
+    filter('configurations:debug')
+    do
+        defines({ 'DEBUG' })
+        symbols('On')
+    end
+
+    filter('configurations:release')
+    do
+        defines({ 'RELEASE', 'NDEBUG' })
+        optimize('On')
+    end
+
+    filter({ 'options:with_rive_text' })
+    do
+        defines({ 'WITH_RIVE_TEXT' })
+    end
+    filter({ 'options:with_rive_audio=system' })
+    do
+        defines({ 'WITH_RIVE_AUDIO' })
+    end
+    filter({ 'options:with_rive_audio=external' })
+    do
+        defines({
+            'WITH_RIVE_AUDIO',
+            'EXTERNAL_RIVE_AUDIO_ENGINE',
+            'MA_NO_DEVICE_IO',
+        })
+    end
+end
+
+newoption({ trigger = 'with_rive_text', description = 'Enables text experiments' })
+
+newoption({
+    trigger = 'with_rive_audio',
+    value = 'disabled',
+    description = 'The audio mode to use.',
+    allowed = { { 'disabled' }, { 'system' }, { 'external' } },
+})
+
+newoption({
+    trigger = 'variant',
+    value = 'type',
+    description = 'Choose a particular variant to build',
+    allowed = {
+        { 'system', 'Builds the static library for the provided system' },
+        { 'emulator', 'Builds for an emulator/simulator for the provided system' },
+        {
+            'runtime',
+            'Build the static library specifically targeting our runtimes',
+        },
+    },
+    default = 'system',
+})
+
+newoption({
+    trigger = 'arch',
+    value = 'ABI',
+    description = 'The ABI with the right toolchain for this build, generally with Android',
+    allowed = { { 'x86' }, { 'x64' }, { 'arm' }, { 'arm64' } },
+})
diff --git a/skia/renderer/include/cg_skia_factory.hpp b/skia/renderer/include/cg_skia_factory.hpp
new file mode 100644
index 0000000..cb36b91
--- /dev/null
+++ b/skia/renderer/include/cg_skia_factory.hpp
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_CGSkiaFactory_HPP_
+#define _RIVE_CGSkiaFactory_HPP_
+
+#include "skia_factory.hpp"
+
+namespace rive
+{
+struct CGSkiaFactory : public SkiaFactory
+{
+    std::vector<uint8_t> platformDecode(Span<const uint8_t>, SkiaFactory::ImageInfo*) override;
+};
+} // namespace rive
+
+#endif // _RIVE_CGSkiaFactory_HPP_
diff --git a/skia/renderer/include/mac_utils.hpp b/skia/renderer/include/mac_utils.hpp
new file mode 100644
index 0000000..e283ab9
--- /dev/null
+++ b/skia/renderer/include/mac_utils.hpp
@@ -0,0 +1,92 @@
+#ifndef _RIVE_MAC_UTILS_HPP_
+#define _RIVE_MAC_UTILS_HPP_
+
+#include "rive/rive_types.hpp"
+#include "rive/span.hpp"
+#include "utils/auto_cf.hpp"
+#include <string>
+
+#ifdef RIVE_BUILD_FOR_APPLE
+
+template <size_t N, typename T> class AutoSTArray
+{
+    T m_storage[N];
+    T* m_ptr;
+    const size_t m_count;
+
+public:
+    AutoSTArray(size_t n) : m_count(n)
+    {
+        m_ptr = m_storage;
+        if (n > N)
+        {
+            m_ptr = new T[n];
+        }
+    }
+    ~AutoSTArray()
+    {
+        if (m_ptr != m_storage)
+        {
+            delete[] m_ptr;
+        }
+    }
+
+    T* data() const { return m_ptr; }
+
+    T& operator[](size_t index)
+    {
+        assert(index < m_count);
+        return m_ptr[index];
+    }
+};
+
+constexpr inline uint32_t make_tag(uint8_t a, uint8_t b, uint8_t c, uint8_t d)
+{
+    return (a << 24) | (b << 16) | (c << 8) | d;
+}
+
+static inline std::string tag2str(uint32_t tag)
+{
+    std::string str = "abcd";
+    str[0] = (tag >> 24) & 0xFF;
+    str[1] = (tag >> 16) & 0xFF;
+    str[2] = (tag >> 8) & 0xFF;
+    str[3] = (tag >> 0) & 0xFF;
+    return str;
+}
+
+static inline float find_float(CFDictionaryRef dict, const void* key)
+{
+    auto num = (CFNumberRef)CFDictionaryGetValue(dict, key);
+    assert(num);
+    float value = 0;
+    CFNumberGetValue(num, kCFNumberFloat32Type, &value);
+    return value;
+}
+
+static inline uint32_t find_u32(CFDictionaryRef dict, const void* key)
+{
+    auto num = (CFNumberRef)CFDictionaryGetValue(dict, key);
+    assert(num);
+    assert(!CFNumberIsFloatType(num));
+    uint32_t value = 0;
+    CFNumberGetValue(num, kCFNumberSInt32Type, &value);
+    return value;
+}
+
+static inline uint32_t number_as_u32(CFNumberRef num)
+{
+    uint32_t value;
+    CFNumberGetValue(num, kCFNumberSInt32Type, &value);
+    return value;
+}
+
+static inline float number_as_float(CFNumberRef num)
+{
+    float value;
+    CFNumberGetValue(num, kCFNumberFloat32Type, &value);
+    return value;
+}
+
+#endif
+#endif
diff --git a/skia/renderer/include/skia_factory.hpp b/skia/renderer/include/skia_factory.hpp
new file mode 100644
index 0000000..27b5a3d
--- /dev/null
+++ b/skia/renderer/include/skia_factory.hpp
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_SKIA_FACTORY_HPP_
+#define _RIVE_SKIA_FACTORY_HPP_
+
+#include "rive/factory.hpp"
+#include <vector>
+
+namespace rive
+{
+
+class SkiaFactory : public Factory
+{
+public:
+    rcp<RenderBuffer> makeRenderBuffer(RenderBufferType, RenderBufferFlags, size_t) override;
+
+    rcp<RenderShader> makeLinearGradient(float sx,
+                                         float sy,
+                                         float ex,
+                                         float ey,
+                                         const ColorInt colors[], // [count]
+                                         const float stops[],     // [count]
+                                         size_t count) override;
+
+    rcp<RenderShader> makeRadialGradient(float cx,
+                                         float cy,
+                                         float radius,
+                                         const ColorInt colors[], // [count]
+                                         const float stops[],     // [count]
+                                         size_t count) override;
+
+    rcp<RenderPath> makeRenderPath(RawPath&, FillRule) override;
+
+    rcp<RenderPath> makeEmptyRenderPath() override;
+
+    rcp<RenderPaint> makeRenderPaint() override;
+
+    rcp<RenderImage> decodeImage(Span<const uint8_t>) override;
+
+    //
+    // New virtual for access the platform's codecs
+    //
+
+    enum class ColorType
+    {
+        rgba,
+        bgra,
+    };
+    enum class AlphaType
+    {
+        premul,
+        opaque,
+    };
+    struct ImageInfo
+    {
+        size_t rowBytes; // number of bytes between rows
+        uint32_t width;  // logical width in pixels
+        uint32_t height; // logical height in pixels
+        ColorType colorType;
+        AlphaType alphaType;
+    };
+
+    // Clients can override this to provide access to the platform's decoders, rather
+    // than solely relying on the codecs built into Skia. This allows for the Skia impl
+    // to not have to duplicate the code for codecs that the platform may already have.
+    virtual std::vector<uint8_t> platformDecode(Span<const uint8_t>, ImageInfo* info)
+    {
+        return std::vector<uint8_t>(); // empty vector means decode failed
+    }
+};
+
+} // namespace rive
+#endif
diff --git a/skia/renderer/include/skia_renderer.hpp b/skia/renderer/include/skia_renderer.hpp
new file mode 100644
index 0000000..d397e80
--- /dev/null
+++ b/skia/renderer/include/skia_renderer.hpp
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_SKIA_RENDERER_HPP_
+#define _RIVE_SKIA_RENDERER_HPP_
+
+#include "rive/renderer.hpp"
+
+class SkCanvas;
+
+namespace rive
+{
+class SkiaRenderer : public Renderer
+{
+protected:
+    SkCanvas* m_Canvas;
+
+public:
+    SkiaRenderer(SkCanvas* canvas) : m_Canvas(canvas) {}
+    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,
+                       uint32_t vertexCount,
+                       uint32_t indexCount,
+                       BlendMode,
+                       float opacity) override;
+};
+} // namespace rive
+#endif
diff --git a/skia/renderer/include/to_skia.hpp b/skia/renderer/include/to_skia.hpp
new file mode 100644
index 0000000..0cf8920
--- /dev/null
+++ b/skia/renderer/include/to_skia.hpp
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_TO_SKIA_HPP_
+#define _RIVE_TO_SKIA_HPP_
+
+#include "include/core/SkMatrix.h"
+#include "include/core/SkPaint.h"
+#include "include/core/SkBlendMode.h"
+#include "include/core/SkPath.h"
+#include "include/core/SkPathTypes.h"
+#include "include/core/SkTileMode.h"
+
+#include "rive/math/math_types.hpp"
+#include "rive/math/mat2d.hpp"
+#include "rive/math/raw_path.hpp"
+#include "rive/math/vec2d.hpp"
+#include "rive/renderer.hpp"
+#include "rive/shapes/paint/stroke_cap.hpp"
+#include "rive/shapes/paint/stroke_join.hpp"
+#include "rive/shapes/paint/blend_mode.hpp"
+
+namespace rive
+{
+class ToSkia
+{
+public:
+    static SkMatrix convert(const rive::Mat2D& m)
+    {
+        return SkMatrix::MakeAll(m[0], m[2], m[4], m[1], m[3], m[5], 0, 0, 1);
+    }
+
+    static SkPoint convert(rive::Vec2D point) { return SkPoint::Make(point.x, point.y); }
+
+    // clang-format off
+        static SkPathFillType convert(FillRule value) {
+            switch (value) {
+                case FillRule::evenOdd: return SkPathFillType::kEvenOdd;
+                case FillRule::nonZero: return SkPathFillType::kWinding;
+            }
+            assert(false);
+            return SkPathFillType::kWinding;
+        }
+
+        static SkPaint::Cap convert(rive::StrokeCap cap) {
+            switch (cap) {
+                case StrokeCap::butt:   return SkPaint::Cap::kButt_Cap;
+                case StrokeCap::round:  return SkPaint::Cap::kRound_Cap;
+                case StrokeCap::square: return SkPaint::Cap::kSquare_Cap;
+            }
+            assert(false);
+            return SkPaint::Cap::kButt_Cap;
+        }
+
+        static SkPaint::Join convert(StrokeJoin join) {
+            switch (join) {
+                case StrokeJoin::bevel: return SkPaint::Join::kBevel_Join;
+                case StrokeJoin::round: return SkPaint::Join::kRound_Join;
+                case StrokeJoin::miter: return SkPaint::Join::kMiter_Join;
+            }
+            assert(false);
+            return SkPaint::Join::kMiter_Join;
+        }
+
+        static SkBlendMode convert(BlendMode blendMode) {
+            switch (blendMode) {
+                case BlendMode::srcOver:    return SkBlendMode::kSrcOver;
+                case BlendMode::screen:     return SkBlendMode::kScreen;
+                case BlendMode::overlay:    return SkBlendMode::kOverlay;
+                case BlendMode::darken:     return SkBlendMode::kDarken;
+                case BlendMode::lighten:    return SkBlendMode::kLighten;
+                case BlendMode::colorDodge: return SkBlendMode::kColorDodge;
+                case BlendMode::colorBurn:  return SkBlendMode::kColorBurn;
+                case BlendMode::hardLight:  return SkBlendMode::kHardLight;
+                case BlendMode::softLight:  return SkBlendMode::kSoftLight;
+                case BlendMode::difference: return SkBlendMode::kDifference;
+                case BlendMode::exclusion:  return SkBlendMode::kExclusion;
+                case BlendMode::multiply:   return SkBlendMode::kMultiply;
+                case BlendMode::hue:        return SkBlendMode::kHue;
+                case BlendMode::saturation: return SkBlendMode::kSaturation;
+                case BlendMode::color:      return SkBlendMode::kColor;
+                case BlendMode::luminosity: return SkBlendMode::kLuminosity;
+            }
+            assert(false);
+            return SkBlendMode::kSrcOver;
+        }
+
+        static SkPath convert(const RawPath& rp) {
+            const auto pts = rp.points();
+            const auto vbs = rp.verbsU8();
+            return SkPath::Make((const SkPoint*)pts.data(), pts.size(),
+                                vbs.data(), math::lossless_numeric_cast<int>(vbs.size()),
+                                nullptr, 0, SkPathFillType::kWinding);
+        }
+        // clang-format off
+    };
+} // namespace rive
+#endif
diff --git a/skia/renderer/premake5_v2.lua b/skia/renderer/premake5_v2.lua
new file mode 100644
index 0000000..5a41efa
--- /dev/null
+++ b/skia/renderer/premake5_v2.lua
@@ -0,0 +1,82 @@
+dofile('rive_build_config.lua')
+
+SKIA_DIR = os.getenv('SKIA_DIR')
+dependencies = os.getenv('DEPENDENCIES')
+
+if SKIA_DIR == nil and dependencies ~= nil then
+    SKIA_DIR = dependencies .. '/skia'
+else
+    if SKIA_DIR == nil then
+        SKIA_DIR = 'skia'
+    end
+    SKIA_DIR = '../dependencies/' .. SKIA_DIR
+end
+
+project('rive_skia_renderer')
+do
+    kind('StaticLib')
+    includedirs({ 'include', '../../cg_renderer/include', '../../include' })
+
+    libdirs({ '../../build/%{cfg.system}/bin/' .. RIVE_BUILD_CONFIG })
+
+    files({ 'src/**.cpp' })
+
+    flags({ 'FatalCompileWarnings' })
+
+    filter({ 'system:macosx or linux or windows or ios' })
+    do
+        includedirs({ SKIA_DIR })
+        libdirs({ SKIA_DIR .. '/out/static' })
+    end
+
+    filter({ 'system:android' })
+    do
+        includedirs({ SKIA_DIR })
+
+        filter({ 'system:android', 'options:arch=x86' })
+        do
+            libdirs({ SKIA_DIR .. '/out/x86' })
+        end
+
+        filter({ 'system:android', 'options:arch=x64' })
+        do
+            libdirs({ SKIA_DIR .. '/out/x64' })
+        end
+
+        filter({ 'system:android', 'options:arch=arm' })
+        do
+            libdirs({ SKIA_DIR .. '/out/arm' })
+        end
+
+        filter({ 'system:android', 'options:arch=arm64' })
+        do
+            libdirs({ SKIA_DIR .. '/out/arm64' })
+        end
+    end
+
+    filter({ 'options:with_rive_text' })
+    do
+        defines({ 'WITH_RIVE_TEXT' })
+    end
+    filter({ 'options:with_rive_audio=system' })
+    do
+        defines({ 'WITH_RIVE_AUDIO' })
+    end
+    filter({ 'options:with_rive_audio=external' })
+    do
+        defines({
+            'WITH_RIVE_AUDIO',
+            'EXTERNAL_RIVE_AUDIO_ENGINE',
+            'MA_NO_DEVICE_IO',
+        })
+    end
+end
+
+newoption({ trigger = 'with_rive_text', description = 'Enables text experiments' })
+
+newoption({
+    trigger = 'with_rive_audio',
+    value = 'disabled',
+    description = 'The audio mode to use.',
+    allowed = { { 'disabled' }, { 'system' }, { 'external' } },
+})
diff --git a/skia/renderer/src/cg_skia_factory.cpp b/skia/renderer/src/cg_skia_factory.cpp
new file mode 100644
index 0000000..a96acb7
--- /dev/null
+++ b/skia/renderer/src/cg_skia_factory.cpp
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "rive/core/type_conversions.hpp"
+#include <vector>
+
+#ifdef RIVE_BUILD_FOR_APPLE
+
+#include "cg_skia_factory.hpp"
+#include "cg_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
+
+using namespace rive;
+
+std::vector<uint8_t> CGSkiaFactory::platformDecode(Span<const uint8_t> span,
+                                                   SkiaFactory::ImageInfo* info)
+{
+    std::vector<uint8_t> pixels;
+
+    AutoCF image = CGRenderer::DecodeToCGImage(span);
+    if (!image)
+    {
+        return pixels;
+    }
+
+    bool isOpaque = false;
+    switch (CGImageGetAlphaInfo(image.get()))
+    {
+        case kCGImageAlphaNone:
+        case kCGImageAlphaNoneSkipFirst:
+        case kCGImageAlphaNoneSkipLast:
+            isOpaque = true;
+            break;
+        default:
+            break;
+    }
+
+    // Now create a drawing context to produce RGBA pixels
+
+    const size_t bitsPerComponent = 8;
+    CGBitmapInfo cgInfo = kCGBitmapByteOrder32Big; // rgba
+    if (isOpaque)
+    {
+        cgInfo |= kCGImageAlphaNoneSkipLast;
+    }
+    else
+    {
+        cgInfo |= kCGImageAlphaPremultipliedLast; // premul
+    }
+    const size_t width = CGImageGetWidth(image);
+    const size_t height = CGImageGetHeight(image);
+    const size_t rowBytes = width * 4; // 4 bytes per pixel
+    const size_t size = rowBytes * height;
+
+    pixels.resize(size);
+
+    AutoCF cs = CGColorSpaceCreateDeviceRGB();
+    AutoCF cg =
+        CGBitmapContextCreate(pixels.data(), width, height, bitsPerComponent, rowBytes, cs, cgInfo);
+    if (!cg)
+    {
+        pixels.clear();
+        return pixels;
+    }
+
+    CGContextSetBlendMode(cg, kCGBlendModeCopy);
+    CGContextDrawImage(cg, CGRectMake(0, 0, width, height), image);
+
+    info->alphaType = isOpaque ? AlphaType::opaque : AlphaType::premul;
+    info->colorType = ColorType::rgba;
+    info->width = castTo<uint32_t>(width);
+    info->height = castTo<uint32_t>(height);
+    info->rowBytes = rowBytes;
+    return pixels;
+};
+
+#endif // RIVE_BUILD_FOR_APPLE
diff --git a/skia/renderer/src/skia_factory.cpp b/skia/renderer/src/skia_factory.cpp
new file mode 100644
index 0000000..4b8ce82
--- /dev/null
+++ b/skia/renderer/src/skia_factory.cpp
@@ -0,0 +1,315 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "skia_factory.hpp"
+#include "skia_renderer.hpp"
+#include "to_skia.hpp"
+
+#include "include/core/SkCanvas.h"
+#include "include/core/SkData.h"
+#include "include/core/SkImage.h"
+#include "include/core/SkPixmap.h"
+#include "include/core/SkPaint.h"
+#include "include/core/SkPath.h"
+#include "include/core/SkVertices.h"
+#include "include/effects/SkGradientShader.h"
+
+#include "rive/math/vec2d.hpp"
+#include "rive/shapes/paint/color.hpp"
+#include "utils/factory_utils.hpp"
+
+using namespace rive;
+
+// skia's has/had bugs in trilerp, so backing down to nearest mip
+const SkSamplingOptions gSampling(SkFilterMode::kLinear, SkMipmapMode::kNearest);
+
+class SkiaRenderPath : public lite_rtti_override<RenderPath, SkiaRenderPath>
+{
+private:
+    SkPath m_Path;
+
+public:
+    SkiaRenderPath() {}
+    SkiaRenderPath(SkPath&& path) : m_Path(std::move(path)) {}
+
+    const SkPath& path() const { return m_Path; }
+
+    void rewind() override;
+    void addRenderPath(RenderPath* path, const Mat2D& transform) override;
+    void fillRule(FillRule value) override;
+    void moveTo(float x, float y) override;
+    void lineTo(float x, float y) override;
+    void cubicTo(float ox, float oy, float ix, float iy, float x, float y) override;
+    virtual void close() override;
+};
+
+class SkiaRenderPaint : public lite_rtti_override<RenderPaint, SkiaRenderPaint>
+{
+private:
+    SkPaint m_Paint;
+
+public:
+    SkiaRenderPaint();
+
+    const SkPaint& paint() const { return m_Paint; }
+
+    void style(RenderPaintStyle style) override;
+    void color(unsigned int value) override;
+    void thickness(float value) override;
+    void join(StrokeJoin value) override;
+    void cap(StrokeCap value) override;
+    void blendMode(BlendMode value) override;
+    void shader(rcp<RenderShader>) override;
+    void invalidateStroke() override {}
+};
+
+class SkiaRenderImage : public lite_rtti_override<RenderImage, SkiaRenderImage>
+{
+private:
+    sk_sp<SkImage> m_SkImage;
+
+public:
+    SkiaRenderImage(sk_sp<SkImage> image);
+
+    sk_sp<SkImage> skImage() const { return m_SkImage; }
+};
+
+class SkiaRenderShader : public lite_rtti_override<RenderShader, SkiaRenderShader>
+{
+public:
+    SkiaRenderShader(sk_sp<SkShader> sh) : shader(std::move(sh)) {}
+
+    sk_sp<SkShader> shader;
+};
+
+void SkiaRenderPath::fillRule(FillRule value) { m_Path.setFillType(ToSkia::convert(value)); }
+
+void SkiaRenderPath::rewind() { m_Path.rewind(); }
+void SkiaRenderPath::addRenderPath(RenderPath* path, const Mat2D& 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); }
+void SkiaRenderPath::lineTo(float x, float y) { m_Path.lineTo(x, y); }
+void SkiaRenderPath::cubicTo(float ox, float oy, float ix, float iy, float x, float y)
+{
+    m_Path.cubicTo(ox, oy, ix, iy, x, y);
+}
+void SkiaRenderPath::close() { m_Path.close(); }
+
+SkiaRenderPaint::SkiaRenderPaint() { m_Paint.setAntiAlias(true); }
+
+void SkiaRenderPaint::style(RenderPaintStyle style)
+{
+    switch (style)
+    {
+        case RenderPaintStyle::fill:
+            m_Paint.setStyle(SkPaint::Style::kFill_Style);
+            break;
+        case RenderPaintStyle::stroke:
+            m_Paint.setStyle(SkPaint::Style::kStroke_Style);
+            break;
+    }
+}
+void SkiaRenderPaint::color(unsigned int value) { m_Paint.setColor(value); }
+void SkiaRenderPaint::thickness(float value) { m_Paint.setStrokeWidth(value); }
+void SkiaRenderPaint::join(StrokeJoin value) { m_Paint.setStrokeJoin(ToSkia::convert(value)); }
+void SkiaRenderPaint::cap(StrokeCap value) { m_Paint.setStrokeCap(ToSkia::convert(value)); }
+
+void SkiaRenderPaint::blendMode(BlendMode value) { m_Paint.setBlendMode(ToSkia::convert(value)); }
+
+void SkiaRenderPaint::shader(rcp<RenderShader> rsh)
+{
+    SkiaRenderShader* sksh = lite_rtti_cast<SkiaRenderShader*>(rsh.get());
+    m_Paint.setShader(sksh ? sksh->shader : nullptr);
+}
+
+void SkiaRenderer::save() { m_Canvas->save(); }
+void SkiaRenderer::restore() { m_Canvas->restore(); }
+void SkiaRenderer::transform(const Mat2D& transform)
+{
+    m_Canvas->concat(ToSkia::convert(transform));
+}
+void SkiaRenderer::drawPath(RenderPath* path, RenderPaint* 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)
+{
+    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));
+    m_Canvas->drawImage(skiaImage->skImage(), 0.0f, 0.0f, gSampling, &paint);
+}
+
+#define SKIA_BUG_13047
+
+void SkiaRenderer::drawImageMesh(const RenderImage* image,
+                                 rcp<RenderBuffer> vertices,
+                                 rcp<RenderBuffer> uvCoords,
+                                 rcp<RenderBuffer> indices,
+                                 uint32_t vertexCount,
+                                 uint32_t indexCount,
+                                 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));
+    assert(indices->sizeInBytes() == indexCount * sizeof(uint16_t));
+
+    SkMatrix scaleM;
+
+    auto uvs = (const SkPoint*)skUVCoords->vecs();
+
+#ifdef SKIA_BUG_13047
+    // The local matrix is ignored for drawVertices, so we have to manually scale
+    // the UVs to match Skia's convention...
+    std::vector<SkPoint> scaledUVs(vertexCount);
+    for (uint32_t i = 0; i < vertexCount; ++i)
+    {
+        scaledUVs[i] = {uvs[i].fX * image->width(), uvs[i].fY * image->height()};
+    }
+    uvs = scaledUVs.data();
+#else
+    // We do this because our UVs are normalized, but Skia expects them to be
+    // sized to the shader (i.e. 0..width, 0..height).
+    // To accomdate this, we effectively scaling the image down to 0..1 to
+    // match the scale of the UVs.
+    scaleM = SkMatrix::Scale(2.0f / image->width(), 2.0f / image->height());
+#endif
+
+    auto skiaImage = skImage->skImage();
+    auto shader = skiaImage->makeShader(SkTileMode::kClamp, SkTileMode::kClamp, gSampling, &scaleM);
+
+    SkPaint paint;
+    paint.setAlphaf(opacity);
+    paint.setBlendMode(ToSkia::convert(blendMode));
+    paint.setShader(shader);
+
+    const SkColor* no_colors = nullptr;
+    auto vertexMode = SkVertices::kTriangles_VertexMode;
+    auto vt = SkVertices::MakeCopy(vertexMode,
+                                   vertexCount,
+                                   (const SkPoint*)skVertices->vecs(),
+                                   uvs,
+                                   no_colors,
+                                   indexCount,
+                                   skIndices->u16s());
+
+    // The blend mode is ignored if we don't have colors && uvs
+    m_Canvas->drawVertices(vt, SkBlendMode::kModulate, paint);
+}
+
+SkiaRenderImage::SkiaRenderImage(sk_sp<SkImage> image) : m_SkImage(std::move(image))
+{
+    m_Width = m_SkImage->width();
+    m_Height = m_SkImage->height();
+}
+
+// Factory
+
+rcp<RenderBuffer> SkiaFactory::makeRenderBuffer(RenderBufferType type,
+                                                RenderBufferFlags flags,
+                                                size_t sizeInBytes)
+{
+    return make_rcp<DataRenderBuffer>(type, flags, sizeInBytes);
+}
+
+rcp<RenderShader> SkiaFactory::makeLinearGradient(float sx,
+                                                  float sy,
+                                                  float ex,
+                                                  float ey,
+                                                  const ColorInt colors[], // [count]
+                                                  const float stops[],     // [count]
+                                                  size_t count)
+{
+    const SkPoint pts[] = {{sx, sy}, {ex, ey}};
+    auto sh =
+        SkGradientShader::MakeLinear(pts, (const SkColor*)colors, stops, count, SkTileMode::kClamp);
+    return rcp<RenderShader>(new SkiaRenderShader(std::move(sh)));
+}
+
+rcp<RenderShader> SkiaFactory::makeRadialGradient(float cx,
+                                                  float cy,
+                                                  float radius,
+                                                  const ColorInt colors[], // [count]
+                                                  const float stops[],     // [count]
+                                                  size_t count)
+{
+    auto sh = SkGradientShader::MakeRadial({cx, cy},
+                                           radius,
+                                           (const SkColor*)colors,
+                                           stops,
+                                           count,
+                                           SkTileMode::kClamp);
+    return rcp<RenderShader>(new SkiaRenderShader(std::move(sh)));
+}
+
+rcp<RenderPath> SkiaFactory::makeRenderPath(RawPath& rawPath, FillRule fillRule)
+{
+    const bool isVolatile = false; // ???
+    const SkScalar* conicWeights = nullptr;
+    const int conicWeightCount = 0;
+    return make_rcp<SkiaRenderPath>(
+        SkPath::Make(reinterpret_cast<const SkPoint*>(rawPath.points().data()),
+                     rawPath.points().size(),
+                     (uint8_t*)rawPath.verbs().data(),
+                     rawPath.verbs().size(),
+                     conicWeights,
+                     conicWeightCount,
+                     ToSkia::convert(fillRule),
+                     isVolatile));
+}
+
+rcp<RenderPath> SkiaFactory::makeEmptyRenderPath() { return make_rcp<SkiaRenderPath>(); }
+
+rcp<RenderPaint> SkiaFactory::makeRenderPaint() { return make_rcp<SkiaRenderPaint>(); }
+
+rcp<RenderImage> SkiaFactory::decodeImage(Span<const uint8_t> encoded)
+{
+    sk_sp<SkData> data = SkData::MakeWithCopy(encoded.data(), encoded.size());
+    auto image = SkImage::MakeFromEncoded(data);
+
+    if (image)
+    {
+        // Our optimized skia build seems to have broken lazy-image decode.
+        // As a work-around for now, force the image to be decoded.
+        image = image->makeRasterImage();
+    }
+    else
+    {
+        // Skia failed, so let's try the platform
+        ImageInfo info;
+        auto pixels = this->platformDecode(encoded, &info);
+        if (pixels.size() > 0)
+        {
+            auto ct =
+                info.colorType == ColorType::rgba ? kRGBA_8888_SkColorType : kBGRA_8888_SkColorType;
+            auto at =
+                info.alphaType == AlphaType::premul ? kPremul_SkAlphaType : kOpaque_SkAlphaType;
+            auto skinfo = SkImageInfo::Make(info.width, info.height, ct, at);
+            image = SkImage::MakeRasterCopy({skinfo, pixels.data(), info.rowBytes});
+        }
+    }
+
+    return image ? make_rcp<SkiaRenderImage>(std::move(image)) : nullptr;
+}
diff --git a/skia/thumbnail_generator/build.sh b/skia/thumbnail_generator/build.sh
new file mode 100755
index 0000000..34de3fd
--- /dev/null
+++ b/skia/thumbnail_generator/build.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+set -e
+
+BASEDIR="$PWD"
+
+if [ -d "$PWD/../../../rive-cpp" ]; then
+    export RIVE_RUNTIME_DIR="$PWD/../../../rive-cpp"
+else
+    export RIVE_RUNTIME_DIR="$PWD/../../../runtime"
+fi
+
+cd ../renderer
+./build.sh "$@"
+
+cd "$BASEDIR"
+
+cd build
+
+OPTION=$1
+
+if [ "$OPTION" = 'help' ]; then
+    echo build.sh - build debug library
+    echo build.sh clean - clean the build
+    echo build.sh release - build release library
+elif [ "$OPTION" = "clean" ]; then
+    echo Cleaning project ...
+    premake5 clean --scripts="$RIVE_RUNTIME_DIR/build"
+elif [ "$OPTION" = "release" ]; then
+    premake5 gmake --scripts="$RIVE_RUNTIME_DIR/build" --with_rive_text --with_rive_layout && make config=release -j7
+else
+    premake5 gmake --scripts="$RIVE_RUNTIME_DIR/build" --with_rive_text --with_rive_layout && make -j7
+fi
diff --git a/skia/thumbnail_generator/build/premake5.lua b/skia/thumbnail_generator/build/premake5.lua
new file mode 100644
index 0000000..a724d64
--- /dev/null
+++ b/skia/thumbnail_generator/build/premake5.lua
@@ -0,0 +1,95 @@
+workspace('rive')
+configurations({ 'debug', 'release' })
+
+require('setup_compiler')
+
+RIVE_RUNTIME_DIR = os.getenv('RIVE_RUNTIME_DIR') or '../../../'
+SKIA_DIR_NAME = os.getenv('SKIA_DIR_NAME') or 'skia'
+
+BASE_DIR = path.getabsolute(RIVE_RUNTIME_DIR .. '/build')
+location('./')
+dofile(path.join(BASE_DIR, 'premake5.lua'))
+
+BASE_DIR = path.getabsolute(RIVE_RUNTIME_DIR .. '/skia/renderer/build')
+location('./')
+dofile(path.join(BASE_DIR, 'premake5.lua'))
+
+project('rive_thumbnail_generator')
+kind('ConsoleApp')
+language('C++')
+cppdialect('C++17')
+targetdir('%{cfg.system}/bin/%{cfg.buildcfg}')
+objdir('%{cfg.system}/obj/%{cfg.buildcfg}')
+
+includedirs({
+    RIVE_RUNTIME_DIR .. '/include',
+    RIVE_RUNTIME_DIR .. '/skia/renderer/include',
+    RIVE_RUNTIME_DIR .. '/skia/dependencies/' .. SKIA_DIR_NAME,
+    RIVE_RUNTIME_DIR .. '/skia/dependencies/' .. SKIA_DIR_NAME .. '/include/core',
+    RIVE_RUNTIME_DIR .. '/skia/dependencies/' .. SKIA_DIR_NAME .. '/include/effects',
+    RIVE_RUNTIME_DIR .. '/skia/dependencies/' .. SKIA_DIR_NAME .. '/include/gpu',
+    RIVE_RUNTIME_DIR .. '/skia/dependencies/' .. SKIA_DIR_NAME .. '/include/config',
+})
+
+if os.host() == 'macosx' then
+    links({
+        'Cocoa.framework',
+        'rive',
+        'skia',
+        'rive_skia_renderer',
+        'rive_harfbuzz',
+        'rive_sheenbidi',
+    })
+else
+    links({
+        'rive',
+        'rive_skia_renderer',
+        'skia',
+        'GL',
+        'rive_harfbuzz',
+        'rive_sheenbidi',
+    })
+end
+
+libdirs({
+    '../../../build/%{cfg.system}/bin/%{cfg.buildcfg}',
+    '../../dependencies/skia/out/static',
+    '../../renderer/build/%{cfg.system}/bin/%{cfg.buildcfg}',
+})
+
+files({ '../src/**.cpp' })
+
+buildoptions({ '-Wall', '-fno-exceptions', '-fno-rtti' })
+
+filter('configurations:debug')
+defines({ 'DEBUG' })
+symbols('On')
+
+filter('configurations:release')
+defines({ 'RELEASE' })
+defines({ 'NDEBUG' })
+optimize('On')
+
+filter({ 'options:with_rive_layout' })
+do
+    defines({ 'YOGA_EXPORT=' })
+    includedirs({ yoga })
+    links({
+        'rive_yoga',
+    })
+end
+
+-- Clean Function --
+newaction({
+    trigger = 'clean',
+    description = 'clean the build',
+    execute = function()
+        print('clean the build...')
+        os.rmdir('./bin')
+        os.rmdir('./obj')
+        os.remove('Makefile')
+        -- no wildcards in os.remove, so use shell
+        os.execute('rm *.make')
+        print('build cleaned')
+    end,
+})
diff --git a/skia/thumbnail_generator/run.sh b/skia/thumbnail_generator/run.sh
new file mode 100755
index 0000000..263888e
--- /dev/null
+++ b/skia/thumbnail_generator/run.sh
@@ -0,0 +1 @@
+./build/bin/debug/rive_thumbnail_generator
\ No newline at end of file
diff --git a/skia/thumbnail_generator/src/main.cpp b/skia/thumbnail_generator/src/main.cpp
new file mode 100644
index 0000000..2a6af9c
--- /dev/null
+++ b/skia/thumbnail_generator/src/main.cpp
@@ -0,0 +1,124 @@
+#include "SkData.h"
+#include "SkImage.h"
+#include "SkStream.h"
+#include "SkSurface.h"
+#include "rive/file.hpp"
+#include "rive/math/aabb.hpp"
+#include "skia_factory.hpp"
+#include "skia_renderer.hpp"
+#include <cstdio>
+#include <stdio.h>
+#include <string>
+#include <iostream>
+#include <sstream>
+
+std::string getFileName(char* path)
+{
+    std::string str(path);
+
+    const size_t from = str.find_last_of("\\/");
+    const size_t to = str.find_last_of(".");
+    return str.substr(from + 1, to - from - 1);
+}
+
+const int DEFAULT_SIZE = 256;
+
+int getArg(char* arg)
+{
+    std::istringstream ss(arg);
+    int x;
+    if (!(ss >> x))
+    {
+        std::cerr << "Invalid number: " << arg << '\n';
+    }
+    else if (!ss.eof())
+    {
+        std::cerr << "Trailing chars after number: " << arg << '\n';
+    }
+    return x == 0 ? DEFAULT_SIZE : x;
+}
+
+int main(int argc, char* argv[])
+{
+    rive::SkiaFactory factory;
+
+    if (argc < 2)
+    {
+        fprintf(stderr, "must pass source file");
+        return 1;
+    }
+    FILE* fp = fopen(argv[1], "rb");
+
+    const char* outPath;
+    std::string filename;
+    std::string fullName;
+    if (argc > 2)
+    {
+        outPath = argv[2];
+    }
+    else
+    {
+        filename = getFileName(argv[1]);
+        fullName = filename + ".png";
+        outPath = fullName.c_str();
+    }
+
+    int width = DEFAULT_SIZE, height = DEFAULT_SIZE;
+    if (argc == 5)
+    {
+        width = getArg(argv[3]);
+        height = getArg(argv[4]);
+    }
+
+    if (fp == nullptr)
+    {
+        fprintf(stderr, "Failed to open rive file.\n");
+        return 1;
+    }
+    fseek(fp, 0, SEEK_END);
+    auto length = ftell(fp);
+    fseek(fp, 0, SEEK_SET);
+    std::vector<uint8_t> bytes(length);
+    if (fread(bytes.data(), 1, length, fp) != length)
+    {
+        fprintf(stderr, "Failed to read rive file.\n");
+        fclose(fp);
+        return 1;
+    }
+    fclose(fp);
+
+    auto file = rive::File::import(bytes, &factory);
+    if (!file)
+    {
+        fprintf(stderr, "Failed to read rive file.\n");
+        return 1;
+    }
+    auto artboard = file->artboardDefault();
+    artboard->advance(0.0f);
+
+    sk_sp<SkSurface> rasterSurface = SkSurface::MakeRasterN32Premul(width, height);
+    SkCanvas* rasterCanvas = rasterSurface->getCanvas();
+
+    rive::SkiaRenderer renderer(rasterCanvas);
+    renderer.save();
+    renderer.align(rive::Fit::cover,
+                   rive::Alignment::center,
+                   rive::AABB(0, 0, width, height),
+                   artboard->bounds());
+    artboard->draw(&renderer);
+    renderer.restore();
+
+    sk_sp<SkImage> img(rasterSurface->makeImageSnapshot());
+    if (!img)
+    {
+        return 1;
+    }
+    sk_sp<SkData> png(img->encodeToData());
+    if (!png)
+    {
+        return 1;
+    }
+    SkFILEWStream out(outPath);
+    (void)out.write(png->data(), png->size());
+    return 0;
+}
diff --git a/src/animation/animation_reset.cpp b/src/animation/animation_reset.cpp
new file mode 100644
index 0000000..2faf0d5
--- /dev/null
+++ b/src/animation/animation_reset.cpp
@@ -0,0 +1,49 @@
+#include "rive/animation/animation_reset.hpp"
+#include "rive/core/vector_binary_writer.hpp"
+#include "rive/generated/core_registry.hpp"
+
+using namespace rive;
+
+AnimationReset::AnimationReset() : m_binaryWriter(&m_WriteBuffer), m_binaryReader(nullptr, 0) {}
+
+void AnimationReset::writeObjectId(uint32_t objectId) { m_binaryWriter.writeVarUint(objectId); }
+
+void AnimationReset::writeTotalProperties(uint32_t value) { m_binaryWriter.writeVarUint(value); }
+
+void AnimationReset::writePropertyKey(uint32_t value) { m_binaryWriter.writeVarUint(value); }
+
+void AnimationReset::writePropertyValue(float value) { m_binaryWriter.writeFloat(value); }
+
+void AnimationReset::clear() { m_binaryWriter.clear(); }
+
+void AnimationReset::complete()
+{
+    m_binaryReader.complete(&m_WriteBuffer.front(), m_binaryWriter.size());
+}
+
+void AnimationReset::apply(Artboard* artboard)
+{
+    m_binaryReader.reset(&m_WriteBuffer.front());
+    while (!m_binaryReader.isEOF())
+    {
+        auto objectId = m_binaryReader.readVarUint32();
+        auto object = artboard->resolve(objectId);
+        auto totalProperties = m_binaryReader.readVarUint32();
+        uint32_t currentPropertyIndex = 0;
+        while (currentPropertyIndex < totalProperties)
+        {
+            auto propertyKey = m_binaryReader.readVarUint32();
+            auto propertyValue = m_binaryReader.readFloat32();
+            switch (CoreRegistry::propertyFieldId(propertyKey))
+            {
+                case CoreDoubleType::id:
+                    CoreRegistry::setDouble(object, propertyKey, propertyValue);
+                    break;
+                case CoreColorType::id:
+                    CoreRegistry::setColor(object, propertyKey, propertyValue);
+                    break;
+            }
+            currentPropertyIndex++;
+        }
+    }
+}
diff --git a/src/animation/animation_reset_factory.cpp b/src/animation/animation_reset_factory.cpp
new file mode 100644
index 0000000..55ca964
--- /dev/null
+++ b/src/animation/animation_reset_factory.cpp
@@ -0,0 +1,215 @@
+#include "rive/animation/animation_reset_factory.hpp"
+#include "rive/animation/linear_animation.hpp"
+#include "rive/animation/animation_state.hpp"
+#include "rive/animation/keyed_object.hpp"
+#include "rive/animation/keyed_property.hpp"
+#include "rive/generated/core_registry.hpp"
+#include <map>
+#include <set>
+
+using namespace rive;
+
+class KeyedPropertyData
+{
+public:
+    const KeyedProperty* keyedProperty;
+    bool isBaseline;
+    KeyedPropertyData(const KeyedProperty* value, bool baselineValue) :
+        keyedProperty(value), isBaseline(baselineValue)
+    {}
+};
+
+class KeyedObjectData
+{
+public:
+    std::vector<KeyedPropertyData> keyedPropertiesData;
+    std::set<int> keyedPropertiesSet;
+    uint32_t objectId;
+    KeyedObjectData(const uint32_t value) { objectId = value; }
+    void addProperties(const KeyedObject* keyedObject, bool isBaseline)
+    {
+        size_t index = 0;
+        while (index < keyedObject->numKeyedProperties())
+        {
+            auto keyedProperty = keyedObject->getProperty(index);
+            auto pos = keyedPropertiesSet.find(keyedProperty->propertyKey());
+            if (pos == keyedPropertiesSet.end())
+            {
+                switch (CoreRegistry::propertyFieldId(keyedProperty->propertyKey()))
+                {
+                    case CoreDoubleType::id:
+                    case CoreColorType::id:
+                        keyedPropertiesSet.insert(keyedProperty->propertyKey());
+                        keyedPropertiesData.push_back(KeyedPropertyData(keyedProperty, isBaseline));
+                        break;
+                }
+            }
+            index++;
+        }
+    }
+};
+
+class AnimationsData
+{
+
+private:
+    std::vector<std::unique_ptr<KeyedObjectData>> keyedObjectsData;
+    KeyedObjectData* getKeyedObjectData(const KeyedObject* keyedObject)
+    {
+        for (auto& keyedObjectData : keyedObjectsData)
+        {
+            if (keyedObjectData->objectId == keyedObject->objectId())
+            {
+                return keyedObjectData.get();
+            }
+        }
+
+        auto keyedObjectData = rivestd::make_unique<KeyedObjectData>(keyedObject->objectId());
+        auto ref = keyedObjectData.get();
+        keyedObjectsData.push_back(std::move(keyedObjectData));
+        return ref;
+    }
+
+    void findKeyedObjects(const LinearAnimation* animation, bool isFirstAnimation)
+    {
+        size_t index = 0;
+        while (index < animation->numKeyedObjects())
+        {
+            auto keyedObject = animation->getObject(index);
+            auto keyedObjectData = getKeyedObjectData(keyedObject);
+
+            keyedObjectData->addProperties(keyedObject, isFirstAnimation);
+            index++;
+        }
+    }
+
+public:
+    AnimationsData(std::vector<const LinearAnimation*>& animations, bool useFirstAsBaseline)
+    {
+        bool isFirstAnimation = useFirstAsBaseline;
+        for (auto animation : animations)
+        {
+            findKeyedObjects(animation, isFirstAnimation);
+            isFirstAnimation = false;
+        }
+    }
+
+    void writeObjects(AnimationReset* animationReset, ArtboardInstance* artboard)
+    {
+        for (auto& keyedObjectData : keyedObjectsData)
+        {
+            auto object = artboard->resolve(keyedObjectData->objectId)->as<Component>();
+            auto propertiesData = keyedObjectData->keyedPropertiesData;
+            if (propertiesData.size() > 0)
+            {
+                animationReset->writeObjectId(keyedObjectData->objectId);
+                animationReset->writeTotalProperties(propertiesData.size());
+                for (auto keyedPropertyData : propertiesData)
+                {
+                    auto keyedProperty = keyedPropertyData.keyedProperty;
+                    auto propertyKey = keyedProperty->propertyKey();
+                    switch (CoreRegistry::propertyFieldId(propertyKey))
+                    {
+                        case CoreDoubleType::id:
+                            animationReset->writePropertyKey(propertyKey);
+                            if (keyedPropertyData.isBaseline)
+                            {
+                                auto firstKeyframe = keyedProperty->first();
+                                if (firstKeyframe != nullptr)
+                                {
+                                    auto value =
+                                        keyedProperty->first()->as<KeyFrameDouble>()->value();
+                                    animationReset->writePropertyValue(value);
+                                }
+                            }
+                            else
+                            {
+                                animationReset->writePropertyValue(
+                                    CoreRegistry::getDouble(object, propertyKey));
+                            }
+                            break;
+                        case CoreColorType::id:
+
+                            animationReset->writePropertyKey(propertyKey);
+                            if (keyedPropertyData.isBaseline)
+                            {
+                                auto firstKeyframe = keyedProperty->first();
+                                if (firstKeyframe != nullptr)
+                                {
+                                    auto value =
+                                        keyedProperty->first()->as<KeyFrameColor>()->value();
+                                    animationReset->writePropertyValue(value);
+                                }
+                            }
+                            else
+                            {
+                                animationReset->writePropertyValue(
+                                    CoreRegistry::getColor(object, propertyKey));
+                            }
+                            break;
+                    }
+                }
+            }
+        }
+        animationReset->complete();
+    }
+};
+
+std::unique_ptr<AnimationReset> AnimationResetFactory::getInstance()
+{
+    std::unique_lock<std::mutex> lock(m_mutex);
+    if (m_resources.size() > 0)
+    {
+        auto instance = std::move(m_resources.back());
+        m_resources.pop_back();
+        return instance;
+    }
+    auto instance = rivestd::make_unique<AnimationReset>();
+    return instance;
+}
+
+void AnimationResetFactory::fromState(StateInstance* stateInstance,
+                                      std::vector<const LinearAnimation*>& animations)
+{
+    if (stateInstance != nullptr)
+    {
+        auto state = stateInstance->state();
+        if (state->is<AnimationState>() && state->as<AnimationState>()->animation() != nullptr)
+        {
+            animations.push_back(state->as<AnimationState>()->animation());
+        }
+    }
+}
+
+std::unique_ptr<AnimationReset> AnimationResetFactory::fromStates(StateInstance* stateFrom,
+                                                                  StateInstance* currentState,
+                                                                  ArtboardInstance* artboard)
+{
+    std::vector<const LinearAnimation*> animations;
+    fromState(stateFrom, animations);
+    fromState(currentState, animations);
+    return fromAnimations(animations, artboard, false);
+}
+
+std::unique_ptr<AnimationReset> AnimationResetFactory::fromAnimations(
+    std::vector<const LinearAnimation*>& animations,
+    ArtboardInstance* artboard,
+    bool useFirstAsBaseline)
+{
+    auto animationsData = new AnimationsData(animations, useFirstAsBaseline);
+    auto animationReset = AnimationResetFactory::getInstance();
+    animationsData->writeObjects(animationReset.get(), artboard);
+    delete animationsData;
+    return animationReset;
+}
+
+std::vector<std::unique_ptr<AnimationReset>> AnimationResetFactory::m_resources;
+
+std::mutex AnimationResetFactory::m_mutex;
+
+void AnimationResetFactory::release(std::unique_ptr<AnimationReset> value)
+{
+    std::unique_lock<std::mutex> lock(m_mutex);
+    value->clear();
+    m_resources.push_back(std::move(value));
+}
diff --git a/src/animation/animation_state.cpp b/src/animation/animation_state.cpp
new file mode 100644
index 0000000..ac46cf1
--- /dev/null
+++ b/src/animation/animation_state.cpp
@@ -0,0 +1,16 @@
+#include "rive/animation/animation_state.hpp"
+#include "rive/animation/linear_animation.hpp"
+#include "rive/animation/animation_state_instance.hpp"
+#include "rive/core_context.hpp"
+#include "rive/artboard.hpp"
+
+using namespace rive;
+
+std::unique_ptr<StateInstance> AnimationState::makeInstance(ArtboardInstance* instance) const
+{
+    return rivestd::make_unique<AnimationStateInstance>(this, instance);
+}
+
+#ifdef TESTING
+void AnimationState::animation(LinearAnimation* animation) { m_Animation = animation; }
+#endif
diff --git a/src/animation/animation_state_instance.cpp b/src/animation/animation_state_instance.cpp
new file mode 100644
index 0000000..5cae3e8
--- /dev/null
+++ b/src/animation/animation_state_instance.cpp
@@ -0,0 +1,39 @@
+#include "rive/animation/animation_state_instance.hpp"
+#include "rive/animation/animation_state.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+
+using namespace rive;
+
+static LinearAnimation emptyAnimation;
+
+AnimationStateInstance::AnimationStateInstance(const AnimationState* state,
+                                               ArtboardInstance* instance) :
+    StateInstance(state),
+    // We're careful to always instance a valid animation here as the
+    // StateMachine makes assumptions about AnimationState's producing valid
+    // AnimationStateInstances with backing animations. This was discovered when
+    // using Clang address sanitizer. We previously returned a
+    // SystemStateInstance (basically a no-op StateMachine state) which would
+    // cause bad casts in parts of the code where we assumed AnimationStates
+    // would have create AnimationStateInstances.
+    m_AnimationInstance(state->animation() ? state->animation() : &emptyAnimation,
+                        instance,
+                        state->speed()),
+    m_KeepGoing(true)
+{}
+
+// NOTE:: should we return bool here? we are not currently using the output of this, we are instead
+// using m_keepGoing directly.
+void AnimationStateInstance::advance(float seconds, StateMachineInstance* stateMachineInstance)
+{
+    m_KeepGoing = m_AnimationInstance.advance(seconds * state()->as<AnimationState>()->speed(),
+                                              stateMachineInstance);
+}
+
+void AnimationStateInstance::apply(ArtboardInstance* instance, float mix)
+{
+    m_AnimationInstance.apply(mix);
+}
+
+bool AnimationStateInstance::keepGoing() const { return m_KeepGoing; }
+void AnimationStateInstance::clearSpilledTime() { m_AnimationInstance.clearSpilledTime(); }
\ No newline at end of file
diff --git a/src/animation/blend_animation.cpp b/src/animation/blend_animation.cpp
new file mode 100644
index 0000000..28bb9ac
--- /dev/null
+++ b/src/animation/blend_animation.cpp
@@ -0,0 +1,37 @@
+#include "rive/artboard.hpp"
+#include "rive/animation/blend_animation.hpp"
+#include "rive/animation/layer_state.hpp"
+#include "rive/importers/layer_state_importer.hpp"
+#include "rive/importers/artboard_importer.hpp"
+
+using namespace rive;
+
+LinearAnimation BlendAnimation::m_EmptyAnimation;
+
+StatusCode BlendAnimation::import(ImportStack& importStack)
+{
+    auto importer = importStack.latest<LayerStateImporter>(LayerStateBase::typeKey);
+    if (importer == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    else if (!importer->addBlendAnimation(this))
+    {
+        return StatusCode::InvalidObject;
+    }
+
+    auto artboardImporter = importStack.latest<ArtboardImporter>(ArtboardBase::typeKey);
+    if (artboardImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+
+    auto artboard = artboardImporter->artboard();
+    size_t animationCount = artboard->animationCount();
+    if ((size_t)animationId() < animationCount)
+    {
+        m_Animation = artboardImporter->artboard()->animation(animationId());
+    }
+
+    return Super::import(importStack);
+}
diff --git a/src/animation/blend_animation_1d.cpp b/src/animation/blend_animation_1d.cpp
new file mode 100644
index 0000000..90c7f25
--- /dev/null
+++ b/src/animation/blend_animation_1d.cpp
@@ -0,0 +1,7 @@
+#include "rive/animation/blend_animation_1d.hpp"
+
+using namespace rive;
+
+StatusCode BlendAnimation1D::onAddedDirty(CoreContext* context) { return StatusCode::Ok; }
+
+StatusCode BlendAnimation1D::onAddedClean(CoreContext* context) { return StatusCode::Ok; }
\ No newline at end of file
diff --git a/src/animation/blend_animation_direct.cpp b/src/animation/blend_animation_direct.cpp
new file mode 100644
index 0000000..a9c175b
--- /dev/null
+++ b/src/animation/blend_animation_direct.cpp
@@ -0,0 +1,34 @@
+#include "rive/animation/blend_animation_direct.hpp"
+#include "rive/animation/state_machine.hpp"
+#include "rive/animation/state_machine_number.hpp"
+#include "rive/importers/state_machine_importer.hpp"
+
+using namespace rive;
+
+StatusCode BlendAnimationDirect::onAddedDirty(CoreContext* context) { return StatusCode::Ok; }
+
+StatusCode BlendAnimationDirect::onAddedClean(CoreContext* context) { return StatusCode::Ok; }
+
+StatusCode BlendAnimationDirect::import(ImportStack& importStack)
+{
+    auto stateMachineImporter = importStack.latest<StateMachineImporter>(StateMachine::typeKey);
+    if (stateMachineImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+
+    // Make sure the inputId doesn't overflow the input buffer.
+    if (blendSource() == static_cast<int>(DirectBlendSource::inputId))
+    {
+        if ((size_t)inputId() >= stateMachineImporter->stateMachine()->inputCount())
+        {
+            return StatusCode::InvalidObject;
+        }
+        auto input = stateMachineImporter->stateMachine()->input((size_t)inputId());
+        if (input == nullptr || !input->is<StateMachineNumber>())
+        {
+            return StatusCode::InvalidObject;
+        }
+    }
+    return Super::import(importStack);
+}
diff --git a/src/animation/blend_state.cpp b/src/animation/blend_state.cpp
new file mode 100644
index 0000000..c521d7b
--- /dev/null
+++ b/src/animation/blend_state.cpp
@@ -0,0 +1,19 @@
+#include "rive/animation/blend_state.hpp"
+#include "rive/animation/blend_animation.hpp"
+
+using namespace rive;
+
+BlendState::~BlendState()
+{
+    for (auto anim : m_Animations)
+    {
+        delete anim;
+    }
+}
+
+void BlendState::addAnimation(BlendAnimation* animation)
+{
+    // Assert it's not already contained.
+    assert(std::find(m_Animations.begin(), m_Animations.end(), animation) == m_Animations.end());
+    m_Animations.push_back(animation);
+}
\ No newline at end of file
diff --git a/src/animation/blend_state_1d.cpp b/src/animation/blend_state_1d.cpp
new file mode 100644
index 0000000..ec95728
--- /dev/null
+++ b/src/animation/blend_state_1d.cpp
@@ -0,0 +1,36 @@
+#include "rive/animation/blend_state_1d.hpp"
+#include "rive/animation/state_machine.hpp"
+#include "rive/animation/state_machine_number.hpp"
+#include "rive/animation/blend_state_1d_instance.hpp"
+#include "rive/importers/state_machine_importer.hpp"
+
+using namespace rive;
+
+std::unique_ptr<StateInstance> BlendState1D::makeInstance(ArtboardInstance* instance) const
+{
+    return rivestd::make_unique<BlendState1DInstance>(this, instance);
+}
+
+StatusCode BlendState1D::import(ImportStack& importStack)
+{
+    auto stateMachineImporter = importStack.latest<StateMachineImporter>(StateMachine::typeKey);
+    if (stateMachineImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+
+    if (hasValidInputId())
+    {
+        // Make sure the inputId doesn't overflow the input buffer.
+        if ((size_t)inputId() >= stateMachineImporter->stateMachine()->inputCount())
+        {
+            return StatusCode::InvalidObject;
+        }
+        auto input = stateMachineImporter->stateMachine()->input((size_t)inputId());
+        if (input == nullptr || !input->is<StateMachineNumber>())
+        {
+            return StatusCode::InvalidObject;
+        }
+    }
+    return Super::import(importStack);
+}
diff --git a/src/animation/blend_state_1d_instance.cpp b/src/animation/blend_state_1d_instance.cpp
new file mode 100644
index 0000000..f1049ba
--- /dev/null
+++ b/src/animation/blend_state_1d_instance.cpp
@@ -0,0 +1,121 @@
+#include "rive/animation/blend_state_1d_instance.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+#include "rive/animation/layer_state_flags.hpp"
+
+using namespace rive;
+
+BlendState1DInstance::BlendState1DInstance(const BlendState1D* blendState,
+                                           ArtboardInstance* instance) :
+    BlendStateInstance<BlendState1D, BlendAnimation1D>(blendState, instance)
+{
+
+    if ((static_cast<LayerStateFlags>(blendState->flags()) & LayerStateFlags::Reset) ==
+        LayerStateFlags::Reset)
+    {
+        auto animations = std::vector<const LinearAnimation*>();
+        for (auto blendAnimation : blendState->animations())
+        {
+            animations.push_back(blendAnimation->animation());
+        }
+        m_AnimationReset = AnimationResetFactory::fromAnimations(animations, instance, true);
+    }
+}
+
+BlendState1DInstance::~BlendState1DInstance()
+{
+    if (m_AnimationReset != nullptr)
+    {
+        AnimationResetFactory::release(std::move(m_AnimationReset));
+    }
+}
+
+int BlendState1DInstance::animationIndex(float value)
+{
+    int idx = 0;
+    int mid = 0;
+    float closestValue = 0;
+    int start = 0;
+    int end = static_cast<int>(m_AnimationInstances.size()) - 1;
+
+    while (start <= end)
+    {
+        mid = (start + end) >> 1;
+        closestValue = m_AnimationInstances[mid].blendAnimation()->value();
+        if (closestValue < value)
+        {
+            start = mid + 1;
+        }
+        else if (closestValue > value)
+        {
+            end = mid - 1;
+        }
+        else
+        {
+            idx = start = mid;
+            break;
+        }
+
+        idx = start;
+    }
+    return idx;
+}
+
+void BlendState1DInstance::apply(ArtboardInstance* instance, float mix)
+{
+    if (m_AnimationReset != nullptr)
+    {
+        m_AnimationReset->apply(instance);
+    }
+    BlendStateInstance::apply(instance, mix);
+}
+
+void BlendState1DInstance::advance(float seconds, StateMachineInstance* stateMachineInstance)
+{
+    BlendStateInstance<BlendState1D, BlendAnimation1D>::advance(seconds, stateMachineInstance);
+
+    auto blendState = state()->as<BlendState1D>();
+    float value = 0.0f;
+    if (blendState->hasValidInputId())
+    {
+        // TODO: https://github.com/rive-app/rive-cpp/issues/229
+        auto inputInstance = stateMachineInstance->input(blendState->inputId());
+        auto numberInput = static_cast<const SMINumber*>(inputInstance);
+        value = numberInput->value();
+    }
+    int index = animationIndex(value);
+    auto animationsCount = static_cast<int>(m_AnimationInstances.size());
+    m_To = index >= 0 && index < animationsCount ? &m_AnimationInstances[index] : nullptr;
+    m_From =
+        index - 1 >= 0 && index - 1 < animationsCount ? &m_AnimationInstances[index - 1] : nullptr;
+
+    float mix, mixFrom;
+    auto toValue = m_To == nullptr ? 0.0f : m_To->blendAnimation()->value();
+    auto fromValue = m_From == nullptr ? 0.0f : m_From->blendAnimation()->value();
+
+    if (m_To == nullptr || m_From == nullptr || toValue == fromValue)
+    {
+        mix = mixFrom = 1.0f;
+    }
+    else
+    {
+        mix = (value - fromValue) / (toValue - fromValue);
+        mixFrom = 1.0f - mix;
+    }
+
+    for (auto& animation : m_AnimationInstances)
+    {
+        auto animationValue = animation.blendAnimation()->value();
+        if (m_To != nullptr && animationValue == toValue)
+        {
+            animation.mix(mix);
+        }
+        else if (m_From != nullptr && animationValue == fromValue)
+        {
+            animation.mix(mixFrom);
+        }
+        else
+        {
+            animation.mix(0.0f);
+        }
+    }
+}
diff --git a/src/animation/blend_state_direct.cpp b/src/animation/blend_state_direct.cpp
new file mode 100644
index 0000000..c9b4597
--- /dev/null
+++ b/src/animation/blend_state_direct.cpp
@@ -0,0 +1,12 @@
+#include "rive/animation/blend_state_direct.hpp"
+#include "rive/animation/state_machine.hpp"
+#include "rive/animation/state_machine_number.hpp"
+#include "rive/animation/blend_state_direct_instance.hpp"
+#include "rive/importers/state_machine_importer.hpp"
+
+using namespace rive;
+
+std::unique_ptr<StateInstance> BlendStateDirect::makeInstance(ArtboardInstance* instance) const
+{
+    return rivestd::make_unique<BlendStateDirectInstance>(this, instance);
+}
\ No newline at end of file
diff --git a/src/animation/blend_state_direct_instance.cpp b/src/animation/blend_state_direct_instance.cpp
new file mode 100644
index 0000000..5c52763
--- /dev/null
+++ b/src/animation/blend_state_direct_instance.cpp
@@ -0,0 +1,35 @@
+
+#include "rive/animation/blend_animation_direct.hpp"
+#include "rive/animation/blend_state_direct_instance.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+#include <iostream>
+
+using namespace rive;
+
+BlendStateDirectInstance::BlendStateDirectInstance(const BlendStateDirect* blendState,
+                                                   ArtboardInstance* instance) :
+    BlendStateInstance<BlendStateDirect, BlendAnimationDirect>(blendState, instance)
+{}
+
+void BlendStateDirectInstance::advance(float seconds, StateMachineInstance* stateMachineInstance)
+{
+    BlendStateInstance<BlendStateDirect, BlendAnimationDirect>::advance(seconds,
+                                                                        stateMachineInstance);
+
+    for (auto& animation : m_AnimationInstances)
+    {
+        if (animation.blendAnimation()->blendSource() ==
+            static_cast<int>(DirectBlendSource::mixValue))
+        {
+            auto value = animation.blendAnimation()->mixValue();
+            animation.mix(std::min(1.0f, std::max(0.0f, value / 100.0f)));
+        }
+        else
+        {
+            auto inputInstance = stateMachineInstance->input(animation.blendAnimation()->inputId());
+            auto numberInput = static_cast<const SMINumber*>(inputInstance);
+            auto value = numberInput->value();
+            animation.mix(std::min(1.0f, std::max(0.0f, value / 100.0f)));
+        }
+    }
+}
diff --git a/src/animation/blend_state_transition.cpp b/src/animation/blend_state_transition.cpp
new file mode 100644
index 0000000..9380ef5
--- /dev/null
+++ b/src/animation/blend_state_transition.cpp
@@ -0,0 +1,40 @@
+#include "rive/artboard.hpp"
+#include "rive/animation/blend_state_transition.hpp"
+#include "rive/animation/blend_state_instance.hpp"
+#include "rive/animation/blend_state_1d.hpp"
+#include "rive/animation/blend_state_direct.hpp"
+#include "rive/animation/blend_state_1d_instance.hpp"
+#include "rive/animation/blend_state_direct_instance.hpp"
+
+using namespace rive;
+
+const LinearAnimationInstance* BlendStateTransition::exitTimeAnimationInstance(
+    const StateInstance* from) const
+{
+    if (from != nullptr)
+    {
+        switch (from->state()->coreType())
+        {
+            case BlendState1D::typeKey:
+
+                return static_cast<const BlendState1DInstance*>(from)->animationInstance(
+                    m_ExitBlendAnimation);
+
+            case BlendStateDirect::typeKey:
+
+                return static_cast<const BlendStateDirectInstance*>(from)->animationInstance(
+                    m_ExitBlendAnimation);
+        }
+    }
+
+    return nullptr;
+}
+
+const LinearAnimation* BlendStateTransition::exitTimeAnimation(const LayerState* from) const
+{
+    if (m_ExitBlendAnimation != nullptr)
+    {
+        return m_ExitBlendAnimation->animation();
+    }
+    return nullptr;
+}
\ No newline at end of file
diff --git a/src/animation/cubic_ease_interpolator.cpp b/src/animation/cubic_ease_interpolator.cpp
new file mode 100644
index 0000000..343ba18
--- /dev/null
+++ b/src/animation/cubic_ease_interpolator.cpp
@@ -0,0 +1,13 @@
+#include "rive/animation/cubic_ease_interpolator.hpp"
+
+using namespace rive;
+
+float CubicEaseInterpolator::transformValue(float valueFrom, float valueTo, float factor)
+{
+    return valueFrom + (valueTo - valueFrom) * transform(factor);
+}
+
+float CubicEaseInterpolator::transform(float factor) const
+{
+    return CubicInterpolatorSolver::calcBezier(m_solver.getT(factor), y1(), y2());
+}
\ No newline at end of file
diff --git a/src/animation/cubic_interpolator.cpp b/src/animation/cubic_interpolator.cpp
new file mode 100644
index 0000000..e1e0b3c
--- /dev/null
+++ b/src/animation/cubic_interpolator.cpp
@@ -0,0 +1,9 @@
+#include "rive/animation/cubic_interpolator.hpp"
+
+using namespace rive;
+
+StatusCode CubicInterpolator::onAddedDirty(CoreContext* context)
+{
+    m_solver.build(x1(), x2());
+    return StatusCode::Ok;
+}
\ No newline at end of file
diff --git a/src/animation/cubic_interpolator_component.cpp b/src/animation/cubic_interpolator_component.cpp
new file mode 100644
index 0000000..a027502
--- /dev/null
+++ b/src/animation/cubic_interpolator_component.cpp
@@ -0,0 +1,19 @@
+#include "rive/animation/cubic_interpolator_component.hpp"
+
+using namespace rive;
+
+StatusCode CubicInterpolatorComponent::onAddedDirty(CoreContext* context)
+{
+    StatusCode code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+    m_solver.build(x1(), x2());
+    return StatusCode::Ok;
+}
+
+float CubicInterpolatorComponent::transform(float factor) const
+{
+    return CubicInterpolatorSolver::calcBezier(m_solver.getT(factor), y1(), y2());
+}
\ No newline at end of file
diff --git a/src/animation/cubic_interpolator_solver.cpp b/src/animation/cubic_interpolator_solver.cpp
new file mode 100644
index 0000000..eb6379e
--- /dev/null
+++ b/src/animation/cubic_interpolator_solver.cpp
@@ -0,0 +1,92 @@
+#include "rive/animation/cubic_interpolator_solver.hpp"
+#include <cmath>
+
+using namespace rive;
+
+const int NewtonIterations = 4;
+const float NewtonMinSlope = 0.001f;
+const float SubdivisionPrecision = 0.0000001f;
+const int SubdivisionMaxIterations = 10;
+
+// Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
+float CubicInterpolatorSolver::calcBezier(float aT, float aA1, float aA2)
+{
+    return (((1.0f - 3.0f * aA2 + 3.0f * aA1) * aT + (3.0f * aA2 - 6.0f * aA1)) * aT +
+            (3.0f * aA1)) *
+           aT;
+}
+
+// Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2.
+static float getSlope(float aT, float aA1, float aA2)
+{
+    return 3.0f * (1.0f - 3.0f * aA2 + 3.0f * aA1) * aT * aT +
+           2.0f * (3.0f * aA2 - 6.0f * aA1) * aT + (3.0f * aA1);
+}
+
+void CubicInterpolatorSolver::build(float x1, float x2)
+{
+    m_x1 = x1;
+    m_x2 = x2;
+    for (int i = 0; i < SplineTableSize; ++i)
+    {
+        m_values[i] = calcBezier(i * SampleStepSize, x1, x2);
+    }
+}
+
+float CubicInterpolatorSolver::getT(float x) const
+{
+    float intervalStart = 0.0f;
+    int currentSample = 1;
+    int lastSample = SplineTableSize - 1;
+
+    for (; currentSample != lastSample && m_values[currentSample] <= x; ++currentSample)
+    {
+        intervalStart += SampleStepSize;
+    }
+    --currentSample;
+
+    // Interpolate to provide an initial guess for t
+    float dist =
+        (x - m_values[currentSample]) / (m_values[currentSample + 1] - m_values[currentSample]);
+    float guessForT = intervalStart + dist * SampleStepSize;
+
+    float initialSlope = getSlope(guessForT, m_x1, m_x2);
+    if (initialSlope >= NewtonMinSlope)
+    {
+        for (int i = 0; i < NewtonIterations; ++i)
+        {
+            float currentSlope = getSlope(guessForT, m_x1, m_x2);
+            if (currentSlope == 0.0f)
+            {
+                return guessForT;
+            }
+            float currentX = calcBezier(guessForT, m_x1, m_x2) - x;
+            guessForT -= currentX / currentSlope;
+        }
+        return guessForT;
+    }
+    else if (initialSlope == 0.0f)
+    {
+        return guessForT;
+    }
+    else
+    {
+        float aB = intervalStart + SampleStepSize;
+        float currentX, currentT;
+        int i = 0;
+        do
+        {
+            currentT = intervalStart + (aB - intervalStart) / 2.0f;
+            currentX = calcBezier(currentT, m_x1, m_x2) - x;
+            if (currentX > 0.0f)
+            {
+                aB = currentT;
+            }
+            else
+            {
+                intervalStart = currentT;
+            }
+        } while (std::abs(currentX) > SubdivisionPrecision && ++i < SubdivisionMaxIterations);
+        return currentT;
+    }
+}
\ No newline at end of file
diff --git a/src/animation/cubic_value_interpolator.cpp b/src/animation/cubic_value_interpolator.cpp
new file mode 100644
index 0000000..ed4f607
--- /dev/null
+++ b/src/animation/cubic_value_interpolator.cpp
@@ -0,0 +1,44 @@
+#include "rive/animation/cubic_value_interpolator.hpp"
+
+using namespace rive;
+
+CubicValueInterpolator::CubicValueInterpolator() : m_D(0.0f), m_ValueTo(0.0f)
+{
+    computeParameters();
+}
+void CubicValueInterpolator::computeParameters()
+{
+    float y1 = m_D;
+    float y2 = CubicValueInterpolator::y1();
+    float y3 = CubicValueInterpolator::y2();
+    float y4 = m_ValueTo;
+
+    m_A = y4 + 3 * (y2 - y3) - y1;
+    m_B = 3 * (y3 - y2 * 2 + y1);
+    m_C = 3 * (y2 - y1);
+    // m_D = y1;
+}
+
+float CubicValueInterpolator::transformValue(float valueFrom, float valueTo, float factor)
+{
+    if (m_D != valueFrom || m_ValueTo != valueTo)
+    {
+        m_D = valueFrom;
+        m_ValueTo = valueTo;
+        computeParameters();
+    }
+    float t = m_solver.getT(factor);
+    return ((m_A * t + m_B) * t + m_C) * t + m_D;
+}
+
+float CubicValueInterpolator::transform(float factor) const
+{
+    assert(false);
+    return factor;
+}
+
+StatusCode CubicValueInterpolator::onAddedDirty(CoreContext* context)
+{
+    computeParameters();
+    return Super::onAddedDirty(context);
+}
\ No newline at end of file
diff --git a/src/animation/elastic_ease.cpp b/src/animation/elastic_ease.cpp
new file mode 100644
index 0000000..fd96d81
--- /dev/null
+++ b/src/animation/elastic_ease.cpp
@@ -0,0 +1,68 @@
+#include "rive/animation/elastic_ease.hpp"
+#include "rive/math/math_types.hpp"
+#include "math.h"
+
+using namespace rive;
+
+ElasticEase::ElasticEase(float amplitude, float period) :
+    m_amplitude(amplitude),
+    m_period(period),
+    m_s(amplitude < 1.0f ? period / 4.0f : period / (2.0f * math::PI) * asinf(1.0f / amplitude))
+{}
+
+float ElasticEase::computeActualAmplitude(float time) const
+{
+    if (m_amplitude < 1.0f)
+    {
+        /// We use this when the amplitude is less than 1.0 (amplitude is
+        /// described as factor of change in value). We also precompute s which is
+        /// the effective starting period we use to align the decaying sin with
+        /// our keyframe.
+        float t = abs(m_s);
+        float absTime = abs(time);
+        if (absTime < t)
+        {
+            float l = absTime / t;
+            return (m_amplitude * l) + (1.0f - l);
+        }
+    }
+
+    return m_amplitude;
+}
+
+float ElasticEase::easeOut(float factor) const
+{
+    float time = factor;
+    float actualAmplitude = computeActualAmplitude(time);
+
+    return (actualAmplitude * pow(2.0f, 10.0f * -time) *
+            sinf((time - m_s) * (2.0f * math::PI) / m_period)) +
+           1.0f;
+}
+
+float ElasticEase::easeIn(float factor) const
+{
+    float time = factor - 1.0f;
+
+    float actualAmplitude = computeActualAmplitude(time);
+
+    return -(actualAmplitude * pow(2.0f, 10.0f * time) *
+             sinf((-time - m_s) * (2.0f * math::PI) / m_period));
+}
+
+float ElasticEase::easeInOut(float factor) const
+{
+    float time = factor * 2.0f - 1.0f;
+    float actualAmplitude = computeActualAmplitude(time);
+    if (time < 0.0f)
+    {
+        return -0.5f * actualAmplitude * pow(2.0f, 10.0f * time) *
+               sinf((-time - m_s) * (2.0f * math::PI) / m_period);
+    }
+    else
+    {
+        return 0.5f * (actualAmplitude * pow(2.0f, 10.0f * -time) *
+                       sinf((time - m_s) * (2.0f * math::PI) / m_period)) +
+               1.0f;
+    }
+}
diff --git a/src/animation/elastic_interpolator.cpp b/src/animation/elastic_interpolator.cpp
new file mode 100644
index 0000000..39a2b19
--- /dev/null
+++ b/src/animation/elastic_interpolator.cpp
@@ -0,0 +1,30 @@
+#include "rive/animation/elastic_interpolator.hpp"
+
+using namespace rive;
+
+ElasticInterpolator::ElasticInterpolator() : m_elastic(1.0f, 0.5f) {}
+
+StatusCode ElasticInterpolator::onAddedDirty(CoreContext* context)
+{
+    m_elastic = ElasticEase(amplitude(), period() == 0.0f ? 0.5f : period());
+    return StatusCode::Ok;
+}
+
+float ElasticInterpolator::transformValue(float valueFrom, float valueTo, float factor)
+{
+    return valueFrom + (valueTo - valueFrom) * transform(factor);
+}
+
+float ElasticInterpolator::transform(float factor) const
+{
+    switch (easing())
+    {
+        case Easing::easeIn:
+            return m_elastic.easeIn(factor);
+        case Easing::easeOut:
+            return m_elastic.easeOut(factor);
+        case Easing::easeInOut:
+            return m_elastic.easeInOut(factor);
+    }
+    return factor;
+}
\ No newline at end of file
diff --git a/src/animation/interpolating_keyframe.cpp b/src/animation/interpolating_keyframe.cpp
new file mode 100644
index 0000000..7f7a0da
--- /dev/null
+++ b/src/animation/interpolating_keyframe.cpp
@@ -0,0 +1,20 @@
+#include "rive/animation/interpolating_keyframe.hpp"
+#include "rive/animation/keyframe_interpolator.hpp"
+#include "rive/core_context.hpp"
+
+using namespace rive;
+
+StatusCode InterpolatingKeyFrame::onAddedDirty(CoreContext* context)
+{
+    if (interpolatorId() != -1)
+    {
+        auto coreObject = context->resolve(interpolatorId());
+        if (coreObject == nullptr || !coreObject->is<KeyFrameInterpolator>())
+        {
+            return StatusCode::MissingObject;
+        }
+        m_interpolator = coreObject->as<KeyFrameInterpolator>();
+    }
+
+    return StatusCode::Ok;
+}
\ No newline at end of file
diff --git a/src/animation/keyed_object.cpp b/src/animation/keyed_object.cpp
new file mode 100644
index 0000000..0bb8e63
--- /dev/null
+++ b/src/animation/keyed_object.cpp
@@ -0,0 +1,98 @@
+#include "rive/animation/keyed_object.hpp"
+#include "rive/animation/keyed_property.hpp"
+#include "rive/animation/linear_animation.hpp"
+#include "rive/artboard.hpp"
+#include "rive/importers/linear_animation_importer.hpp"
+#include "rive/generated/core_registry.hpp"
+
+using namespace rive;
+
+KeyedObject::KeyedObject() {}
+KeyedObject::~KeyedObject() {}
+
+void KeyedObject::addKeyedProperty(std::unique_ptr<KeyedProperty> property)
+{
+    m_keyedProperties.push_back(std::move(property));
+}
+
+StatusCode KeyedObject::onAddedDirty(CoreContext* context)
+{
+    // Make sure we're keying a valid object.
+    Core* coreObject = context->resolve(objectId());
+    if (coreObject == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+
+    for (auto& property : m_keyedProperties)
+    {
+        // Validate coreObject supports propertyKey
+        if (!CoreRegistry::objectSupportsProperty(coreObject, property->propertyKey()))
+        {
+            return StatusCode::InvalidObject;
+        }
+        StatusCode code;
+        if ((code = property->onAddedDirty(context)) != StatusCode::Ok)
+        {
+            return code;
+        }
+    }
+    return StatusCode::Ok;
+}
+
+StatusCode KeyedObject::onAddedClean(CoreContext* context)
+{
+    for (auto& property : m_keyedProperties)
+    {
+        property->onAddedClean(context);
+    }
+    return StatusCode::Ok;
+}
+
+void KeyedObject::reportKeyedCallbacks(KeyedCallbackReporter* reporter,
+                                       float secondsFrom,
+                                       float secondsTo,
+                                       bool isAtStartFrame) const
+{
+    for (const std::unique_ptr<KeyedProperty>& property : m_keyedProperties)
+    {
+        if (!CoreRegistry::isCallback(property->propertyKey()))
+        {
+            continue;
+        }
+        property->reportKeyedCallbacks(reporter,
+                                       objectId(),
+                                       secondsFrom,
+                                       secondsTo,
+                                       isAtStartFrame);
+    }
+}
+
+void KeyedObject::apply(Artboard* artboard, float time, float mix)
+{
+    Core* object = artboard->resolve(objectId());
+    if (object == nullptr)
+    {
+        return;
+    }
+    for (std::unique_ptr<KeyedProperty>& property : m_keyedProperties)
+    {
+        if (CoreRegistry::isCallback(property->propertyKey()))
+        {
+            continue;
+        }
+        property->apply(object, time, mix);
+    }
+}
+
+StatusCode KeyedObject::import(ImportStack& importStack)
+{
+    auto importer = importStack.latest<LinearAnimationImporter>(LinearAnimationBase::typeKey);
+    if (importer == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    // we transfer ownership of ourself to the importer!
+    importer->addKeyedObject(std::unique_ptr<KeyedObject>(this));
+    return Super::import(importStack);
+}
\ No newline at end of file
diff --git a/src/animation/keyed_property.cpp b/src/animation/keyed_property.cpp
new file mode 100644
index 0000000..7156fb8
--- /dev/null
+++ b/src/animation/keyed_property.cpp
@@ -0,0 +1,174 @@
+#include "rive/animation/keyed_property.hpp"
+#include "rive/animation/keyed_object.hpp"
+#include "rive/animation/keyframe.hpp"
+#include "rive/animation/interpolating_keyframe.hpp"
+#include "rive/animation/keyed_callback_reporter.hpp"
+#include "rive/importers/import_stack.hpp"
+#include "rive/importers/keyed_object_importer.hpp"
+
+using namespace rive;
+
+KeyedProperty::KeyedProperty() {}
+KeyedProperty::~KeyedProperty() {}
+
+void KeyedProperty::addKeyFrame(std::unique_ptr<KeyFrame> keyframe)
+{
+    m_keyFrames.push_back(std::move(keyframe));
+}
+
+int KeyedProperty::closestFrameIndex(float seconds, int exactOffset) const
+{
+    int mid = 0;
+    float closestSeconds = 0;
+    int start = 0;
+    auto numKeyFrames = static_cast<int>(m_keyFrames.size());
+    int end = numKeyFrames - 1;
+
+    // If it's the last keyframe, we skip the binary search
+    if (seconds > m_keyFrames[end]->seconds())
+    {
+        return end + 1;
+    }
+
+    while (start <= end)
+    {
+        mid = (start + end) >> 1;
+        closestSeconds = m_keyFrames[mid]->seconds();
+        if (closestSeconds < seconds)
+        {
+            start = mid + 1;
+        }
+        else if (closestSeconds > seconds)
+        {
+            end = mid - 1;
+        }
+        else
+        {
+            return mid + exactOffset;
+        }
+    }
+    return start;
+}
+
+void KeyedProperty::reportKeyedCallbacks(KeyedCallbackReporter* reporter,
+                                         uint32_t objectId,
+                                         float secondsFrom,
+                                         float secondsTo,
+                                         bool isAtStartFrame) const
+{
+    if (secondsFrom == secondsTo)
+    {
+        return;
+    }
+    bool isForward = secondsFrom <= secondsTo;
+    int fromExactOffset = 0;
+    int toExactOffset = isForward ? 1 : 0;
+    if (isForward)
+    {
+        if (!isAtStartFrame)
+        {
+            fromExactOffset = 1;
+        }
+    }
+    else
+    {
+        if (isAtStartFrame)
+        {
+            fromExactOffset = 1;
+        }
+    }
+    int idx = closestFrameIndex(secondsFrom, fromExactOffset);
+    int idxTo = closestFrameIndex(secondsTo, toExactOffset);
+
+    if (idxTo < idx)
+    {
+        auto swap = idx;
+        idx = idxTo;
+        idxTo = swap;
+    }
+    while (idxTo > idx)
+    {
+        const std::unique_ptr<KeyFrame>& frame = m_keyFrames[idx];
+        reporter->reportKeyedCallback(objectId, propertyKey(), secondsTo - frame->seconds());
+        idx++;
+    }
+}
+
+void KeyedProperty::apply(Core* object, float seconds, float mix)
+{
+    assert(!m_keyFrames.empty());
+
+    int idx = closestFrameIndex(seconds);
+    int pk = propertyKey();
+
+    if (idx == 0)
+    {
+        static_cast<InterpolatingKeyFrame*>(m_keyFrames[0].get())->apply(object, pk, mix);
+    }
+    else
+    {
+        if (idx < static_cast<int>(m_keyFrames.size()))
+        {
+            InterpolatingKeyFrame* fromFrame =
+                static_cast<InterpolatingKeyFrame*>(m_keyFrames[idx - 1].get());
+            InterpolatingKeyFrame* toFrame =
+                static_cast<InterpolatingKeyFrame*>(m_keyFrames[idx].get());
+            if (seconds == toFrame->seconds())
+            {
+                toFrame->apply(object, pk, mix);
+            }
+            else
+            {
+                if (fromFrame->interpolationType() == 0)
+                {
+                    fromFrame->apply(object, pk, mix);
+                }
+                else
+                {
+                    fromFrame->applyInterpolation(object, pk, seconds, toFrame, mix);
+                }
+            }
+        }
+        else
+        {
+            static_cast<InterpolatingKeyFrame*>(m_keyFrames[idx - 1].get())->apply(object, pk, mix);
+        }
+    }
+}
+
+StatusCode KeyedProperty::onAddedDirty(CoreContext* context)
+{
+    StatusCode code;
+    for (auto& keyframe : m_keyFrames)
+    {
+        if ((code = keyframe->onAddedDirty(context)) != StatusCode::Ok)
+        {
+            return code;
+        }
+    }
+    return StatusCode::Ok;
+}
+
+StatusCode KeyedProperty::onAddedClean(CoreContext* context)
+{
+    StatusCode code;
+    for (auto& keyframe : m_keyFrames)
+    {
+        if ((code = keyframe->onAddedClean(context)) != StatusCode::Ok)
+        {
+            return code;
+        }
+    }
+    return StatusCode::Ok;
+}
+
+StatusCode KeyedProperty::import(ImportStack& importStack)
+{
+    auto importer = importStack.latest<KeyedObjectImporter>(KeyedObjectBase::typeKey);
+    if (importer == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    importer->addKeyedProperty(std::unique_ptr<KeyedProperty>(this));
+    return Super::import(importStack);
+}
diff --git a/src/animation/keyframe.cpp b/src/animation/keyframe.cpp
new file mode 100644
index 0000000..9989fd7
--- /dev/null
+++ b/src/animation/keyframe.cpp
@@ -0,0 +1,21 @@
+#include "rive/animation/keyframe.hpp"
+#include "rive/animation/cubic_interpolator.hpp"
+#include "rive/animation/keyed_property.hpp"
+#include "rive/core_context.hpp"
+#include "rive/importers/import_stack.hpp"
+#include "rive/importers/keyed_property_importer.hpp"
+
+using namespace rive;
+
+void KeyFrame::computeSeconds(int fps) { m_seconds = frame() / (float)fps; }
+
+StatusCode KeyFrame::import(ImportStack& importStack)
+{
+    auto importer = importStack.latest<KeyedPropertyImporter>(KeyedProperty::typeKey);
+    if (importer == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    importer->addKeyFrame(std::unique_ptr<KeyFrame>(this));
+    return Super::import(importStack);
+}
\ No newline at end of file
diff --git a/src/animation/keyframe_bool.cpp b/src/animation/keyframe_bool.cpp
new file mode 100644
index 0000000..2d89d8d
--- /dev/null
+++ b/src/animation/keyframe_bool.cpp
@@ -0,0 +1,18 @@
+#include "rive/animation/keyframe_bool.hpp"
+#include "rive/generated/core_registry.hpp"
+
+using namespace rive;
+
+void KeyFrameBool::apply(Core* object, int propertyKey, float mix)
+{
+    CoreRegistry::setBool(object, propertyKey, value());
+}
+
+void KeyFrameBool::applyInterpolation(Core* object,
+                                      int propertyKey,
+                                      float currentTime,
+                                      const KeyFrame* nextFrame,
+                                      float mix)
+{
+    CoreRegistry::setBool(object, propertyKey, value());
+}
\ No newline at end of file
diff --git a/src/animation/keyframe_callback.cpp b/src/animation/keyframe_callback.cpp
new file mode 100644
index 0000000..4989039
--- /dev/null
+++ b/src/animation/keyframe_callback.cpp
@@ -0,0 +1,2 @@
+#include "rive/animation/keyframe_callback.hpp"
+#include "rive/core_context.hpp"
diff --git a/src/animation/keyframe_color.cpp b/src/animation/keyframe_color.cpp
new file mode 100644
index 0000000..c5228de
--- /dev/null
+++ b/src/animation/keyframe_color.cpp
@@ -0,0 +1,41 @@
+#include "rive/animation/keyframe_color.hpp"
+#include "rive/generated/core_registry.hpp"
+#include "rive/shapes/paint/color.hpp"
+
+using namespace rive;
+
+static void applyColor(Core* object, int propertyKey, float mix, int value)
+{
+    if (mix == 1.0f)
+    {
+        CoreRegistry::setColor(object, propertyKey, value);
+    }
+    else
+    {
+        auto mixedColor = colorLerp(CoreRegistry::getColor(object, propertyKey), value, mix);
+        CoreRegistry::setColor(object, propertyKey, mixedColor);
+    }
+}
+
+void KeyFrameColor::apply(Core* object, int propertyKey, float mix)
+{
+    applyColor(object, propertyKey, mix, value());
+}
+
+void KeyFrameColor::applyInterpolation(Core* object,
+                                       int propertyKey,
+                                       float currentTime,
+                                       const KeyFrame* nextFrame,
+                                       float mix)
+{
+    auto kfc = nextFrame->as<KeyFrameColor>();
+    const KeyFrameColor& nextColor = *kfc;
+    float f = (currentTime - seconds()) / (nextColor.seconds() - seconds());
+
+    if (KeyFrameInterpolator* keyframeInterpolator = interpolator())
+    {
+        f = keyframeInterpolator->transform(f);
+    }
+
+    applyColor(object, propertyKey, mix, colorLerp(value(), nextColor.value(), f));
+}
\ No newline at end of file
diff --git a/src/animation/keyframe_double.cpp b/src/animation/keyframe_double.cpp
new file mode 100644
index 0000000..180981b
--- /dev/null
+++ b/src/animation/keyframe_double.cpp
@@ -0,0 +1,52 @@
+#include "rive/animation/keyframe_double.hpp"
+#include "rive/generated/core_registry.hpp"
+
+using namespace rive;
+
+// This whole class is intentionally misnamed to match our editor code. The
+// editor uses doubles (float64) for numeric values but at runtime 32 bit
+// floating point numbers suffice. So even though this is a "double keyframe" to
+// match editor names, the actual values are stored and applied in 32 bits.
+
+static void applyDouble(Core* object, int propertyKey, float mix, float value)
+{
+    if (mix == 1.0f)
+    {
+        CoreRegistry::setDouble(object, propertyKey, value);
+    }
+    else
+    {
+        float mixi = 1.0f - mix;
+        CoreRegistry::setDouble(object,
+                                propertyKey,
+                                CoreRegistry::getDouble(object, propertyKey) * mixi + value * mix);
+    }
+}
+
+void KeyFrameDouble::apply(Core* object, int propertyKey, float mix)
+{
+    applyDouble(object, propertyKey, mix, value());
+}
+
+void KeyFrameDouble::applyInterpolation(Core* object,
+                                        int propertyKey,
+                                        float currentTime,
+                                        const KeyFrame* nextFrame,
+                                        float mix)
+{
+    auto kfd = nextFrame->as<KeyFrameDouble>();
+    const KeyFrameDouble& nextDouble = *kfd;
+    float f = (currentTime - seconds()) / (nextDouble.seconds() - seconds());
+
+    float frameValue;
+    if (KeyFrameInterpolator* keyframeInterpolator = interpolator())
+    {
+        frameValue = keyframeInterpolator->transformValue(value(), nextDouble.value(), f);
+    }
+    else
+    {
+        frameValue = value() + (nextDouble.value() - value()) * f;
+    }
+
+    applyDouble(object, propertyKey, mix, frameValue);
+}
\ No newline at end of file
diff --git a/src/animation/keyframe_id.cpp b/src/animation/keyframe_id.cpp
new file mode 100644
index 0000000..dfa8fcc
--- /dev/null
+++ b/src/animation/keyframe_id.cpp
@@ -0,0 +1,18 @@
+#include "rive/animation/keyframe_id.hpp"
+#include "rive/generated/core_registry.hpp"
+
+using namespace rive;
+
+void KeyFrameId::apply(Core* object, int propertyKey, float mix)
+{
+    CoreRegistry::setUint(object, propertyKey, value());
+}
+
+void KeyFrameId::applyInterpolation(Core* object,
+                                    int propertyKey,
+                                    float currentTime,
+                                    const KeyFrame* nextFrame,
+                                    float mix)
+{
+    CoreRegistry::setUint(object, propertyKey, value());
+}
\ No newline at end of file
diff --git a/src/animation/keyframe_interpolator.cpp b/src/animation/keyframe_interpolator.cpp
new file mode 100644
index 0000000..6c972a8
--- /dev/null
+++ b/src/animation/keyframe_interpolator.cpp
@@ -0,0 +1,17 @@
+#include "rive/animation/keyframe_interpolator.hpp"
+#include "rive/importers/artboard_importer.hpp"
+#include "rive/importers/import_stack.hpp"
+#include "rive/artboard.hpp"
+
+using namespace rive;
+
+StatusCode KeyFrameInterpolator::import(ImportStack& importStack)
+{
+    auto artboardImporter = importStack.latest<ArtboardImporter>(ArtboardBase::typeKey);
+    if (artboardImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    artboardImporter->addComponent(this);
+    return Super::import(importStack);
+}
\ No newline at end of file
diff --git a/src/animation/keyframe_string.cpp b/src/animation/keyframe_string.cpp
new file mode 100644
index 0000000..7968dda
--- /dev/null
+++ b/src/animation/keyframe_string.cpp
@@ -0,0 +1,18 @@
+#include "rive/animation/keyframe_string.hpp"
+#include "rive/generated/core_registry.hpp"
+
+using namespace rive;
+
+void KeyFrameString::apply(Core* object, int propertyKey, float mix)
+{
+    CoreRegistry::setString(object, propertyKey, value());
+}
+
+void KeyFrameString::applyInterpolation(Core* object,
+                                        int propertyKey,
+                                        float currentTime,
+                                        const KeyFrame* nextFrame,
+                                        float mix)
+{
+    CoreRegistry::setString(object, propertyKey, value());
+}
\ No newline at end of file
diff --git a/src/animation/keyframe_uint.cpp b/src/animation/keyframe_uint.cpp
new file mode 100644
index 0000000..398abe3
--- /dev/null
+++ b/src/animation/keyframe_uint.cpp
@@ -0,0 +1,18 @@
+#include "rive/animation/keyframe_uint.hpp"
+#include "rive/generated/core_registry.hpp"
+
+using namespace rive;
+
+void KeyFrameUint::apply(Core* object, int propertyKey, float mix)
+{
+    CoreRegistry::setUint(object, propertyKey, value());
+}
+
+void KeyFrameUint::applyInterpolation(Core* object,
+                                      int propertyKey,
+                                      float currentTime,
+                                      const KeyFrame* nextFrame,
+                                      float mix)
+{
+    CoreRegistry::setUint(object, propertyKey, value());
+}
\ No newline at end of file
diff --git a/src/animation/layer_state.cpp b/src/animation/layer_state.cpp
new file mode 100644
index 0000000..173267a
--- /dev/null
+++ b/src/animation/layer_state.cpp
@@ -0,0 +1,62 @@
+#include "rive/animation/layer_state.hpp"
+#include "rive/animation/transition_bool_condition.hpp"
+#include "rive/importers/import_stack.hpp"
+#include "rive/importers/state_machine_layer_importer.hpp"
+#include "rive/generated/animation/state_machine_layer_base.hpp"
+#include "rive/animation/state_transition.hpp"
+#include "rive/animation/system_state_instance.hpp"
+
+using namespace rive;
+
+LayerState::~LayerState()
+{
+    for (auto transition : m_Transitions)
+    {
+        delete transition;
+    }
+}
+
+StatusCode LayerState::onAddedDirty(CoreContext* context)
+{
+    StatusCode code;
+    for (auto transition : m_Transitions)
+    {
+        if ((code = transition->onAddedDirty(context)) != StatusCode::Ok)
+        {
+            return code;
+        }
+    }
+    return StatusCode::Ok;
+}
+
+StatusCode LayerState::onAddedClean(CoreContext* context)
+{
+    StatusCode code;
+    for (auto transition : m_Transitions)
+    {
+        if ((code = transition->onAddedClean(context)) != StatusCode::Ok)
+        {
+            return code;
+        }
+    }
+    return StatusCode::Ok;
+}
+
+StatusCode LayerState::import(ImportStack& importStack)
+{
+    auto layerImporter =
+        importStack.latest<StateMachineLayerImporter>(StateMachineLayerBase::typeKey);
+    if (layerImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    layerImporter->addState(this);
+    return Super::import(importStack);
+}
+
+void LayerState::addTransition(StateTransition* transition) { m_Transitions.push_back(transition); }
+
+std::unique_ptr<StateInstance> LayerState::makeInstance(ArtboardInstance* instance) const
+{
+    return rivestd::make_unique<SystemStateInstance>(this, instance);
+}
\ No newline at end of file
diff --git a/src/animation/linear_animation.cpp b/src/animation/linear_animation.cpp
new file mode 100644
index 0000000..21d795d
--- /dev/null
+++ b/src/animation/linear_animation.cpp
@@ -0,0 +1,140 @@
+#include "rive/animation/linear_animation.hpp"
+#include "rive/animation/keyed_object.hpp"
+#include "rive/animation/keyed_callback_reporter.hpp"
+#include "rive/artboard.hpp"
+#include "rive/importers/artboard_importer.hpp"
+#include "rive/importers/import_stack.hpp"
+#include <cmath>
+
+using namespace rive;
+
+#ifdef TESTING
+int LinearAnimation::deleteCount = 0;
+#endif
+
+LinearAnimation::LinearAnimation() {}
+
+LinearAnimation::~LinearAnimation()
+{
+#ifdef TESTING
+    deleteCount++;
+#endif
+}
+
+StatusCode LinearAnimation::onAddedDirty(CoreContext* context)
+{
+    StatusCode code;
+    for (const auto& object : m_KeyedObjects)
+    {
+        if ((code = object->onAddedDirty(context)) != StatusCode::Ok)
+        {
+            return code;
+        }
+    }
+    return StatusCode::Ok;
+}
+
+StatusCode LinearAnimation::onAddedClean(CoreContext* context)
+{
+    StatusCode code;
+    for (const auto& object : m_KeyedObjects)
+    {
+        if ((code = object->onAddedClean(context)) != StatusCode::Ok)
+        {
+            return code;
+        }
+    }
+    return StatusCode::Ok;
+}
+
+void LinearAnimation::addKeyedObject(std::unique_ptr<KeyedObject> object)
+{
+    m_KeyedObjects.push_back(std::move(object));
+}
+
+void LinearAnimation::apply(Artboard* artboard, float time, float mix) const
+{
+    if (quantize())
+    {
+        float ffps = (float)fps();
+        time = std::floor(time * ffps) / ffps;
+    }
+    for (const auto& object : m_KeyedObjects)
+    {
+        object->apply(artboard, time, mix);
+    }
+}
+
+StatusCode LinearAnimation::import(ImportStack& importStack)
+{
+    auto artboardImporter = importStack.latest<ArtboardImporter>(ArtboardBase::typeKey);
+    if (artboardImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    artboardImporter->addAnimation(this);
+    return Super::import(importStack);
+}
+
+float LinearAnimation::startSeconds() const
+{
+    return (enableWorkArea() ? workStart() : 0) / (float)fps();
+}
+float LinearAnimation::endSeconds() const
+{
+    return (enableWorkArea() ? workEnd() : duration()) / (float)fps();
+}
+
+float LinearAnimation::startTime() const { return (speed() >= 0) ? startSeconds() : endSeconds(); }
+float LinearAnimation::startTime(float multiplier) const
+{
+    return ((speed() * multiplier) >= 0) ? startSeconds() : endSeconds();
+}
+float LinearAnimation::endTime() const { return (speed() >= 0) ? endSeconds() : startSeconds(); }
+float LinearAnimation::durationSeconds() const { return std::abs(endSeconds() - startSeconds()); }
+
+// Matches Dart modulus: https://api.dart.dev/stable/2.19.0/dart-core/double/operator_modulo.html
+static float positiveMod(float value, float range)
+{
+    assert(range > 0.0f);
+    float v = fmodf(value, range);
+    if (v < 0.0f)
+    {
+        v += range;
+    }
+    return v;
+}
+
+float LinearAnimation::globalToLocalSeconds(float seconds) const
+{
+    switch (loop())
+    {
+        case Loop::oneShot:
+            return seconds + startTime();
+        case Loop::loop:
+            return positiveMod(seconds, (durationSeconds())) + startTime();
+        case Loop::pingPong:
+            float localTime = positiveMod(seconds, (durationSeconds()));
+            int direction = ((int)(seconds / (durationSeconds()))) % 2;
+            return direction == 0 ? localTime + startTime() : endTime() - localTime;
+    }
+    RIVE_UNREACHABLE();
+}
+
+void LinearAnimation::reportKeyedCallbacks(KeyedCallbackReporter* reporter,
+                                           float secondsFrom,
+                                           float secondsTo,
+                                           float speedDirection,
+                                           bool fromPong) const
+{
+    float startingTime = startTime(speedDirection);
+    bool isAtStartFrame = startingTime == secondsFrom;
+
+    if (!isAtStartFrame || !fromPong)
+    {
+        for (const auto& object : m_KeyedObjects)
+        {
+            object->reportKeyedCallbacks(reporter, secondsFrom, secondsTo, isAtStartFrame);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/animation/linear_animation_instance.cpp b/src/animation/linear_animation_instance.cpp
new file mode 100644
index 0000000..08ebd16
--- /dev/null
+++ b/src/animation/linear_animation_instance.cpp
@@ -0,0 +1,273 @@
+#include "rive/animation/linear_animation_instance.hpp"
+#include "rive/animation/linear_animation.hpp"
+#include "rive/animation/loop.hpp"
+#include "rive/animation/keyed_callback_reporter.hpp"
+#include <cmath>
+#include <cassert>
+
+using namespace rive;
+
+LinearAnimationInstance::LinearAnimationInstance(const LinearAnimation* animation,
+                                                 ArtboardInstance* instance,
+                                                 float speedMultiplier) :
+    Scene(instance),
+    m_animation((assert(animation != nullptr), animation)),
+    m_time((speedMultiplier >= 0) ? animation->startTime() : animation->endTime()),
+    m_speedDirection((speedMultiplier >= 0) ? 1 : -1),
+    m_totalTime(0.0f),
+    m_lastTotalTime(0.0f),
+    m_spilledTime(0.0f),
+    m_direction(1)
+{}
+
+LinearAnimationInstance::LinearAnimationInstance(LinearAnimationInstance const& lhs) :
+    Scene(lhs),
+    m_animation(lhs.m_animation),
+    m_time(lhs.m_time),
+    m_speedDirection(lhs.m_speedDirection),
+    m_totalTime(lhs.m_totalTime),
+    m_lastTotalTime(lhs.m_lastTotalTime),
+    m_spilledTime(lhs.m_spilledTime),
+    m_direction(lhs.m_direction),
+    m_didLoop(lhs.m_didLoop),
+    m_loopValue(lhs.m_loopValue)
+{}
+
+LinearAnimationInstance::~LinearAnimationInstance() {}
+
+bool LinearAnimationInstance::advanceAndApply(float seconds)
+{
+    bool more = this->advance(seconds, this);
+    this->apply();
+    m_artboardInstance->advance(seconds);
+    return more;
+}
+
+bool LinearAnimationInstance::advance(float elapsedSeconds, KeyedCallbackReporter* reporter)
+{
+    const LinearAnimation& animation = *m_animation;
+    float deltaSeconds = elapsedSeconds * animation.speed() * m_direction;
+    m_spilledTime = 0.0f;
+    if (deltaSeconds == 0)
+    {
+        m_didLoop = false;
+        return true;
+    }
+
+    m_lastTotalTime = m_totalTime;
+    m_totalTime += std::abs(deltaSeconds);
+
+    // NOTE:
+    // do not track spilled time, if our one shot loop is already completed.
+    // stop gap before we move spilled tracking into state machine logic.
+    bool killSpilledTime = !this->keepGoing(elapsedSeconds);
+
+    float lastTime = m_time;
+    m_time += deltaSeconds;
+    if (reporter != nullptr)
+    {
+        animation.reportKeyedCallbacks(reporter, lastTime, m_time, m_speedDirection, false);
+    }
+
+    int fps = animation.fps();
+    float frames = m_time * fps;
+    int start = animation.enableWorkArea() ? animation.workStart() : 0;
+    int end = animation.enableWorkArea() ? animation.workEnd() : animation.duration();
+    int range = end - start;
+
+    bool didLoop = false;
+
+    // this has some issues when deltaSeconds is 0,
+    // right now we basically assume we default to going forwards in that case
+    //
+    int direction = deltaSeconds < 0 ? -1 : 1;
+    switch (loop())
+    {
+        case Loop::oneShot:
+            if (direction == 1 && frames > end)
+            {
+                // Account for the time dilation or contraction applied in the
+                // animation local time by its speed to calculate spilled time.
+                // Calculate the ratio of the time excess by the total elapsed
+                // time in local time (deltaFrames) and multiply the elapsed time
+                // by it.
+                auto deltaFrames = deltaSeconds * fps;
+                auto spilledFramesRatio = (frames - end) / deltaFrames;
+                m_spilledTime = spilledFramesRatio * elapsedSeconds;
+                frames = (float)end;
+                m_time = frames / fps;
+                didLoop = true;
+            }
+            else if (direction == -1 && frames < start)
+            {
+                auto deltaFrames = std::abs(deltaSeconds * fps);
+                auto spilledFramesRatio = (start - frames) / deltaFrames;
+                m_spilledTime = spilledFramesRatio * elapsedSeconds;
+                frames = (float)start;
+                m_time = frames / fps;
+                didLoop = true;
+            }
+            break;
+        case Loop::loop:
+            if (direction == 1 && frames >= end)
+            {
+                // How spilled time has to be calculated, given that local time can be scaled
+                // to a factor of the regular time:
+                // - for convenience, calculate the local elapsed time in frames (deltaFrames)
+                // - get the remainder of current frame position (frames) by duration (range)
+                // - use that remainder as the ratio of the original time that was not consumed
+                // by the loop (spilledFramesRatio)
+                // - multiply the original elapsedTime by the ratio to set the spilled time
+                auto deltaFrames = deltaSeconds * fps;
+                auto remainder = std::fmod(frames - start, (float)range);
+                auto spilledFramesRatio = remainder / deltaFrames;
+                m_spilledTime = spilledFramesRatio * elapsedSeconds;
+                frames = start + remainder;
+                m_time = frames / fps;
+                didLoop = true;
+                if (reporter != nullptr)
+                {
+                    animation.reportKeyedCallbacks(reporter, 0.0f, m_time, m_speedDirection, false);
+                }
+            }
+            else if (direction == -1 && frames <= start)
+            {
+                auto deltaFrames = deltaSeconds * fps;
+                auto remainder = std::abs(std::fmod(start - frames, (float)range));
+                auto spilledFramesRatio = std::abs(remainder / deltaFrames);
+                m_spilledTime = spilledFramesRatio * elapsedSeconds;
+                frames = end - remainder;
+                m_time = frames / fps;
+                didLoop = true;
+                if (reporter != nullptr)
+                {
+                    animation.reportKeyedCallbacks(reporter,
+                                                   end / (float)fps,
+                                                   m_time,
+                                                   m_speedDirection,
+                                                   false);
+                }
+            }
+            break;
+        case Loop::pingPong:
+            bool fromPong = true;
+            while (true)
+            {
+                if (direction == 1 && frames >= end)
+                {
+                    m_spilledTime = (frames - end) / fps;
+                    frames = end + (end - frames);
+                    lastTime = end / (float)fps;
+                }
+                else if (direction == -1 && frames < start)
+                {
+                    m_spilledTime = (start - frames) / fps;
+                    frames = start + (start - frames);
+                    lastTime = start / (float)fps;
+                }
+                else
+                {
+                    // we're within the range, we can stop fixing. We do this in
+                    // a loop to fix conditions when time has advanced so far
+                    // that we've ping-ponged back and forth a few times in a
+                    // single frame. We want to accomodate for this in cases
+                    // where animations are not advanced on regular intervals.
+                    break;
+                }
+                m_time = frames / fps;
+                m_direction *= -1;
+                direction *= -1;
+                didLoop = true;
+                if (reporter != nullptr)
+                {
+                    animation.reportKeyedCallbacks(reporter,
+                                                   lastTime,
+                                                   m_time,
+                                                   m_speedDirection,
+                                                   fromPong);
+                }
+                fromPong = !fromPong;
+            }
+            break;
+    }
+
+    if (killSpilledTime)
+    {
+        m_spilledTime = 0;
+    }
+
+    m_didLoop = didLoop;
+    return this->keepGoing(elapsedSeconds);
+}
+
+void LinearAnimationInstance::time(float value)
+{
+    if (m_time == value)
+    {
+        return;
+    }
+    m_time = value;
+    // Make sure to keep last and total in relative lockstep so state machines
+    // can track change even when setting time.
+    auto diff = m_totalTime - m_lastTotalTime;
+
+    int start = (m_animation->enableWorkArea() ? m_animation->workStart() : 0) * m_animation->fps();
+    m_totalTime = value - start;
+    m_lastTotalTime = m_totalTime - diff;
+
+    // leaving this RIGHT now. but is this required? it kinda messes up
+    // playing things backwards and seeking. what purpose does it solve?
+    m_direction = 1;
+}
+
+void LinearAnimationInstance::reset(float speedMultiplier = 1.0)
+{
+    m_time = (speedMultiplier >= 0) ? m_animation->startTime() : m_animation->endTime();
+}
+
+uint32_t LinearAnimationInstance::fps() const { return m_animation->fps(); }
+
+uint32_t LinearAnimationInstance::duration() const { return m_animation->duration(); }
+
+float LinearAnimationInstance::speed() const { return m_animation->speed(); }
+
+float LinearAnimationInstance::startTime() const { return m_animation->startTime(); }
+
+std::string LinearAnimationInstance::name() const { return m_animation->name(); }
+
+bool LinearAnimationInstance::isTranslucent() const
+{
+    return m_artboardInstance->isTranslucent(this);
+}
+
+// Returns either the animation's default or overridden loop values
+int LinearAnimationInstance::loopValue() const
+{
+    if (m_loopValue != -1)
+    {
+        return m_loopValue;
+    }
+    return m_animation->loopValue();
+}
+
+// Override the animation's loop value
+void LinearAnimationInstance::loopValue(int value)
+{
+    if (m_loopValue == value)
+    {
+        return;
+    }
+    if (m_loopValue == -1 && m_animation->loopValue() == value)
+    {
+        return;
+    }
+    m_loopValue = value;
+}
+
+float LinearAnimationInstance::durationSeconds() const { return m_animation->durationSeconds(); }
+
+void LinearAnimationInstance::reportEvent(Event* event, float secondsDelay)
+{
+    const std::vector<Event*> events{event};
+    notifyListeners(events);
+}
\ No newline at end of file
diff --git a/src/animation/listener_action.cpp b/src/animation/listener_action.cpp
new file mode 100644
index 0000000..b67fa2d
--- /dev/null
+++ b/src/animation/listener_action.cpp
@@ -0,0 +1,21 @@
+#include "rive/animation/state_machine_listener.hpp"
+#include "rive/importers/import_stack.hpp"
+#include "rive/importers/state_machine_listener_importer.hpp"
+#include "rive/importers/state_machine_importer.hpp"
+#include "rive/animation/listener_input_change.hpp"
+#include "rive/animation/state_machine.hpp"
+
+using namespace rive;
+
+StatusCode ListenerAction::import(ImportStack& importStack)
+{
+    auto stateMachineListenerImporter =
+        importStack.latest<StateMachineListenerImporter>(StateMachineListenerBase::typeKey);
+    if (stateMachineListenerImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+
+    stateMachineListenerImporter->addAction(std::unique_ptr<ListenerAction>(this));
+    return Super::import(importStack);
+}
diff --git a/src/animation/listener_align_target.cpp b/src/animation/listener_align_target.cpp
new file mode 100644
index 0000000..415321e
--- /dev/null
+++ b/src/animation/listener_align_target.cpp
@@ -0,0 +1,38 @@
+#include "rive/animation/listener_align_target.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/node.hpp"
+#include "rive/constraints/constraint.hpp"
+
+using namespace rive;
+
+void ListenerAlignTarget::perform(StateMachineInstance* stateMachineInstance,
+                                  Vec2D position,
+                                  Vec2D previousPosition) const
+{
+    auto coreTarget = stateMachineInstance->artboard()->resolve(targetId());
+    if (coreTarget == nullptr || !coreTarget->is<Node>())
+    {
+        return;
+    }
+    auto target = coreTarget->as<Node>();
+    Mat2D targetParentWorld = getParentWorld(*target);
+    Mat2D inverse;
+    if (!targetParentWorld.invert(&inverse))
+    {
+        return;
+    }
+    if (preserveOffset())
+    {
+
+        auto localPosition = inverse * position;
+        auto prevLocalPosition = inverse * previousPosition;
+        target->x(target->x() + localPosition.x - prevLocalPosition.x);
+        target->y(target->y() + localPosition.y - prevLocalPosition.y);
+    }
+    else
+    {
+        auto localPosition = inverse * position;
+        target->x(localPosition.x);
+        target->y(localPosition.y);
+    }
+}
diff --git a/src/animation/listener_bool_change.cpp b/src/animation/listener_bool_change.cpp
new file mode 100644
index 0000000..e84808c
--- /dev/null
+++ b/src/animation/listener_bool_change.cpp
@@ -0,0 +1,79 @@
+#include "rive/animation/listener_bool_change.hpp"
+#include "rive/animation/nested_bool.hpp"
+#include "rive/animation/nested_input.hpp"
+#include "rive/animation/nested_state_machine.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/animation/state_machine_bool.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+using namespace rive;
+
+bool ListenerBoolChange::validateInputType(const StateMachineInput* input) const
+{
+    // A null input is valid as the StateMachine can attempt to limp along if we
+    // introduce new input types that old conditions are expected to handle in
+    // newer runtimes. The older runtimes will just evaluate them to true.
+    return input == nullptr || input->is<StateMachineBool>();
+}
+
+bool ListenerBoolChange::validateNestedInputType(const NestedInput* input) const
+{
+    // A null nested input is valid as the StateMachine can attempt to limp along if we
+    // introduce new input types that old conditions are expected to handle in
+    // newer runtimes. The older runtimes will just evaluate them to true.
+    return input == nullptr || input->is<NestedBool>();
+}
+
+void ListenerBoolChange::perform(StateMachineInstance* stateMachineInstance,
+                                 Vec2D position,
+                                 Vec2D previousPosition) const
+{
+    if (nestedInputId() != Core::emptyId)
+    {
+        auto nestedInputInstance = stateMachineInstance->artboard()->resolve(nestedInputId());
+        if (nestedInputInstance == nullptr)
+        {
+            return;
+        }
+        auto nestedBoolInput = static_cast<NestedBool*>(nestedInputInstance);
+        if (nestedBoolInput != nullptr)
+        {
+            switch (value())
+            {
+                case 0:
+                    nestedBoolInput->nestedValue(false);
+                    break;
+                case 1:
+                    nestedBoolInput->nestedValue(true);
+                    break;
+                default:
+                    nestedBoolInput->nestedValue(!nestedBoolInput->nestedValue());
+                    break;
+            }
+        }
+    }
+    else
+    {
+        auto inputInstance = stateMachineInstance->input(inputId());
+        if (inputInstance == nullptr)
+        {
+            return;
+        }
+        // If it's not null, it must be our correct type (why we validate at load time).
+        auto boolInput = static_cast<SMIBool*>(inputInstance);
+        if (boolInput != nullptr)
+        {
+            switch (value())
+            {
+                case 0:
+                    boolInput->value(false);
+                    break;
+                case 1:
+                    boolInput->value(true);
+                    break;
+                default:
+                    boolInput->value(!boolInput->value());
+                    break;
+            }
+        }
+    }
+}
diff --git a/src/animation/listener_fire_event.cpp b/src/animation/listener_fire_event.cpp
new file mode 100644
index 0000000..bd6d074
--- /dev/null
+++ b/src/animation/listener_fire_event.cpp
@@ -0,0 +1,17 @@
+#include "rive/animation/listener_fire_event.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/event.hpp"
+
+using namespace rive;
+
+void ListenerFireEvent::perform(StateMachineInstance* stateMachineInstance,
+                                Vec2D position,
+                                Vec2D previousPosition) const
+{
+    auto coreEvent = stateMachineInstance->artboard()->resolve(eventId());
+    if (coreEvent == nullptr || !coreEvent->is<Event>())
+    {
+        return;
+    }
+    stateMachineInstance->reportEvent(coreEvent->as<Event>());
+}
\ No newline at end of file
diff --git a/src/animation/listener_input_change.cpp b/src/animation/listener_input_change.cpp
new file mode 100644
index 0000000..c721674
--- /dev/null
+++ b/src/animation/listener_input_change.cpp
@@ -0,0 +1,36 @@
+#include "rive/animation/listener_input_change.hpp"
+#include "rive/animation/nested_input.hpp"
+#include "rive/animation/state_machine.hpp"
+#include "rive/animation/state_machine_listener.hpp"
+#include "rive/artboard.hpp"
+#include "rive/importers/artboard_importer.hpp"
+#include "rive/importers/import_stack.hpp"
+#include "rive/importers/state_machine_listener_importer.hpp"
+#include "rive/importers/state_machine_importer.hpp"
+
+using namespace rive;
+
+StatusCode ListenerInputChange::import(ImportStack& importStack)
+{
+    auto stateMachineImporter = importStack.latest<StateMachineImporter>(StateMachine::typeKey);
+    if (stateMachineImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+
+    auto artboardImporter = importStack.latest<ArtboardImporter>(ArtboardBase::typeKey);
+    if (artboardImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    auto input = stateMachineImporter->stateMachine()->input((size_t)inputId());
+    auto nested = artboardImporter->artboard()->resolve(nestedInputId());
+    auto nestedInput = nested != nullptr ? nested->as<NestedInput>() : nullptr;
+
+    // The listener should validate either an input or a nested input
+    if (!validateInputType(input) && !validateNestedInputType(nestedInput))
+    {
+        return StatusCode::InvalidObject;
+    }
+    return Super::import(importStack);
+}
diff --git a/src/animation/listener_number_change.cpp b/src/animation/listener_number_change.cpp
new file mode 100644
index 0000000..61cac0c
--- /dev/null
+++ b/src/animation/listener_number_change.cpp
@@ -0,0 +1,58 @@
+#include "rive/animation/listener_number_change.hpp"
+#include "rive/animation/nested_input.hpp"
+#include "rive/animation/nested_number.hpp"
+#include "rive/animation/nested_state_machine.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/animation/state_machine_number.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+
+using namespace rive;
+
+bool ListenerNumberChange::validateInputType(const StateMachineInput* input) const
+{
+    // A null input is valid as the StateMachine can attempt to limp along if we
+    // introduce new input types that old conditions are expected to handle in
+    // newer runtimes. The older runtimes will just evaluate them to true.
+    return input == nullptr || input->is<StateMachineNumber>();
+}
+
+bool ListenerNumberChange::validateNestedInputType(const NestedInput* input) const
+{
+    // A null nested input is valid as the StateMachine can attempt to limp along if we
+    // introduce new input types that old conditions are expected to handle in
+    // newer runtimes. The older runtimes will just evaluate them to true.
+    return input == nullptr || input->is<NestedNumber>();
+}
+
+void ListenerNumberChange::perform(StateMachineInstance* stateMachineInstance,
+                                   Vec2D position,
+                                   Vec2D previousPosition) const
+{
+    if (nestedInputId() != Core::emptyId)
+    {
+        auto nestedInputInstance = stateMachineInstance->artboard()->resolve(nestedInputId());
+        if (nestedInputInstance == nullptr)
+        {
+            return;
+        }
+        auto nestedNumberInput = static_cast<NestedNumber*>(nestedInputInstance);
+        if (nestedNumberInput != nullptr)
+        {
+            nestedNumberInput->nestedValue(value());
+        }
+    }
+    else
+    {
+        auto inputInstance = stateMachineInstance->input(inputId());
+        if (inputInstance == nullptr)
+        {
+            return;
+        }
+        // If it's not null, it must be our correct type (why we validate at load time).
+        auto numberInput = static_cast<SMINumber*>(inputInstance);
+        if (numberInput != nullptr)
+        {
+            numberInput->value(value());
+        }
+    }
+}
diff --git a/src/animation/listener_trigger_change.cpp b/src/animation/listener_trigger_change.cpp
new file mode 100644
index 0000000..2832ff8
--- /dev/null
+++ b/src/animation/listener_trigger_change.cpp
@@ -0,0 +1,59 @@
+#include "rive/animation/listener_trigger_change.hpp"
+#include "rive/animation/nested_input.hpp"
+#include "rive/animation/nested_trigger.hpp"
+#include "rive/animation/nested_state_machine.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/animation/state_machine_trigger.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+#include "rive/core/field_types/core_callback_type.hpp"
+
+using namespace rive;
+
+bool ListenerTriggerChange::validateInputType(const StateMachineInput* input) const
+{
+    // A null input is valid as the StateMachine can attempt to limp along if we
+    // introduce new input types that old conditions are expected to handle in
+    // newer runtimes. The older runtimes will just evaluate them to true.
+    return input == nullptr || input->is<StateMachineTrigger>();
+}
+
+bool ListenerTriggerChange::validateNestedInputType(const NestedInput* input) const
+{
+    // A null nested input is valid as the StateMachine can attempt to limp along if we
+    // introduce new input types that old conditions are expected to handle in
+    // newer runtimes. The older runtimes will just evaluate them to true.
+    return input == nullptr || input->is<NestedTrigger>();
+}
+
+void ListenerTriggerChange::perform(StateMachineInstance* stateMachineInstance,
+                                    Vec2D position,
+                                    Vec2D previousPosition) const
+{
+    if (nestedInputId() != Core::emptyId)
+    {
+        auto nestedInputInstance = stateMachineInstance->artboard()->resolve(nestedInputId());
+        if (nestedInputInstance == nullptr)
+        {
+            return;
+        }
+        auto nestedTriggerInput = static_cast<NestedTrigger*>(nestedInputInstance);
+        if (nestedTriggerInput != nullptr)
+        {
+            nestedTriggerInput->fire(CallbackData(stateMachineInstance, 0));
+        }
+    }
+    else
+    {
+        auto inputInstance = stateMachineInstance->input(inputId());
+        if (inputInstance == nullptr)
+        {
+            return;
+        }
+        // If it's not null, it must be our correct type (why we validate at load time).
+        auto triggerInput = static_cast<SMITrigger*>(inputInstance);
+        if (triggerInput != nullptr)
+        {
+            triggerInput->fire();
+        }
+    }
+}
diff --git a/src/animation/listener_viewmodel_change.cpp b/src/animation/listener_viewmodel_change.cpp
new file mode 100644
index 0000000..5047ee5
--- /dev/null
+++ b/src/animation/listener_viewmodel_change.cpp
@@ -0,0 +1,37 @@
+#include "rive/animation/listener_viewmodel_change.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/data_bind/bindable_property.hpp"
+#include "rive/importers/bindable_property_importer.hpp"
+
+using namespace rive;
+
+StatusCode ListenerViewModelChange::import(ImportStack& importStack)
+{
+
+    auto bindablePropertyImporter =
+        importStack.latest<BindablePropertyImporter>(BindablePropertyBase::typeKey);
+    if (bindablePropertyImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    m_bindableProperty = bindablePropertyImporter->bindableProperty();
+
+    return Super::import(importStack);
+}
+
+// Note: perform works the same way whether the value comes from a direct value assignment or from
+// another view model. In the case of coming from another view model, the state machine instance
+// method "updataDataBinds" will handle updating the value of the bound object. That's the benefit
+// of binding the same bindable property with two data binding objects.
+void ListenerViewModelChange::perform(StateMachineInstance* stateMachineInstance,
+                                      Vec2D position,
+                                      Vec2D previousPosition) const
+{
+    // Get the bindable property instance from the state machine instance context
+    auto bindableInstance = stateMachineInstance->bindablePropertyInstance(m_bindableProperty);
+    // Get the data bound object (that goes from target to source) from this bindable instance
+    auto dataBind = stateMachineInstance->bindableDataBind(bindableInstance);
+    // Apply the change that will assign the value of the bindable property to the view model
+    // property instance
+    dataBind->updateSourceBinding();
+}
\ No newline at end of file
diff --git a/src/animation/nested_animation.cpp b/src/animation/nested_animation.cpp
new file mode 100644
index 0000000..7916e88
--- /dev/null
+++ b/src/animation/nested_animation.cpp
@@ -0,0 +1,20 @@
+#include "rive/nested_animation.hpp"
+#include "rive/container_component.hpp"
+#include "rive/nested_artboard.hpp"
+
+using namespace rive;
+
+StatusCode NestedAnimation::onAddedDirty(CoreContext* context)
+{
+    StatusCode code = Super::onAddedDirty(context);
+    if (code == StatusCode::Ok)
+    {
+        if (!parent()->is<NestedArtboard>())
+        {
+            return StatusCode::InvalidObject;
+        }
+        auto nestedArtboard = parent()->as<NestedArtboard>();
+        nestedArtboard->addNestedAnimation(this);
+    }
+    return code;
+}
\ No newline at end of file
diff --git a/src/animation/nested_bool.cpp b/src/animation/nested_bool.cpp
new file mode 100644
index 0000000..e575022
--- /dev/null
+++ b/src/animation/nested_bool.cpp
@@ -0,0 +1,49 @@
+#include "rive/animation/nested_bool.hpp"
+#include "rive/animation/nested_state_machine.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+#include "rive/container_component.hpp"
+
+using namespace rive;
+class StateMachineInstance;
+
+// Use the NestedBoolBase m_NestedValue on initialization but then it won't
+// be used anymore and interface directly with the nested input value.
+void NestedBool::applyValue()
+{
+    auto inputInstance = input();
+    if (inputInstance != nullptr)
+    {
+        auto boolInput = static_cast<SMIBool*>(inputInstance);
+        if (boolInput != nullptr)
+        {
+            boolInput->value(NestedBoolBase::nestedValue());
+        }
+    }
+}
+
+void NestedBool::nestedValue(bool value)
+{
+    auto inputInstance = input();
+    if (inputInstance != nullptr)
+    {
+        auto boolInput = static_cast<SMIBool*>(inputInstance);
+        if (boolInput != nullptr && boolInput->value() != value)
+        {
+            boolInput->value(value);
+        }
+    }
+}
+
+bool NestedBool::nestedValue() const
+{
+    auto inputInstance = input();
+    if (inputInstance != nullptr)
+    {
+        auto boolInput = static_cast<SMIBool*>(inputInstance);
+        if (boolInput != nullptr)
+        {
+            return boolInput->value();
+        }
+    }
+    return false;
+}
\ No newline at end of file
diff --git a/src/animation/nested_linear_animation.cpp b/src/animation/nested_linear_animation.cpp
new file mode 100644
index 0000000..2d53dee
--- /dev/null
+++ b/src/animation/nested_linear_animation.cpp
@@ -0,0 +1,13 @@
+#include "rive/animation/nested_linear_animation.hpp"
+#include "rive/animation/linear_animation_instance.hpp"
+
+using namespace rive;
+
+NestedLinearAnimation::NestedLinearAnimation() {}
+NestedLinearAnimation::~NestedLinearAnimation() {}
+
+void NestedLinearAnimation::initializeAnimation(ArtboardInstance* artboard)
+{
+    m_AnimationInstance =
+        rivestd::make_unique<LinearAnimationInstance>(artboard->animation(animationId()), artboard);
+}
\ No newline at end of file
diff --git a/src/animation/nested_number.cpp b/src/animation/nested_number.cpp
new file mode 100644
index 0000000..a3d8829
--- /dev/null
+++ b/src/animation/nested_number.cpp
@@ -0,0 +1,49 @@
+#include "rive/animation/nested_number.hpp"
+#include "rive/animation/nested_state_machine.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+#include "rive/container_component.hpp"
+
+using namespace rive;
+class StateMachineInstance;
+
+// Use the NestedNumberBase m_NestedValue on initialization but then it won't
+// be used anymore and interface directly with the nested input value.
+void NestedNumber::applyValue()
+{
+    auto inputInstance = input();
+    if (inputInstance != nullptr)
+    {
+        auto numInput = static_cast<SMINumber*>(inputInstance);
+        if (numInput != nullptr)
+        {
+            numInput->value(NestedNumberBase::nestedValue());
+        }
+    }
+}
+
+void NestedNumber::nestedValue(float value)
+{
+    auto inputInstance = input();
+    if (inputInstance != nullptr)
+    {
+        auto numInput = static_cast<SMINumber*>(inputInstance);
+        if (numInput != nullptr && numInput->value() != value)
+        {
+            numInput->value(value);
+        }
+    }
+}
+
+float NestedNumber::nestedValue() const
+{
+    auto inputInstance = input();
+    if (inputInstance != nullptr)
+    {
+        auto numInput = static_cast<SMINumber*>(inputInstance);
+        if (numInput != nullptr)
+        {
+            return numInput->value();
+        }
+    }
+    return 0.0;
+}
\ No newline at end of file
diff --git a/src/animation/nested_remap_animation.cpp b/src/animation/nested_remap_animation.cpp
new file mode 100644
index 0000000..4edc815
--- /dev/null
+++ b/src/animation/nested_remap_animation.cpp
@@ -0,0 +1,30 @@
+#include "rive/animation/nested_remap_animation.hpp"
+#include "rive/animation/linear_animation_instance.hpp"
+
+using namespace rive;
+
+void NestedRemapAnimation::timeChanged()
+{
+    if (m_AnimationInstance != nullptr)
+    {
+        m_AnimationInstance->time(m_AnimationInstance->animation()->globalToLocalSeconds(
+            m_AnimationInstance->durationSeconds() * time()));
+    }
+}
+
+void NestedRemapAnimation::initializeAnimation(ArtboardInstance* artboard)
+{
+    Super::initializeAnimation(artboard);
+    timeChanged();
+}
+
+bool NestedRemapAnimation::advance(float elapsedSeconds)
+{
+    bool keepGoing = false;
+    if (m_AnimationInstance != nullptr && mix() != 0.0f)
+    {
+        m_AnimationInstance->apply(mix());
+        keepGoing = true;
+    }
+    return keepGoing;
+}
\ No newline at end of file
diff --git a/src/animation/nested_simple_animation.cpp b/src/animation/nested_simple_animation.cpp
new file mode 100644
index 0000000..c747fd2
--- /dev/null
+++ b/src/animation/nested_simple_animation.cpp
@@ -0,0 +1,22 @@
+#include "rive/animation/nested_simple_animation.hpp"
+#include "rive/animation/linear_animation_instance.hpp"
+
+using namespace rive;
+
+bool NestedSimpleAnimation::advance(float elapsedSeconds)
+{
+    bool keepGoing = false;
+    if (m_AnimationInstance != nullptr)
+    {
+        if (isPlaying())
+        {
+            keepGoing =
+                m_AnimationInstance->advance(elapsedSeconds * speed(), m_AnimationInstance.get());
+        }
+        if (mix() != 0.0f)
+        {
+            m_AnimationInstance->apply(mix());
+        }
+    }
+    return keepGoing;
+}
\ No newline at end of file
diff --git a/src/animation/nested_state_machine.cpp b/src/animation/nested_state_machine.cpp
new file mode 100644
index 0000000..8176757
--- /dev/null
+++ b/src/animation/nested_state_machine.cpp
@@ -0,0 +1,126 @@
+#include "rive/animation/nested_bool.hpp"
+#include "rive/animation/nested_input.hpp"
+#include "rive/animation/nested_number.hpp"
+#include "rive/animation/nested_state_machine.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/hit_result.hpp"
+
+using namespace rive;
+
+NestedStateMachine::NestedStateMachine() {}
+NestedStateMachine::~NestedStateMachine() {}
+
+bool NestedStateMachine::advance(float elapsedSeconds)
+{
+    bool keepGoing = false;
+    if (m_StateMachineInstance != nullptr)
+    {
+        keepGoing = m_StateMachineInstance->advance(elapsedSeconds);
+    }
+    return keepGoing;
+}
+
+void NestedStateMachine::initializeAnimation(ArtboardInstance* artboard)
+{
+    m_StateMachineInstance = artboard->stateMachineAt(animationId());
+    auto count = m_nestedInputs.size();
+    for (size_t i = 0; i < count; i++)
+    {
+        auto nestedInput = m_nestedInputs[i];
+        if (nestedInput->is<NestedBool>() || nestedInput->is<NestedNumber>())
+        {
+            nestedInput->applyValue();
+        }
+    }
+}
+
+StateMachineInstance* NestedStateMachine::stateMachineInstance()
+{
+    return m_StateMachineInstance.get();
+}
+
+#ifdef WITH_RIVE_TOOLS
+bool NestedStateMachine::hitTest(Vec2D position) const
+{
+    if (m_StateMachineInstance != nullptr)
+    {
+        return m_StateMachineInstance->hitTest(position);
+    }
+    return false;
+}
+#endif
+
+HitResult NestedStateMachine::pointerMove(Vec2D position)
+{
+    if (m_StateMachineInstance != nullptr)
+    {
+        return m_StateMachineInstance->pointerMove(position);
+    }
+    return HitResult::none;
+}
+
+HitResult NestedStateMachine::pointerDown(Vec2D position)
+{
+    if (m_StateMachineInstance != nullptr)
+    {
+        return m_StateMachineInstance->pointerDown(position);
+    }
+    return HitResult::none;
+}
+
+HitResult NestedStateMachine::pointerUp(Vec2D position)
+{
+    if (m_StateMachineInstance != nullptr)
+    {
+        return m_StateMachineInstance->pointerUp(position);
+    }
+    return HitResult::none;
+}
+
+HitResult NestedStateMachine::pointerExit(Vec2D position)
+{
+    if (m_StateMachineInstance != nullptr)
+    {
+        return m_StateMachineInstance->pointerExit(position);
+    }
+    return HitResult::none;
+}
+
+NestedInput* NestedStateMachine::input(size_t index)
+{
+    if (index < m_nestedInputs.size())
+    {
+        return m_nestedInputs[index];
+    }
+    return nullptr;
+}
+
+NestedInput* NestedStateMachine::input(std::string name)
+{
+    for (auto input : m_nestedInputs)
+    {
+        if (input->name() == name)
+        {
+            return input;
+        }
+    }
+    return nullptr;
+}
+
+void NestedStateMachine::addNestedInput(NestedInput* input) { m_nestedInputs.push_back(input); }
+
+void NestedStateMachine::dataContextFromInstance(ViewModelInstance* viewModelInstance)
+{
+    if (m_StateMachineInstance != nullptr)
+    {
+        m_StateMachineInstance->dataContextFromInstance(viewModelInstance);
+    }
+}
+
+void NestedStateMachine::dataContext(DataContext* dataContext)
+{
+    if (m_StateMachineInstance != nullptr)
+    {
+        m_StateMachineInstance->dataContext(dataContext);
+    }
+}
\ No newline at end of file
diff --git a/src/animation/nested_trigger.cpp b/src/animation/nested_trigger.cpp
new file mode 100644
index 0000000..89fb586
--- /dev/null
+++ b/src/animation/nested_trigger.cpp
@@ -0,0 +1,22 @@
+#include "rive/animation/nested_trigger.hpp"
+#include "rive/animation/nested_state_machine.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+#include "rive/container_component.hpp"
+
+using namespace rive;
+class StateMachineInstance;
+
+void NestedTrigger::fire(const CallbackData& value) { this->applyValue(); }
+
+void NestedTrigger::applyValue()
+{
+    auto inputInstance = input();
+    if (inputInstance != nullptr)
+    {
+        auto numInput = static_cast<SMITrigger*>(inputInstance);
+        if (numInput != nullptr)
+        {
+            numInput->fire();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/animation/state_instance.cpp b/src/animation/state_instance.cpp
new file mode 100644
index 0000000..e698651
--- /dev/null
+++ b/src/animation/state_instance.cpp
@@ -0,0 +1,8 @@
+#include "rive/animation/state_instance.hpp"
+using namespace rive;
+
+StateInstance::StateInstance(const LayerState* layerState) : m_LayerState(layerState) {}
+
+StateInstance::~StateInstance() {}
+
+const LayerState* StateInstance::state() const { return m_LayerState; }
\ No newline at end of file
diff --git a/src/animation/state_machine.cpp b/src/animation/state_machine.cpp
new file mode 100644
index 0000000..9b44672
--- /dev/null
+++ b/src/animation/state_machine.cpp
@@ -0,0 +1,157 @@
+#include "rive/animation/state_machine.hpp"
+#include "rive/artboard.hpp"
+#include "rive/importers/artboard_importer.hpp"
+#include "rive/animation/state_machine_layer.hpp"
+#include "rive/animation/state_machine_input.hpp"
+#include "rive/animation/state_machine_listener.hpp"
+
+using namespace rive;
+
+StateMachine::StateMachine() {}
+
+StateMachine::~StateMachine() {}
+
+StatusCode StateMachine::onAddedDirty(CoreContext* context)
+{
+    StatusCode code;
+    for (auto& object : m_Inputs)
+    {
+        if ((code = object->onAddedDirty(context)) != StatusCode::Ok)
+        {
+            return code;
+        }
+    }
+    for (auto& object : m_Layers)
+    {
+        if ((code = object->onAddedDirty(context)) != StatusCode::Ok)
+        {
+            return code;
+        }
+    }
+    for (auto& object : m_Listeners)
+    {
+        if ((code = object->onAddedDirty(context)) != StatusCode::Ok)
+        {
+            return code;
+        }
+    }
+    return StatusCode::Ok;
+}
+
+StatusCode StateMachine::onAddedClean(CoreContext* context)
+{
+    StatusCode code;
+    for (auto& object : m_Inputs)
+    {
+        if ((code = object->onAddedClean(context)) != StatusCode::Ok)
+        {
+            return code;
+        }
+    }
+    for (auto& object : m_Layers)
+    {
+        if ((code = object->onAddedClean(context)) != StatusCode::Ok)
+        {
+            return code;
+        }
+    }
+    for (auto& object : m_Listeners)
+    {
+        if ((code = object->onAddedClean(context)) != StatusCode::Ok)
+        {
+            return code;
+        }
+    }
+    return StatusCode::Ok;
+}
+
+StatusCode StateMachine::import(ImportStack& importStack)
+{
+    auto artboardImporter = importStack.latest<ArtboardImporter>(ArtboardBase::typeKey);
+    if (artboardImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    artboardImporter->addStateMachine(this);
+    return Super::import(importStack);
+}
+
+void StateMachine::addLayer(std::unique_ptr<StateMachineLayer> layer)
+{
+    m_Layers.push_back(std::move(layer));
+}
+
+void StateMachine::addInput(std::unique_ptr<StateMachineInput> input)
+{
+    m_Inputs.push_back(std::move(input));
+}
+
+void StateMachine::addListener(std::unique_ptr<StateMachineListener> listener)
+{
+    m_Listeners.push_back(std::move(listener));
+}
+
+void StateMachine::addDataBind(std::unique_ptr<DataBind> dataBind)
+{
+    m_dataBinds.push_back(std::move(dataBind));
+}
+
+const StateMachineInput* StateMachine::input(std::string name) const
+{
+    for (auto& input : m_Inputs)
+    {
+        if (input->name() == name)
+        {
+            return input.get();
+        }
+    }
+    return nullptr;
+}
+
+const StateMachineInput* StateMachine::input(size_t index) const
+{
+    if (index < m_Inputs.size())
+    {
+        return m_Inputs[index].get();
+    }
+    return nullptr;
+}
+
+const StateMachineLayer* StateMachine::layer(std::string name) const
+{
+    for (auto& layer : m_Layers)
+    {
+        if (layer->name() == name)
+        {
+            return layer.get();
+        }
+    }
+    return nullptr;
+}
+
+const StateMachineLayer* StateMachine::layer(size_t index) const
+{
+    if (index < m_Layers.size())
+    {
+        return m_Layers[index].get();
+    }
+    return nullptr;
+}
+
+const StateMachineListener* StateMachine::listener(size_t index) const
+{
+    if (index < m_Listeners.size())
+    {
+        return m_Listeners[index].get();
+    }
+    return nullptr;
+}
+
+const DataBind* StateMachine::dataBind(size_t index) const
+{
+    if (index < m_dataBinds.size())
+    {
+        return m_dataBinds[index].get();
+    }
+    return nullptr;
+}
\ No newline at end of file
diff --git a/src/animation/state_machine_input.cpp b/src/animation/state_machine_input.cpp
new file mode 100644
index 0000000..981f717
--- /dev/null
+++ b/src/animation/state_machine_input.cpp
@@ -0,0 +1,22 @@
+#include "rive/animation/state_machine_input.hpp"
+#include "rive/importers/import_stack.hpp"
+#include "rive/importers/state_machine_importer.hpp"
+#include "rive/generated/animation/state_machine_base.hpp"
+
+using namespace rive;
+
+StatusCode StateMachineInput::onAddedDirty(CoreContext* context) { return StatusCode::Ok; }
+
+StatusCode StateMachineInput::onAddedClean(CoreContext* context) { return StatusCode::Ok; }
+
+StatusCode StateMachineInput::import(ImportStack& importStack)
+{
+    auto stateMachineImporter = importStack.latest<StateMachineImporter>(StateMachineBase::typeKey);
+    if (stateMachineImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    // WOW -- we're handing off ownership of this!
+    stateMachineImporter->addInput(std::unique_ptr<StateMachineInput>(this));
+    return Super::import(importStack);
+}
\ No newline at end of file
diff --git a/src/animation/state_machine_input_instance.cpp b/src/animation/state_machine_input_instance.cpp
new file mode 100644
index 0000000..d2e98d6
--- /dev/null
+++ b/src/animation/state_machine_input_instance.cpp
@@ -0,0 +1,73 @@
+#include "rive/animation/state_machine_input_instance.hpp"
+#include "rive/animation/state_machine_bool.hpp"
+#include "rive/animation/state_machine_number.hpp"
+#include "rive/animation/state_machine_trigger.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+
+using namespace rive;
+
+SMIInput::SMIInput(const StateMachineInput* input, StateMachineInstance* machineInstance) :
+    m_machineInstance(machineInstance), m_input(input)
+{}
+
+uint16_t SMIInput::inputCoreType() const { return m_input->coreType(); }
+
+const std::string& SMIInput::name() const { return m_input->name(); }
+
+void SMIInput::valueChanged()
+{
+    m_machineInstance->markNeedsAdvance();
+#ifdef WITH_RIVE_TOOLS
+    auto callback = m_machineInstance->m_inputChangedCallback;
+    if (callback != nullptr)
+    {
+        callback(m_machineInstance, m_index);
+    }
+#endif
+}
+
+// bool
+
+SMIBool::SMIBool(const StateMachineBool* input, StateMachineInstance* machineInstance) :
+    SMIInput(input, machineInstance), m_Value(input->value())
+{}
+
+void SMIBool::value(bool newValue)
+{
+    if (m_Value == newValue)
+    {
+        return;
+    }
+    m_Value = newValue;
+    valueChanged();
+}
+
+// number
+SMINumber::SMINumber(const StateMachineNumber* input, StateMachineInstance* machineInstance) :
+    SMIInput(input, machineInstance), m_Value(input->value())
+{}
+
+void SMINumber::value(float newValue)
+{
+    if (m_Value == newValue)
+    {
+        return;
+    }
+    m_Value = newValue;
+    valueChanged();
+}
+
+// trigger
+SMITrigger::SMITrigger(const StateMachineTrigger* input, StateMachineInstance* machineInstance) :
+    SMIInput(input, machineInstance)
+{}
+
+void SMITrigger::fire()
+{
+    if (m_fired)
+    {
+        return;
+    }
+    m_fired = true;
+    valueChanged();
+}
diff --git a/src/animation/state_machine_instance.cpp b/src/animation/state_machine_instance.cpp
new file mode 100644
index 0000000..e549a1a
--- /dev/null
+++ b/src/animation/state_machine_instance.cpp
@@ -0,0 +1,1293 @@
+#include "rive/animation/animation_reset.hpp"
+#include "rive/animation/animation_reset_factory.hpp"
+#include "rive/animation/animation_state_instance.hpp"
+#include "rive/animation/animation_state.hpp"
+#include "rive/animation/any_state.hpp"
+#include "rive/animation/cubic_interpolator.hpp"
+#include "rive/animation/entry_state.hpp"
+#include "rive/animation/layer_state_flags.hpp"
+#include "rive/animation/nested_linear_animation.hpp"
+#include "rive/animation/nested_state_machine.hpp"
+#include "rive/animation/state_instance.hpp"
+#include "rive/animation/state_machine_bool.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+#include "rive/animation/state_machine_input.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/animation/state_machine_layer.hpp"
+#include "rive/animation/state_machine_listener.hpp"
+#include "rive/animation/state_machine_number.hpp"
+#include "rive/animation/state_machine_trigger.hpp"
+#include "rive/animation/state_machine.hpp"
+#include "rive/animation/state_transition.hpp"
+#include "rive/animation/transition_condition.hpp"
+#include "rive/animation/transition_comparator.hpp"
+#include "rive/animation/transition_property_viewmodel_comparator.hpp"
+#include "rive/animation/transition_viewmodel_condition.hpp"
+#include "rive/animation/state_machine_fire_event.hpp"
+#include "rive/data_bind_flags.hpp"
+#include "rive/event_report.hpp"
+#include "rive/gesture_click_phase.hpp"
+#include "rive/hit_result.hpp"
+#include "rive/math/aabb.hpp"
+#include "rive/math/hit_test.hpp"
+#include "rive/nested_animation.hpp"
+#include "rive/nested_artboard.hpp"
+#include "rive/shapes/shape.hpp"
+#include "rive/math/math_types.hpp"
+#include "rive/audio_event.hpp"
+#include <unordered_map>
+#include <chrono>
+
+using namespace rive;
+namespace rive
+{
+class StateMachineLayerInstance
+{
+public:
+    ~StateMachineLayerInstance()
+    {
+        delete m_anyStateInstance;
+        delete m_currentState;
+        delete m_stateFrom;
+    }
+
+    void init(StateMachineInstance* stateMachineInstance,
+              const StateMachineLayer* layer,
+              ArtboardInstance* instance)
+    {
+        m_stateMachineInstance = stateMachineInstance;
+        m_artboardInstance = instance;
+        assert(m_layer == nullptr);
+        m_anyStateInstance = layer->anyState()->makeInstance(instance).release();
+        m_layer = layer;
+        changeState(m_layer->entryState());
+        auto now = std::chrono::high_resolution_clock::now();
+        auto nanos =
+            std::chrono::duration_cast<std::chrono::nanoseconds>(now.time_since_epoch()).count();
+        srand(nanos);
+    }
+
+    void updateMix(float seconds)
+    {
+        if (m_transition != nullptr && m_stateFrom != nullptr && m_transition->duration() != 0)
+        {
+            m_mix = std::min(
+                1.0f,
+                std::max(0.0f, (m_mix + seconds / m_transition->mixTime(m_stateFrom->state()))));
+            if (m_mix == 1.0f && !m_transitionCompleted)
+            {
+                m_transitionCompleted = true;
+                clearAnimationReset();
+                fireEvents(StateMachineFireOccurance::atEnd, m_transition->events());
+            }
+        }
+        else
+        {
+            m_mix = 1.0f;
+        }
+    }
+
+    bool advance(float seconds)
+    {
+        m_stateMachineChangedOnAdvance = false;
+        m_currentState->advance(seconds, m_stateMachineInstance);
+        updateMix(seconds);
+
+        if (m_stateFrom != nullptr && m_mix < 1.0f && !m_holdAnimationFrom)
+        {
+            // This didn't advance during our updateState, but it should now
+            // that we realize we need to mix it in.
+            m_stateFrom->advance(seconds, m_stateMachineInstance);
+        }
+
+        apply();
+
+        for (int i = 0; updateState(i != 0); i++)
+        {
+            apply();
+
+            if (i == maxIterations)
+            {
+                fprintf(stderr, "StateMachine exceeded max iterations.\n");
+                return false;
+            }
+        }
+
+        m_currentState->clearSpilledTime();
+
+        return m_mix != 1.0f || m_waitingForExit ||
+               (m_currentState != nullptr && m_currentState->keepGoing());
+    }
+
+    bool isTransitioning()
+    {
+        return m_transition != nullptr && m_stateFrom != nullptr && m_transition->duration() != 0 &&
+               m_mix < 1.0f;
+    }
+
+    bool updateState(bool ignoreTriggers)
+    {
+        // Don't allow changing state while a transition is taking place
+        // (we're mixing one state onto another) if enableEarlyExit is not true.
+        if (isTransitioning() && !m_transition->enableEarlyExit())
+        {
+            return false;
+        }
+
+        m_waitingForExit = false;
+
+        if (tryChangeState(m_anyStateInstance, ignoreTriggers))
+        {
+            return true;
+        }
+
+        return tryChangeState(m_currentState, ignoreTriggers);
+    }
+
+    void fireEvents(StateMachineFireOccurance occurs,
+                    const std::vector<StateMachineFireEvent*>& fireEvents)
+    {
+        for (auto event : fireEvents)
+        {
+            if (event->occurs() == occurs)
+            {
+                event->perform(m_stateMachineInstance);
+            }
+        }
+    }
+
+    bool canChangeState(const LayerState* stateTo)
+    {
+        return !((m_currentState == nullptr ? nullptr : m_currentState->state()) == stateTo);
+    }
+
+    double randomValue() { return ((double)rand() / (RAND_MAX)); }
+
+    bool changeState(const LayerState* stateTo)
+    {
+        if ((m_currentState == nullptr ? nullptr : m_currentState->state()) == stateTo)
+        {
+            return false;
+        }
+
+        // Fire end events for the state we're changing from.
+        if (m_currentState != nullptr)
+        {
+            fireEvents(StateMachineFireOccurance::atEnd, m_currentState->state()->events());
+        }
+
+        m_currentState =
+            stateTo == nullptr ? nullptr : stateTo->makeInstance(m_artboardInstance).release();
+
+        // Fire start events for the state we're changing to.
+        if (m_currentState != nullptr)
+        {
+            fireEvents(StateMachineFireOccurance::atStart, m_currentState->state()->events());
+        }
+        return true;
+    }
+
+    StateTransition* findRandomTransition(StateInstance* stateFromInstance, bool ignoreTriggers)
+    {
+        uint32_t totalWeight = 0;
+        auto stateFrom = stateFromInstance->state();
+        for (size_t i = 0, length = stateFrom->transitionCount(); i < length; i++)
+        {
+            auto transition = stateFrom->transition(i);
+            auto allowed =
+                transition->allowed(stateFromInstance, m_stateMachineInstance, ignoreTriggers);
+            if (allowed == AllowTransition::yes && canChangeState(transition->stateTo()))
+            {
+                transition->evaluatedRandomWeight(transition->randomWeight());
+                totalWeight += transition->randomWeight();
+            }
+            else
+            {
+                transition->evaluatedRandomWeight(0);
+                if (allowed == AllowTransition::waitingForExit)
+                {
+                    m_waitingForExit = true;
+                }
+            }
+        }
+        if (totalWeight > 0)
+        {
+            double randomWeight = randomValue() * totalWeight * 1.0;
+            float currentWeight = 0;
+            size_t index = 0;
+            StateTransition* transition;
+            while (index < stateFrom->transitionCount())
+            {
+                transition = stateFrom->transition(index);
+                auto transitionWeight = transition->evaluatedRandomWeight();
+                if (currentWeight + transitionWeight > randomWeight)
+                {
+                    return transition;
+                }
+                currentWeight += transitionWeight;
+                index++;
+            }
+        }
+        return nullptr;
+    }
+
+    StateTransition* findAllowedTransition(StateInstance* stateFromInstance, bool ignoreTriggers)
+    {
+        auto stateFrom = stateFromInstance->state();
+        // If it should randomize
+        if ((static_cast<LayerStateFlags>(stateFrom->flags()) & LayerStateFlags::Random) ==
+            LayerStateFlags::Random)
+        {
+            return findRandomTransition(stateFromInstance, ignoreTriggers);
+        }
+        // Else search the first valid transition
+        for (size_t i = 0, length = stateFrom->transitionCount(); i < length; i++)
+        {
+            auto transition = stateFrom->transition(i);
+            auto allowed =
+                transition->allowed(stateFromInstance, m_stateMachineInstance, ignoreTriggers);
+            if (allowed == AllowTransition::yes && canChangeState(transition->stateTo()))
+            {
+                transition->evaluatedRandomWeight(transition->randomWeight());
+                return transition;
+            }
+            else
+            {
+                transition->evaluatedRandomWeight(0);
+                if (allowed == AllowTransition::waitingForExit)
+                {
+                    m_waitingForExit = true;
+                }
+            }
+        }
+        return nullptr;
+    }
+
+    void buildAnimationResetForTransition()
+    {
+        m_animationReset =
+            AnimationResetFactory::fromStates(m_stateFrom, m_currentState, m_artboardInstance);
+    }
+
+    void clearAnimationReset()
+    {
+        if (m_animationReset != nullptr)
+        {
+            AnimationResetFactory::release(std::move(m_animationReset));
+            m_animationReset = nullptr;
+        }
+    }
+
+    bool tryChangeState(StateInstance* stateFromInstance, bool ignoreTriggers)
+    {
+        if (stateFromInstance == nullptr)
+        {
+            return false;
+        }
+        auto outState = m_currentState;
+        auto transition = findAllowedTransition(stateFromInstance, ignoreTriggers);
+        if (transition != nullptr)
+        {
+            clearAnimationReset();
+            changeState(transition->stateTo());
+            m_stateMachineChangedOnAdvance = true;
+            // state actually has changed
+            m_transition = transition;
+            fireEvents(StateMachineFireOccurance::atStart, transition->events());
+            if (transition->duration() == 0)
+            {
+                m_transitionCompleted = true;
+                fireEvents(StateMachineFireOccurance::atEnd, transition->events());
+            }
+            else
+            {
+                m_transitionCompleted = false;
+            }
+
+            if (m_stateFrom != m_anyStateInstance)
+            {
+                // Old state from is done.
+                delete m_stateFrom;
+            }
+            m_stateFrom = outState;
+
+            if (!m_transitionCompleted)
+            {
+                buildAnimationResetForTransition();
+            }
+
+            // If we had an exit time and wanted to pause on exit, make
+            // sure to hold the exit time. Delegate this to the
+            // transition by telling it that it was completed.
+            if (outState != nullptr && transition->applyExitCondition(outState))
+            {
+                // Make sure we apply this state. This only returns true
+                // when it's an animation state instance.
+                auto instance =
+                    static_cast<AnimationStateInstance*>(m_stateFrom)->animationInstance();
+
+                m_holdAnimation = instance->animation();
+                m_holdTime = instance->time();
+            }
+            m_mixFrom = m_mix;
+
+            // Keep mixing last animation that was mixed in.
+            if (m_mix != 0.0f)
+            {
+                m_holdAnimationFrom = transition->pauseOnExit();
+            }
+            if (m_stateFrom != nullptr && m_stateFrom->state()->is<AnimationState>() &&
+                m_currentState != nullptr)
+            {
+                auto instance =
+                    static_cast<AnimationStateInstance*>(m_stateFrom)->animationInstance();
+
+                auto spilledTime = instance->spilledTime();
+                m_currentState->advance(spilledTime, m_stateMachineInstance);
+            }
+            m_mix = 0.0f;
+            updateMix(0.0f);
+            m_waitingForExit = false;
+            return true;
+        }
+        return false;
+    }
+
+    void apply(/*Artboard* artboard*/)
+    {
+        if (m_animationReset != nullptr)
+        {
+            m_animationReset->apply(m_artboardInstance);
+        }
+        if (m_holdAnimation != nullptr)
+        {
+            m_holdAnimation->apply(m_artboardInstance, m_holdTime, m_mixFrom);
+            m_holdAnimation = nullptr;
+        }
+
+        CubicInterpolator* cubic = nullptr;
+        if (m_transition != nullptr && m_transition->interpolator() != nullptr)
+        {
+            cubic = m_transition->interpolator();
+        }
+
+        if (m_stateFrom != nullptr && m_mix < 1.0f)
+        {
+            auto fromMix = cubic != nullptr ? cubic->transform(m_mixFrom) : m_mixFrom;
+            m_stateFrom->apply(m_artboardInstance, fromMix);
+        }
+        if (m_currentState != nullptr)
+        {
+            auto mix = cubic != nullptr ? cubic->transform(m_mix) : m_mix;
+            m_currentState->apply(m_artboardInstance, mix);
+        }
+    }
+
+    bool stateChangedOnAdvance() const { return m_stateMachineChangedOnAdvance; }
+
+    const LayerState* currentState()
+    {
+        return m_currentState == nullptr ? nullptr : m_currentState->state();
+    }
+
+    const LinearAnimationInstance* currentAnimation() const
+    {
+        if (m_currentState == nullptr || !m_currentState->state()->is<AnimationState>())
+        {
+            return nullptr;
+        }
+        return static_cast<AnimationStateInstance*>(m_currentState)->animationInstance();
+    }
+
+private:
+    static const int maxIterations = 100;
+    StateMachineInstance* m_stateMachineInstance = nullptr;
+    const StateMachineLayer* m_layer = nullptr;
+    ArtboardInstance* m_artboardInstance = nullptr;
+
+    StateInstance* m_anyStateInstance = nullptr;
+    StateInstance* m_currentState = nullptr;
+    StateInstance* m_stateFrom = nullptr;
+
+    const StateTransition* m_transition = nullptr;
+    std::unique_ptr<AnimationReset> m_animationReset = nullptr;
+    bool m_transitionCompleted = false;
+
+    bool m_holdAnimationFrom = false;
+
+    float m_mix = 1.0f;
+    float m_mixFrom = 1.0f;
+    bool m_stateMachineChangedOnAdvance = false;
+
+    bool m_waitingForExit = false;
+    /// Used to ensure a specific animation is applied on the next apply.
+    const LinearAnimation* m_holdAnimation = nullptr;
+    float m_holdTime = 0.0f;
+};
+
+class ListenerGroup
+{
+public:
+    ListenerGroup(const StateMachineListener* listener) : m_listener(listener) {}
+    void consume() { m_isConsumed = true; }
+    //
+    void hover() { m_isHovered = true; }
+    void unhover() { m_isHovered = false; }
+    void reset()
+    {
+        m_isConsumed = false;
+        m_prevIsHovered = m_isHovered;
+        m_isHovered = false;
+        if (m_clickPhase == GestureClickPhase::clicked)
+        {
+            m_clickPhase = GestureClickPhase::out;
+        }
+    }
+    bool isConsumed() { return m_isConsumed; }
+    bool isHovered() { return m_isHovered; }
+    bool prevHovered() { return m_prevIsHovered; }
+    void clickPhase(GestureClickPhase value) { m_clickPhase = value; }
+    GestureClickPhase clickPhase() { return m_clickPhase; }
+    const StateMachineListener* listener() const { return m_listener; };
+    // A vector storing the previous position for this specific listener gorup
+    Vec2D previousPosition;
+
+private:
+    // Consumed listeners aren't processed again in the current frame
+    bool m_isConsumed = false;
+    // This variable holds the hover status of the the listener itself so it can
+    // be shared between all shapes that target it
+    bool m_isHovered = false;
+    // Variable storing the previous hovered state to check for hover changes
+    bool m_prevIsHovered = false;
+    // A click gesture is composed of three phases and is shared between all shapes
+    GestureClickPhase m_clickPhase = GestureClickPhase::out;
+    const StateMachineListener* m_listener;
+};
+
+/// Representation of a Shape from the Artboard Instance and all the listeners it
+/// triggers. Allows tracking hover and performing hit detection only once on
+/// shapes that trigger multiple listeners.
+class HitShape : public HitComponent
+{
+public:
+    HitShape(Component* shape, StateMachineInstance* stateMachineInstance) :
+        HitComponent(shape, stateMachineInstance)
+    {
+        if (shape->as<Shape>()->isTargetOpaque())
+        {
+            canEarlyOut = false;
+        }
+    }
+    bool isHovered = false;
+    bool canEarlyOut = true;
+    bool hasDownListener = false;
+    bool hasUpListener = false;
+    float hitRadius = 2;
+    std::vector<ListenerGroup*> listeners;
+
+    bool hitTest(Vec2D position) const
+#ifdef WITH_RIVE_TOOLS
+        override
+#endif
+    {
+        auto shape = m_component->as<Shape>();
+        auto worldBounds = shape->worldBounds();
+        if (!worldBounds.contains(position))
+        {
+            return false;
+        }
+        auto hitArea = AABB(position.x - hitRadius,
+                            position.y - hitRadius,
+                            position.x + hitRadius,
+                            position.y + hitRadius)
+                           .round();
+        return shape->hitTest(hitArea);
+    }
+
+    void prepareEvent(Vec2D position, ListenerType hitType) override
+    {
+        if (canEarlyOut && (hitType != ListenerType::down || !hasDownListener) &&
+            (hitType != ListenerType::up || !hasUpListener))
+        {
+#ifdef TESTING
+            earlyOutCount++;
+#endif
+            return;
+        }
+        isHovered = hitTest(position);
+
+        // // iterate all listeners associated with this hit shape
+        if (isHovered)
+        {
+            for (auto listenerGroup : listeners)
+            {
+
+                listenerGroup->hover();
+            }
+        }
+    }
+
+    HitResult processEvent(Vec2D position, ListenerType hitType, bool canHit) override
+    {
+        // If the shape doesn't have any ListenerType::move / enter / exit and the event
+        // being processed is not of the type it needs to handle. There is no need to perform
+        // a hitTest (which is relatively expensive and would be happening on every
+        // pointer move) so we early out.
+        if (canEarlyOut && (hitType != ListenerType::down || !hasDownListener) &&
+            (hitType != ListenerType::up || !hasUpListener))
+        {
+            return HitResult::none;
+        }
+        auto shape = m_component->as<Shape>();
+
+        // // iterate all listeners associated with this hit shape
+        for (auto listenerGroup : listeners)
+        {
+            if (listenerGroup->isConsumed())
+            {
+                continue;
+            }
+            // Because each group is tested individually for its hover state, a group
+            // could be marked "incorrectly" as hovered at this point.
+            // But once we iterate each element in the drawing order, that group can
+            // be occluded by an opaque target on top  of it.
+            // So although it is hovered in isolation, it shouldn't be considered as
+            // hovered in the full context.
+            // In this case, we unhover the group so it is not marked as previously
+            // hovered.
+            if (!canHit && listenerGroup->isHovered())
+            {
+                listenerGroup->unhover();
+            }
+
+            bool isGroupHovered = canHit ? listenerGroup->isHovered() : false;
+            bool hoverChange = listenerGroup->prevHovered() != isGroupHovered;
+            // If hover has changes, it means that the element is hovered for the
+            // first time. Previous positions need to be reset to avoid jumps.
+            if (hoverChange && isGroupHovered)
+            {
+                listenerGroup->previousPosition.x = position.x;
+                listenerGroup->previousPosition.y = position.y;
+            }
+
+            // Handle click gesture phases. A click gesture has two phases.
+            // First one attached to a pointer down actions, second one attached to a
+            // pointer up action. Both need to act on a shape of the listener group.
+            if (isGroupHovered)
+            {
+                if (hitType == ListenerType::down)
+                {
+                    listenerGroup->clickPhase(GestureClickPhase::down);
+                }
+                else if (hitType == ListenerType::up &&
+                         listenerGroup->clickPhase() == GestureClickPhase::down)
+                {
+                    listenerGroup->clickPhase(GestureClickPhase::clicked);
+                }
+            }
+            else
+            {
+                if (hitType == ListenerType::down || hitType == ListenerType::up)
+                {
+                    listenerGroup->clickPhase(GestureClickPhase::out);
+                }
+            }
+            auto listener = listenerGroup->listener();
+            // Always update hover states regardless of which specific listener type
+            // we're trying to trigger.
+            // If hover has changed and:
+            // - it's hovering and the listener is of type enter
+            // - it's not hovering and the listener is of type exit
+            if (hoverChange &&
+                ((isGroupHovered && listener->listenerType() == ListenerType::enter) ||
+                 (!isGroupHovered && listener->listenerType() == ListenerType::exit)))
+            {
+                listener->performChanges(m_stateMachineInstance,
+                                         position,
+                                         listenerGroup->previousPosition);
+                m_stateMachineInstance->markNeedsAdvance();
+                listenerGroup->consume();
+            }
+            // Perform changes if:
+            // - the click gesture is complete and the listener is of type click
+            // - the event type matches the listener type and it is hovering the group
+            if ((listenerGroup->clickPhase() == GestureClickPhase::clicked &&
+                 listener->listenerType() == ListenerType::click) ||
+                (isGroupHovered && hitType == listener->listenerType()))
+            {
+                listener->performChanges(m_stateMachineInstance,
+                                         position,
+                                         listenerGroup->previousPosition);
+                m_stateMachineInstance->markNeedsAdvance();
+                listenerGroup->consume();
+            }
+            listenerGroup->previousPosition.x = position.x;
+            listenerGroup->previousPosition.y = position.y;
+        }
+        return (isHovered && canHit)
+                   ? shape->isTargetOpaque() ? HitResult::hitOpaque : HitResult::hit
+                   : HitResult::none;
+    }
+
+    void addListener(ListenerGroup* listenerGroup)
+    {
+        auto stateMachineListener = listenerGroup->listener();
+        auto listenerType = stateMachineListener->listenerType();
+        if (listenerType == ListenerType::enter || listenerType == ListenerType::exit ||
+            listenerType == ListenerType::move)
+        {
+            canEarlyOut = false;
+        }
+        else
+        {
+            if (listenerType == ListenerType::down || listenerType == ListenerType::click)
+            {
+                hasDownListener = true;
+            }
+            if (listenerType == ListenerType::up || listenerType == ListenerType::click)
+            {
+                hasUpListener = true;
+            }
+        }
+        listeners.push_back(listenerGroup);
+    }
+};
+class HitNestedArtboard : public HitComponent
+{
+public:
+    HitNestedArtboard(Component* nestedArtboard, StateMachineInstance* stateMachineInstance) :
+        HitComponent(nestedArtboard, stateMachineInstance)
+    {}
+    ~HitNestedArtboard() override {}
+
+#ifdef WITH_RIVE_TOOLS
+    bool hitTest(Vec2D position) const override
+    {
+        auto nestedArtboard = m_component->as<NestedArtboard>();
+        if (nestedArtboard->isCollapsed())
+        {
+            return false;
+        }
+        Vec2D nestedPosition;
+        if (!nestedArtboard->worldToLocal(position, &nestedPosition))
+        {
+            // Mounted artboard isn't ready or has a 0 scale transform.
+            return false;
+        }
+
+        for (auto nestedAnimation : nestedArtboard->nestedAnimations())
+        {
+            if (nestedAnimation->is<NestedStateMachine>())
+            {
+                auto nestedStateMachine = nestedAnimation->as<NestedStateMachine>();
+                if (nestedStateMachine->hitTest(nestedPosition))
+                {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+#endif
+    HitResult processEvent(Vec2D position, ListenerType hitType, bool canHit) override
+    {
+        auto nestedArtboard = m_component->as<NestedArtboard>();
+        HitResult hitResult = HitResult::none;
+        if (nestedArtboard->isCollapsed())
+        {
+            return hitResult;
+        }
+        Vec2D nestedPosition;
+        if (!nestedArtboard->worldToLocal(position, &nestedPosition))
+        {
+            // Mounted artboard isn't ready or has a 0 scale transform.
+            return hitResult;
+        }
+
+        for (auto nestedAnimation : nestedArtboard->nestedAnimations())
+        {
+            if (nestedAnimation->is<NestedStateMachine>())
+            {
+                auto nestedStateMachine = nestedAnimation->as<NestedStateMachine>();
+                if (canHit)
+                {
+                    switch (hitType)
+                    {
+                        case ListenerType::down:
+                            hitResult = nestedStateMachine->pointerDown(nestedPosition);
+                            break;
+                        case ListenerType::up:
+                            hitResult = nestedStateMachine->pointerUp(nestedPosition);
+                            break;
+                        case ListenerType::move:
+                            hitResult = nestedStateMachine->pointerMove(nestedPosition);
+                            break;
+                        case ListenerType::enter:
+                        case ListenerType::exit:
+                        case ListenerType::event:
+                        case ListenerType::click:
+                            break;
+                    }
+                }
+                else
+                {
+                    switch (hitType)
+                    {
+                        case ListenerType::down:
+                        case ListenerType::up:
+                        case ListenerType::move:
+                            nestedStateMachine->pointerExit(nestedPosition);
+                            break;
+                        case ListenerType::enter:
+                        case ListenerType::exit:
+                        case ListenerType::event:
+                        case ListenerType::click:
+                            break;
+                    }
+                }
+            }
+        }
+        return hitResult;
+    }
+    void prepareEvent(Vec2D position, ListenerType hitType) override {}
+};
+
+} // namespace rive
+
+HitResult StateMachineInstance::updateListeners(Vec2D position, ListenerType hitType)
+{
+    if (m_artboardInstance->frameOrigin())
+    {
+        position -= Vec2D(m_artboardInstance->originX() * m_artboardInstance->width(),
+                          m_artboardInstance->originY() * m_artboardInstance->height());
+    }
+    // First reset all listener groups before processing the events
+    for (const auto& listenerGroup : m_listenerGroups)
+    {
+        listenerGroup.get()->reset();
+    }
+    // Next prepare the event to set the common hover status for each group
+    for (const auto& hitShape : m_hitComponents)
+    {
+        hitShape->prepareEvent(position, hitType);
+    }
+    bool hitSomething = false;
+    bool hitOpaque = false;
+    // Finally process the events
+    for (const auto& hitShape : m_hitComponents)
+    {
+        // TODO: quick reject.
+
+        HitResult hitResult = hitShape->processEvent(position, hitType, !hitOpaque);
+        if (hitResult != HitResult::none)
+        {
+            hitSomething = true;
+            if (hitResult == HitResult::hitOpaque)
+            {
+                hitOpaque = true;
+            }
+        }
+    }
+    return hitSomething ? hitOpaque ? HitResult::hitOpaque : HitResult::hit : HitResult::none;
+}
+
+#ifdef WITH_RIVE_TOOLS
+bool StateMachineInstance::hitTest(Vec2D position) const
+{
+    if (m_artboardInstance->frameOrigin())
+    {
+        position -= Vec2D(m_artboardInstance->originX() * m_artboardInstance->width(),
+                          m_artboardInstance->originY() * m_artboardInstance->height());
+    }
+
+    for (const auto& hitShape : m_hitComponents)
+    {
+        // TODO: quick reject.
+
+        if (hitShape->hitTest(position))
+        {
+            return true;
+        }
+    }
+    return false;
+}
+#endif
+
+HitResult StateMachineInstance::pointerMove(Vec2D position)
+{
+    return updateListeners(position, ListenerType::move);
+}
+HitResult StateMachineInstance::pointerDown(Vec2D position)
+{
+    return updateListeners(position, ListenerType::down);
+}
+HitResult StateMachineInstance::pointerUp(Vec2D position)
+{
+    return updateListeners(position, ListenerType::up);
+}
+HitResult StateMachineInstance::pointerExit(Vec2D position)
+{
+    return updateListeners(position, ListenerType::exit);
+}
+
+#ifdef TESTING
+const LayerState* StateMachineInstance::layerState(size_t index)
+{
+    if (index < m_machine->layerCount())
+    {
+        return m_layers[index].currentState();
+    }
+    return nullptr;
+}
+#endif
+
+StateMachineInstance::StateMachineInstance(const StateMachine* machine,
+                                           ArtboardInstance* instance) :
+    Scene(instance), m_machine(machine)
+{
+    const auto count = machine->inputCount();
+    m_inputInstances.resize(count);
+    for (size_t i = 0; i < count; i++)
+    {
+        auto input = machine->input(i);
+        if (input == nullptr)
+        {
+            continue;
+        }
+        switch (input->coreType())
+        {
+            case StateMachineBool::typeKey:
+                m_inputInstances[i] = new SMIBool(input->as<StateMachineBool>(), this);
+                break;
+            case StateMachineNumber::typeKey:
+                m_inputInstances[i] = new SMINumber(input->as<StateMachineNumber>(), this);
+                break;
+            case StateMachineTrigger::typeKey:
+                m_inputInstances[i] = new SMITrigger(input->as<StateMachineTrigger>(), this);
+                break;
+            default:
+                // Sanity check.
+                break;
+        }
+#ifdef WITH_RIVE_TOOLS
+        auto instance = m_inputInstances[i];
+        if (instance != nullptr)
+        {
+            instance->m_index = i;
+        }
+#endif
+    }
+
+    m_layerCount = machine->layerCount();
+    m_layers = new StateMachineLayerInstance[m_layerCount];
+    for (size_t i = 0; i < m_layerCount; i++)
+    {
+        m_layers[i].init(this, machine->layer(i), m_artboardInstance);
+    }
+
+    // Initialize dataBinds. All databinds are cloned for the state machine instance.
+    // That enables binding each instance to its own context without polluting the rest.
+    auto dataBindCount = machine->dataBindCount();
+    for (size_t i = 0; i < dataBindCount; i++)
+    {
+        auto dataBind = machine->dataBind(i);
+        auto dataBindClone = static_cast<DataBind*>(dataBind->clone());
+        m_dataBinds.push_back(dataBindClone);
+        if (dataBind->target()->is<BindableProperty>())
+        {
+            auto bindableProperty = dataBind->target()->as<BindableProperty>();
+            auto bindablePropertyInstance = m_bindablePropertyInstances.find(bindableProperty);
+            BindableProperty* bindablePropertyClone;
+            if (bindablePropertyInstance == m_bindablePropertyInstances.end())
+            {
+                bindablePropertyClone = bindableProperty->clone()->as<BindableProperty>();
+                m_bindablePropertyInstances[bindableProperty] = bindablePropertyClone;
+            }
+            else
+            {
+                bindablePropertyClone = bindablePropertyInstance->second;
+            }
+            dataBindClone->target(bindablePropertyClone);
+            // We are only storing in this unordered map data binds that are targetting the source.
+            // For now, this is only the case for listener actions.
+            if (static_cast<DataBindFlags>(dataBindClone->flags()) == DataBindFlags::ToSource)
+            {
+                m_bindableDataBinds[bindablePropertyClone] = dataBindClone;
+            }
+        }
+    }
+
+    // Initialize listeners. Store a lookup table of shape id to hit shape
+    // representation (an object that stores all the listeners triggered by the
+    // shape producing a listener).
+    std::unordered_map<Component*, HitShape*> hitShapeLookup;
+    for (std::size_t i = 0; i < machine->listenerCount(); i++)
+    {
+        auto listener = machine->listener(i);
+        if (listener->listenerType() == ListenerType::event)
+        {
+            continue;
+        }
+        auto listenerGroup = rivestd::make_unique<ListenerGroup>(listener);
+        auto target = m_artboardInstance->resolve(listener->targetId());
+        if (target != nullptr && target->is<ContainerComponent>())
+        {
+            target->as<ContainerComponent>()->forAll([&](Component* component) {
+                if (component->is<Shape>())
+                {
+                    HitShape* hitShape;
+                    auto itr = hitShapeLookup.find(component);
+                    if (itr == hitShapeLookup.end())
+                    {
+                        component->as<Shape>()->addFlags(PathFlags::neverDeferUpdate);
+                        component->as<Shape>()->addDirt(ComponentDirt::Path, true);
+                        auto hs = rivestd::make_unique<HitShape>(component, this);
+                        hitShapeLookup[component] = hitShape = hs.get();
+                        m_hitComponents.push_back(std::move(hs));
+                    }
+                    else
+                    {
+                        hitShape = itr->second;
+                    }
+                    hitShape->addListener(listenerGroup.get());
+                }
+                return true;
+            });
+        }
+        m_listenerGroups.push_back(std::move(listenerGroup));
+    }
+
+    for (auto nestedArtboard : instance->nestedArtboards())
+    {
+        if (nestedArtboard->hasNestedStateMachines())
+        {
+            auto hn =
+                rivestd::make_unique<HitNestedArtboard>(nestedArtboard->as<Component>(), this);
+            m_hitComponents.push_back(std::move(hn));
+        }
+        for (auto animation : nestedArtboard->nestedAnimations())
+        {
+            if (animation->is<NestedStateMachine>())
+            {
+                auto notifier = animation->as<NestedStateMachine>()->stateMachineInstance();
+                notifier->setNestedArtboard(nestedArtboard);
+                notifier->addNestedEventListener(this);
+            }
+            else if (animation->is<NestedLinearAnimation>())
+            {
+                auto notifier = animation->as<NestedLinearAnimation>()->animationInstance();
+                notifier->setNestedArtboard(nestedArtboard);
+                notifier->addNestedEventListener(this);
+            }
+        }
+    }
+    sortHitComponents();
+}
+
+StateMachineInstance::~StateMachineInstance()
+{
+    for (auto inst : m_inputInstances)
+    {
+        delete inst;
+    }
+    delete[] m_layers;
+}
+
+void StateMachineInstance::sortHitComponents()
+{
+    Drawable* last = m_artboardInstance->firstDrawable();
+    if (last)
+    {
+        // walk to the end, so we can visit in reverse-order
+        while (last->prev)
+        {
+            last = last->prev;
+        }
+    }
+    auto hitShapesCount = m_hitComponents.size();
+    auto currentSortedIndex = 0;
+    for (auto drawable = last; drawable; drawable = drawable->next)
+    {
+        for (size_t i = currentSortedIndex; i < hitShapesCount; i++)
+        {
+            if (m_hitComponents[i]->component() == drawable)
+            {
+                if (currentSortedIndex != i)
+                {
+                    std::iter_swap(m_hitComponents.begin() + currentSortedIndex,
+                                   m_hitComponents.begin() + i);
+                }
+                currentSortedIndex++;
+                break;
+            }
+        }
+        if (currentSortedIndex == hitShapesCount)
+        {
+            break;
+        }
+    }
+}
+
+void StateMachineInstance::updateDataBinds()
+{
+    for (auto dataBind : m_dataBinds)
+    {
+        auto d = dataBind->dirt();
+        if (d != ComponentDirt::None)
+        {
+            dataBind->dirt(ComponentDirt::None);
+            dataBind->update(d);
+        }
+    }
+}
+
+bool StateMachineInstance::advance(float seconds)
+{
+    updateDataBinds();
+    if (m_artboardInstance->hasChangedDrawOrderInLastUpdate())
+    {
+        sortHitComponents();
+    }
+    this->notifyEventListeners(m_reportedEvents, nullptr);
+    m_reportedEvents.clear();
+    m_needsAdvance = false;
+    for (size_t i = 0; i < m_layerCount; i++)
+    {
+        if (m_layers[i].advance(seconds))
+        {
+            m_needsAdvance = true;
+        }
+    }
+
+    for (auto inst : m_inputInstances)
+    {
+        inst->advanced();
+    }
+
+    return m_needsAdvance;
+}
+
+bool StateMachineInstance::advanceAndApply(float seconds)
+{
+    bool keepGoing = this->advance(seconds);
+    keepGoing = m_artboardInstance->advance(seconds) || keepGoing;
+    return keepGoing;
+}
+
+void StateMachineInstance::markNeedsAdvance() { m_needsAdvance = true; }
+bool StateMachineInstance::needsAdvance() const { return m_needsAdvance; }
+
+std::string StateMachineInstance::name() const { return m_machine->name(); }
+
+SMIInput* StateMachineInstance::input(size_t index) const
+{
+    if (index < m_inputInstances.size())
+    {
+        return m_inputInstances[index];
+    }
+    return nullptr;
+}
+
+template <typename SMType, typename InstType>
+InstType* StateMachineInstance::getNamedInput(const std::string& name) const
+{
+    for (const auto inst : m_inputInstances)
+    {
+        auto input = inst->input();
+        if (input->is<SMType>() && input->name() == name)
+        {
+            return static_cast<InstType*>(inst);
+        }
+    }
+    return nullptr;
+}
+
+SMIBool* StateMachineInstance::getBool(const std::string& name) const
+{
+    return getNamedInput<StateMachineBool, SMIBool>(name);
+}
+SMINumber* StateMachineInstance::getNumber(const std::string& name) const
+{
+    return getNamedInput<StateMachineNumber, SMINumber>(name);
+}
+SMITrigger* StateMachineInstance::getTrigger(const std::string& name) const
+{
+    return getNamedInput<StateMachineTrigger, SMITrigger>(name);
+}
+
+void StateMachineInstance::dataContextFromInstance(ViewModelInstance* viewModelInstance)
+{
+    dataContext(new DataContext(viewModelInstance));
+}
+
+void StateMachineInstance::dataContext(DataContext* dataContext)
+{
+    m_DataContext = dataContext;
+    for (auto dataBind : m_dataBinds)
+    {
+        if (dataBind->is<DataBindContext>())
+        {
+            dataBind->as<DataBindContext>()->bindFromContext(dataContext);
+        }
+    }
+}
+
+size_t StateMachineInstance::stateChangedCount() const
+{
+    size_t count = 0;
+    for (size_t i = 0; i < m_layerCount; i++)
+    {
+        if (m_layers[i].stateChangedOnAdvance())
+        {
+            count++;
+        }
+    }
+    return count;
+}
+
+const LayerState* StateMachineInstance::stateChangedByIndex(size_t index) const
+{
+    size_t count = 0;
+    for (size_t i = 0; i < m_layerCount; i++)
+    {
+        if (m_layers[i].stateChangedOnAdvance())
+        {
+            if (count == index)
+            {
+                return m_layers[i].currentState();
+            }
+            count++;
+        }
+    }
+    return nullptr;
+}
+
+size_t StateMachineInstance::currentAnimationCount() const
+{
+    size_t count = 0;
+    for (size_t i = 0; i < m_layerCount; i++)
+    {
+        if (m_layers[i].currentAnimation() != nullptr)
+        {
+            count++;
+        }
+    }
+    return count;
+}
+
+const LinearAnimationInstance* StateMachineInstance::currentAnimationByIndex(size_t index) const
+{
+    size_t count = 0;
+    for (size_t i = 0; i < m_layerCount; i++)
+    {
+        if (m_layers[i].currentAnimation() != nullptr)
+        {
+            if (count == index)
+            {
+                return m_layers[i].currentAnimation();
+            }
+            count++;
+        }
+    }
+    return nullptr;
+}
+
+void StateMachineInstance::reportEvent(Event* event, float delaySeconds)
+{
+    m_reportedEvents.push_back(EventReport(event, delaySeconds));
+}
+
+std::size_t StateMachineInstance::reportedEventCount() const { return m_reportedEvents.size(); }
+
+const EventReport StateMachineInstance::reportedEventAt(std::size_t index) const
+{
+    if (index >= m_reportedEvents.size())
+    {
+        return EventReport(nullptr, 0.0f);
+    }
+    return m_reportedEvents[index];
+}
+
+void StateMachineInstance::notify(const std::vector<EventReport>& events, NestedArtboard* context)
+{
+    notifyEventListeners(events, context);
+}
+
+void StateMachineInstance::notifyEventListeners(const std::vector<EventReport>& events,
+                                                NestedArtboard* source)
+{
+    if (events.size() > 0)
+    {
+        // We trigger the listeners in order
+        for (size_t i = 0; i < m_machine->listenerCount(); i++)
+        {
+            auto listener = m_machine->listener(i);
+            auto target = artboard()->resolve(listener->targetId());
+            if (listener != nullptr && listener->listenerType() == ListenerType::event &&
+                (source == nullptr || source == target))
+            {
+                for (const auto event : events)
+                {
+                    auto sourceArtboard =
+                        source == nullptr ? artboard() : source->artboardInstance();
+
+                    // listener->eventId() can point to an id from an event in the context of this
+                    // artboard or the context of a nested artboard. Because those ids belong to
+                    // different contexts, they can have the same value. So when the eventId is
+                    // resolved within one context, but actually pointing to the other, it can
+                    // return the wrong event object. If, by chance, that event exists in the other
+                    // context, and is being reported, it will trigger the wrong set of actions.
+                    // This validation makes sure that a listener must be targetting the current
+                    // artboard to disambiguate between external and internal events.
+                    if (source == nullptr &&
+                        sourceArtboard->resolve(listener->targetId()) != artboard())
+                    {
+                        continue;
+                    }
+                    auto listenerEvent = sourceArtboard->resolve(listener->eventId());
+                    if (listenerEvent == event.event())
+                    {
+                        listener->performChanges(this, Vec2D(), Vec2D());
+                        break;
+                    }
+                }
+            }
+        }
+        // Bubble the event up to parent artboard state machines immediately
+        for (auto listener : nestedEventListeners())
+        {
+            listener->notify(events, nestedArtboard());
+        }
+
+        for (auto report : events)
+        {
+            auto event = report.event();
+            if (event->is<AudioEvent>())
+            {
+                event->as<AudioEvent>()->play();
+            }
+        }
+    }
+}
+
+BindableProperty* StateMachineInstance::bindablePropertyInstance(
+    BindableProperty* bindableProperty) const
+{
+    auto bindablePropertyInstance = m_bindablePropertyInstances.find(bindableProperty);
+    if (bindablePropertyInstance == m_bindablePropertyInstances.end())
+    {
+        return nullptr;
+    }
+    return bindablePropertyInstance->second;
+}
+
+DataBind* StateMachineInstance::bindableDataBind(BindableProperty* bindableProperty)
+{
+    auto dataBind = m_bindableDataBinds.find(bindableProperty);
+    if (dataBind == m_bindableDataBinds.end())
+    {
+        return nullptr;
+    }
+    return dataBind->second;
+}
diff --git a/src/animation/state_machine_layer.cpp b/src/animation/state_machine_layer.cpp
new file mode 100644
index 0000000..3aaec68
--- /dev/null
+++ b/src/animation/state_machine_layer.cpp
@@ -0,0 +1,76 @@
+#include "rive/animation/state_machine_layer.hpp"
+#include "rive/importers/import_stack.hpp"
+#include "rive/importers/state_machine_importer.hpp"
+#include "rive/generated/animation/state_machine_base.hpp"
+#include "rive/animation/any_state.hpp"
+#include "rive/animation/entry_state.hpp"
+#include "rive/animation/exit_state.hpp"
+
+using namespace rive;
+
+StateMachineLayer::~StateMachineLayer()
+{
+    for (auto state : m_States)
+    {
+        delete state;
+    }
+}
+
+StatusCode StateMachineLayer::onAddedDirty(CoreContext* context)
+{
+    StatusCode code;
+    for (auto state : m_States)
+    {
+        if ((code = state->onAddedDirty(context)) != StatusCode::Ok)
+        {
+            return code;
+        }
+        switch (state->coreType())
+        {
+            case AnyState::typeKey:
+                m_Any = state->as<AnyState>();
+                break;
+            case EntryState::typeKey:
+                m_Entry = state->as<EntryState>();
+                break;
+            case ExitState::typeKey:
+                m_Exit = state->as<ExitState>();
+                break;
+        }
+    }
+    if (m_Any == nullptr || m_Entry == nullptr || m_Exit == nullptr)
+    {
+        // The layer is corrupt, we must have all three of these states.
+        return StatusCode::InvalidObject;
+    }
+
+    return StatusCode::Ok;
+}
+
+StatusCode StateMachineLayer::onAddedClean(CoreContext* context)
+{
+    StatusCode code;
+    for (auto state : m_States)
+    {
+        if ((code = state->onAddedClean(context)) != StatusCode::Ok)
+        {
+            return code;
+        }
+    }
+
+    return StatusCode::Ok;
+}
+
+void StateMachineLayer::addState(LayerState* state) { m_States.push_back(state); }
+
+StatusCode StateMachineLayer::import(ImportStack& importStack)
+{
+    auto stateMachineImporter = importStack.latest<StateMachineImporter>(StateMachineBase::typeKey);
+    if (stateMachineImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    // WOW -- we're handing off ownership of this!
+    stateMachineImporter->addLayer(std::unique_ptr<StateMachineLayer>(this));
+    return Super::import(importStack);
+}
diff --git a/src/animation/state_machine_listener.cpp b/src/animation/state_machine_listener.cpp
new file mode 100644
index 0000000..7abf521
--- /dev/null
+++ b/src/animation/state_machine_listener.cpp
@@ -0,0 +1,49 @@
+#include "rive/animation/state_machine_listener.hpp"
+#include "rive/importers/import_stack.hpp"
+#include "rive/importers/state_machine_importer.hpp"
+#include "rive/generated/animation/state_machine_base.hpp"
+#include "rive/artboard.hpp"
+#include "rive/shapes/shape.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/animation/listener_input_change.hpp"
+
+using namespace rive;
+
+StateMachineListener::StateMachineListener() {}
+StateMachineListener::~StateMachineListener() {}
+
+void StateMachineListener::addAction(std::unique_ptr<ListenerAction> action)
+{
+    m_actions.push_back(std::move(action));
+}
+
+StatusCode StateMachineListener::import(ImportStack& importStack)
+{
+    auto stateMachineImporter = importStack.latest<StateMachineImporter>(StateMachineBase::typeKey);
+    if (stateMachineImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    // Handing off ownership of this!
+    stateMachineImporter->addListener(std::unique_ptr<StateMachineListener>(this));
+    return Super::import(importStack);
+}
+
+const ListenerAction* StateMachineListener::action(size_t index) const
+{
+    if (index < m_actions.size())
+    {
+        return m_actions[index].get();
+    }
+    return nullptr;
+}
+
+void StateMachineListener::performChanges(StateMachineInstance* stateMachineInstance,
+                                          Vec2D position,
+                                          Vec2D previousPosition) const
+{
+    for (auto& action : m_actions)
+    {
+        action->perform(stateMachineInstance, position, previousPosition);
+    }
+}
\ No newline at end of file
diff --git a/src/animation/state_transition.cpp b/src/animation/state_transition.cpp
new file mode 100644
index 0000000..f419c4b
--- /dev/null
+++ b/src/animation/state_transition.cpp
@@ -0,0 +1,215 @@
+#include "rive/animation/animation_state_instance.hpp"
+#include "rive/animation/animation_state.hpp"
+#include "rive/animation/cubic_interpolator.hpp"
+#include "rive/animation/layer_state.hpp"
+#include "rive/animation/linear_animation.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+#include "rive/animation/state_machine_trigger.hpp"
+#include "rive/animation/state_transition.hpp"
+#include "rive/animation/transition_condition.hpp"
+#include "rive/animation/transition_trigger_condition.hpp"
+#include "rive/animation/transition_input_condition.hpp"
+#include "rive/animation/transition_viewmodel_condition.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/animation/transition_property_viewmodel_comparator.hpp"
+#include "rive/importers/import_stack.hpp"
+#include "rive/importers/layer_state_importer.hpp"
+
+using namespace rive;
+
+StateTransition::~StateTransition()
+{
+    for (auto condition : m_Conditions)
+    {
+        delete condition;
+    }
+}
+
+StatusCode StateTransition::onAddedDirty(CoreContext* context)
+{
+    StatusCode code;
+
+    if (interpolatorId() != -1)
+    {
+        auto coreObject = context->resolve(interpolatorId());
+        if (coreObject == nullptr || !coreObject->is<CubicInterpolator>())
+        {
+            return StatusCode::MissingObject;
+        }
+        m_Interpolator = coreObject->as<CubicInterpolator>();
+    }
+
+    for (auto condition : m_Conditions)
+    {
+        if ((code = condition->onAddedDirty(context)) != StatusCode::Ok)
+        {
+            return code;
+        }
+    }
+    return StatusCode::Ok;
+}
+
+StatusCode StateTransition::onAddedClean(CoreContext* context)
+{
+    StatusCode code;
+    for (auto condition : m_Conditions)
+    {
+        if ((code = condition->onAddedClean(context)) != StatusCode::Ok)
+        {
+            return code;
+        }
+    }
+    return StatusCode::Ok;
+}
+
+StatusCode StateTransition::import(ImportStack& importStack)
+{
+    auto stateImporter = importStack.latest<LayerStateImporter>(LayerState::typeKey);
+    if (stateImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    stateImporter->addTransition(this);
+    return Super::import(importStack);
+}
+
+void StateTransition::addCondition(TransitionCondition* condition)
+{
+    m_Conditions.push_back(condition);
+}
+
+float StateTransition::mixTime(const LayerState* stateFrom) const
+{
+    if (duration() == 0)
+    {
+        return 0;
+    }
+    if ((transitionFlags() & StateTransitionFlags::DurationIsPercentage) ==
+        StateTransitionFlags::DurationIsPercentage)
+    {
+        float animationDuration = 0.0f;
+        if (stateFrom->is<AnimationState>())
+        {
+            auto animation = stateFrom->as<AnimationState>()->animation();
+            if (animation != nullptr)
+            {
+                animationDuration = animation->durationSeconds();
+            }
+        }
+        return duration() / 100.0f * animationDuration;
+    }
+    else
+    {
+        return duration() / 1000.0f;
+    }
+}
+
+float StateTransition::exitTimeSeconds(const LayerState* stateFrom, bool absolute) const
+{
+    if ((transitionFlags() & StateTransitionFlags::ExitTimeIsPercentage) ==
+        StateTransitionFlags::ExitTimeIsPercentage)
+    {
+        float animationDuration = 0.0f;
+        float start = 0.0f;
+
+        auto exitAnimation = exitTimeAnimation(stateFrom);
+        if (exitAnimation != nullptr)
+        {
+            // TODO: needs a looking for speed
+            start = absolute ? exitAnimation->startSeconds() : 0.0f;
+            animationDuration = exitAnimation->durationSeconds();
+        }
+
+        return start + exitTime() / 100.0f * animationDuration;
+    }
+    return exitTime() / 1000.0f;
+}
+
+const LinearAnimationInstance* StateTransition::exitTimeAnimationInstance(
+    const StateInstance* from) const
+{
+    return from != nullptr && from->state()->is<AnimationState>()
+               ? static_cast<const AnimationStateInstance*>(from)->animationInstance()
+               : nullptr;
+}
+
+const LinearAnimation* StateTransition::exitTimeAnimation(const LayerState* from) const
+{
+    return from != nullptr && from->is<AnimationState>() ? from->as<AnimationState>()->animation()
+                                                         : nullptr;
+}
+
+AllowTransition StateTransition::allowed(StateInstance* stateFrom,
+                                         StateMachineInstance* stateMachineInstance,
+                                         bool ignoreTriggers) const
+{
+    if (isDisabled())
+    {
+        return AllowTransition::no;
+    }
+
+    for (auto condition : m_Conditions)
+    {
+        if ((ignoreTriggers && condition->is<TransitionTriggerCondition>()) ||
+            !condition->evaluate(stateMachineInstance))
+        {
+            return AllowTransition::no;
+        }
+    }
+
+    if (enableExitTime())
+    {
+        auto exitAnimation = exitTimeAnimationInstance(stateFrom);
+        if (exitAnimation != nullptr)
+        {
+            // Exit time is specified in a value less than a single loop, so we
+            // want to allow exiting regardless of which loop we're on. To do
+            // that we bring the exit time up to the loop our lastTime is at.
+            auto lastTime = exitAnimation->lastTotalTime();
+            auto time = exitAnimation->totalTime();
+            auto exitTime = exitTimeSeconds(stateFrom->state());
+            auto animationFrom = exitAnimation->animation();
+            auto duration = animationFrom->durationSeconds();
+
+            // TODO: there are some considerations to have when exit time is
+            // combined with another condition (like trigger)
+            //   - not sure how to get this to make sense with pingPing
+            //   animations
+            //   - also if exit time is, say 50% on a loop, this will be happy
+            //   to fire
+            //       - when time is anywhere in 50%-100%, 150%-200%. as opposed
+            //       to just at 50%
+            //       .... makes you wonder if we need some kind of exit
+            //       after/exit before time
+            //       .... but i suspect that will introduce some more issues?
+
+            // There's only one iteration in oneShot,
+            if (exitTime <= duration && animationFrom->loop() != Loop::oneShot)
+            {
+                // Get exit time relative to the loop lastTime was in.
+                exitTime += std::floor(lastTime / duration) * duration;
+            }
+
+            // TODO: needs a looking for speed
+            if (time < exitTime)
+            {
+                return AllowTransition::waitingForExit;
+            }
+        }
+    }
+    return AllowTransition::yes;
+}
+
+bool StateTransition::applyExitCondition(StateInstance* from) const
+{
+    // Hold exit time when the user has set to pauseOnExit on this condition
+    // (only valid when exiting from an Animation).
+    bool useExitTime = enableExitTime() && (from != nullptr && from->state()->is<AnimationState>());
+    if (pauseOnExit() && useExitTime)
+    {
+        static_cast<AnimationStateInstance*>(from)->animationInstance()->time(
+            exitTimeSeconds(from->state(), true));
+        return true;
+    }
+    return useExitTime;
+}
\ No newline at end of file
diff --git a/src/animation/system_state_instance.cpp b/src/animation/system_state_instance.cpp
new file mode 100644
index 0000000..4eb489a
--- /dev/null
+++ b/src/animation/system_state_instance.cpp
@@ -0,0 +1,11 @@
+#include "rive/animation/system_state_instance.hpp"
+using namespace rive;
+
+SystemStateInstance::SystemStateInstance(const LayerState* layerState, ArtboardInstance* instance) :
+    StateInstance(layerState)
+{}
+
+void SystemStateInstance::advance(float seconds, StateMachineInstance* stateMachineInstance) {}
+void SystemStateInstance::apply(ArtboardInstance* artboard, float mix) {}
+
+bool SystemStateInstance::keepGoing() const { return false; }
\ No newline at end of file
diff --git a/src/animation/transition_bool_condition.cpp b/src/animation/transition_bool_condition.cpp
new file mode 100644
index 0000000..f50a84e
--- /dev/null
+++ b/src/animation/transition_bool_condition.cpp
@@ -0,0 +1,27 @@
+#include "rive/animation/transition_bool_condition.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+#include "rive/animation/state_machine_bool.hpp"
+#include "rive/animation/transition_condition_op.hpp"
+
+using namespace rive;
+
+bool TransitionBoolCondition::validateInputType(const StateMachineInput* input) const
+{
+    // A null input is valid as the StateMachine can attempt to limp along if we
+    // introduce new input types that old conditions are expected to handle in
+    // newer runtimes. The older runtimes will just evaluate them to true.
+    return input == nullptr || input->is<StateMachineBool>();
+}
+
+bool TransitionBoolCondition::evaluate(const StateMachineInstance* stateMachineInstance) const
+{
+    auto inputInstance = stateMachineInstance->input(inputId());
+    if (inputInstance == nullptr)
+    {
+        return true;
+    }
+    auto boolInput = static_cast<const SMIBool*>(inputInstance);
+
+    return (boolInput->value() && op() == TransitionConditionOp::equal) ||
+           (!boolInput->value() && op() == TransitionConditionOp::notEqual);
+}
diff --git a/src/animation/transition_comparator.cpp b/src/animation/transition_comparator.cpp
new file mode 100644
index 0000000..b14254a
--- /dev/null
+++ b/src/animation/transition_comparator.cpp
@@ -0,0 +1,101 @@
+#include "rive/animation/transition_comparator.hpp"
+#include "rive/animation/transition_viewmodel_condition.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/importers/transition_viewmodel_condition_importer.hpp"
+
+using namespace rive;
+
+StatusCode TransitionComparator::import(ImportStack& importStack)
+{
+    auto transitionViewModelConditionImporter =
+        importStack.latest<TransitionViewModelConditionImporter>(
+            TransitionViewModelCondition::typeKey);
+    if (transitionViewModelConditionImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    transitionViewModelConditionImporter->setComparator(this);
+    return Super::import(importStack);
+}
+
+bool TransitionComparator::compareNumbers(float left, float right, TransitionConditionOp op)
+{
+    switch (op)
+    {
+        case TransitionConditionOp::equal:
+            return left == right;
+        case TransitionConditionOp::notEqual:
+            return left != right;
+        case TransitionConditionOp::lessThanOrEqual:
+            return left <= right;
+        case TransitionConditionOp::lessThan:
+            return left < right;
+        case TransitionConditionOp::greaterThanOrEqual:
+            return left >= right;
+        case TransitionConditionOp::greaterThan:
+            return left > right;
+        default:
+            return false;
+    }
+}
+
+bool TransitionComparator::compareStrings(std::string left,
+                                          std::string right,
+                                          TransitionConditionOp op)
+{
+    switch (op)
+    {
+        case TransitionConditionOp::equal:
+            return left == right;
+        case TransitionConditionOp::notEqual:
+            return left != right;
+        default:
+            return false;
+    }
+}
+
+bool TransitionComparator::compareBooleans(bool left, bool right, TransitionConditionOp op)
+{
+    switch (op)
+    {
+        case TransitionConditionOp::equal:
+            return left == right;
+        case TransitionConditionOp::notEqual:
+            return left != right;
+        default:
+            return false;
+    }
+}
+
+bool TransitionComparator::compareEnums(uint16_t left, uint16_t right, TransitionConditionOp op)
+{
+    switch (op)
+    {
+        case TransitionConditionOp::equal:
+            return left == right;
+        case TransitionConditionOp::notEqual:
+            return left != right;
+        default:
+            return false;
+    }
+}
+
+bool TransitionComparator::compareColors(int left, int right, TransitionConditionOp op)
+{
+    switch (op)
+    {
+        case TransitionConditionOp::equal:
+            return left == right;
+        case TransitionConditionOp::notEqual:
+            return left != right;
+        default:
+            return false;
+    }
+}
+
+bool TransitionComparator::compare(TransitionComparator* comparand,
+                                   TransitionConditionOp operation,
+                                   const StateMachineInstance* stateMachineInstance)
+{
+    return false;
+}
\ No newline at end of file
diff --git a/src/animation/transition_condition.cpp b/src/animation/transition_condition.cpp
new file mode 100644
index 0000000..9486516
--- /dev/null
+++ b/src/animation/transition_condition.cpp
@@ -0,0 +1,20 @@
+#include "rive/animation/transition_condition.hpp"
+#include "rive/animation/state_transition.hpp"
+#include "rive/importers/state_transition_importer.hpp"
+
+using namespace rive;
+
+StatusCode TransitionCondition::onAddedDirty(CoreContext* context) { return StatusCode::Ok; }
+
+StatusCode TransitionCondition::onAddedClean(CoreContext* context) { return StatusCode::Ok; }
+
+StatusCode TransitionCondition::import(ImportStack& importStack)
+{
+    auto transitionImporter = importStack.latest<StateTransitionImporter>(StateTransition::typeKey);
+    if (transitionImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    transitionImporter->addCondition(this);
+    return Super::import(importStack);
+}
diff --git a/src/animation/transition_input_condition.cpp b/src/animation/transition_input_condition.cpp
new file mode 100644
index 0000000..52fd89e
--- /dev/null
+++ b/src/animation/transition_input_condition.cpp
@@ -0,0 +1,26 @@
+#include "rive/animation/transition_input_condition.hpp"
+#include "rive/importers/state_machine_importer.hpp"
+#include "rive/animation/state_machine.hpp"
+
+using namespace rive;
+
+StatusCode TransitionInputCondition::import(ImportStack& importStack)
+{
+    auto stateMachineImporter = importStack.latest<StateMachineImporter>(StateMachine::typeKey);
+    if (stateMachineImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+
+    // Make sure the inputId doesn't overflow the input buffer.
+    if ((size_t)inputId() >= stateMachineImporter->stateMachine()->inputCount())
+    {
+        return StatusCode::InvalidObject;
+    }
+    if (!validateInputType(stateMachineImporter->stateMachine()->input((size_t)inputId())))
+    {
+        return StatusCode::InvalidObject;
+    }
+
+    return Super::import(importStack);
+}
\ No newline at end of file
diff --git a/src/animation/transition_number_condition.cpp b/src/animation/transition_number_condition.cpp
new file mode 100644
index 0000000..da820e3
--- /dev/null
+++ b/src/animation/transition_number_condition.cpp
@@ -0,0 +1,41 @@
+#include "rive/animation/transition_number_condition.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+#include "rive/animation/state_machine_number.hpp"
+#include "rive/animation/transition_condition_op.hpp"
+
+using namespace rive;
+
+bool TransitionNumberCondition::validateInputType(const StateMachineInput* input) const
+{
+    // A null input is valid as the StateMachine can attempt to limp along if we
+    // introduce new input types that old conditions are expected to handle in
+    // newer runtimes. The older runtimes will just evaluate them to true.
+    return input == nullptr || input->is<StateMachineNumber>();
+}
+
+bool TransitionNumberCondition::evaluate(const StateMachineInstance* stateMachineInstance) const
+{
+    auto inputInstance = stateMachineInstance->input(inputId());
+    if (inputInstance == nullptr)
+    {
+        return true;
+    }
+    auto numberInput = static_cast<const SMINumber*>(inputInstance);
+
+    switch (op())
+    {
+        case TransitionConditionOp::equal:
+            return numberInput->value() == value();
+        case TransitionConditionOp::notEqual:
+            return numberInput->value() != value();
+        case TransitionConditionOp::lessThanOrEqual:
+            return numberInput->value() <= value();
+        case TransitionConditionOp::lessThan:
+            return numberInput->value() < value();
+        case TransitionConditionOp::greaterThanOrEqual:
+            return numberInput->value() >= value();
+        case TransitionConditionOp::greaterThan:
+            return numberInput->value() > value();
+    }
+    return false;
+}
diff --git a/src/animation/transition_property_artboard_comparator.cpp b/src/animation/transition_property_artboard_comparator.cpp
new file mode 100644
index 0000000..a769c7e
--- /dev/null
+++ b/src/animation/transition_property_artboard_comparator.cpp
@@ -0,0 +1,54 @@
+#include "rive/animation/transition_property_artboard_comparator.hpp"
+#include "rive/animation/transition_property_viewmodel_comparator.hpp"
+#include "rive/animation/transition_value_number_comparator.hpp"
+#include "rive/animation/artboard_property.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/data_bind/bindable_property_number.hpp"
+
+using namespace rive;
+
+float TransitionPropertyArtboardComparator::propertyValue(
+    const StateMachineInstance* stateMachineInstance)
+{
+    auto artboard = stateMachineInstance->artboard();
+    if (artboard != nullptr)
+    {
+
+        auto property = static_cast<ArtboardProperty>(propertyType());
+        switch (property)
+        {
+            case ArtboardProperty::width:
+                return artboard->layoutWidth();
+                break;
+            case ArtboardProperty::height:
+                return artboard->layoutHeight();
+                break;
+            case ArtboardProperty::ratio:
+                return artboard->layoutWidth() / artboard->layoutHeight();
+                break;
+
+            default:
+                break;
+        }
+    }
+    return 0;
+}
+
+bool TransitionPropertyArtboardComparator::compare(TransitionComparator* comparand,
+                                                   TransitionConditionOp operation,
+                                                   const StateMachineInstance* stateMachineInstance)
+{
+    auto value = propertyValue(stateMachineInstance);
+    if (comparand->is<TransitionPropertyViewModelComparator>())
+    {
+        auto rightValue = comparand->as<TransitionPropertyViewModelComparator>()
+                              ->value<BindablePropertyNumber, float>(stateMachineInstance);
+        return compareNumbers(value, rightValue, operation);
+    }
+    else if (comparand->is<TransitionValueNumberComparator>())
+    {
+        auto rightValue = comparand->as<TransitionValueNumberComparator>()->value();
+        return compareNumbers(value, rightValue, operation);
+    }
+    return false;
+}
\ No newline at end of file
diff --git a/src/animation/transition_property_comparator.cpp b/src/animation/transition_property_comparator.cpp
new file mode 100644
index 0000000..05d25ae
--- /dev/null
+++ b/src/animation/transition_property_comparator.cpp
@@ -0,0 +1,3 @@
+#include "rive/animation/transition_property_comparator.hpp"
+
+using namespace rive;
\ No newline at end of file
diff --git a/src/animation/transition_property_viewmodel_comparator.cpp b/src/animation/transition_property_viewmodel_comparator.cpp
new file mode 100644
index 0000000..80f92ec
--- /dev/null
+++ b/src/animation/transition_property_viewmodel_comparator.cpp
@@ -0,0 +1,127 @@
+#include "rive/animation/transition_property_viewmodel_comparator.hpp"
+#include "rive/animation/transition_value_number_comparator.hpp"
+#include "rive/animation/transition_value_string_comparator.hpp"
+#include "rive/animation/transition_value_color_comparator.hpp"
+#include "rive/animation/transition_value_boolean_comparator.hpp"
+#include "rive/animation/transition_value_enum_comparator.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/importers/bindable_property_importer.hpp"
+#include "rive/data_bind/bindable_property_number.hpp"
+#include "rive/data_bind/bindable_property_string.hpp"
+#include "rive/data_bind/bindable_property_color.hpp"
+#include "rive/data_bind/bindable_property_enum.hpp"
+#include "rive/data_bind/bindable_property_boolean.hpp"
+
+using namespace rive;
+
+StatusCode TransitionPropertyViewModelComparator::import(ImportStack& importStack)
+{
+    auto bindablePropertyImporter =
+        importStack.latest<BindablePropertyImporter>(BindablePropertyBase::typeKey);
+    if (bindablePropertyImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    m_bindableProperty = bindablePropertyImporter->bindableProperty();
+
+    return Super::import(importStack);
+}
+
+bool TransitionPropertyViewModelComparator::compare(
+    TransitionComparator* comparand,
+    TransitionConditionOp operation,
+    const StateMachineInstance* stateMachineInstance)
+{
+    switch (m_bindableProperty->coreType())
+    {
+        case BindablePropertyNumber::typeKey:
+            if (comparand->is<TransitionPropertyViewModelComparator>())
+            {
+                auto rightValue = comparand->as<TransitionPropertyViewModelComparator>()
+                                      ->value<BindablePropertyNumber, float>(stateMachineInstance);
+                return compareNumbers(value<BindablePropertyNumber, float>(stateMachineInstance),
+                                      rightValue,
+                                      operation);
+            }
+            else if (comparand->is<TransitionValueNumberComparator>())
+            {
+                auto rightValue = comparand->as<TransitionValueNumberComparator>()->value();
+                return compareNumbers(value<BindablePropertyNumber, float>(stateMachineInstance),
+                                      rightValue,
+                                      operation);
+            }
+            break;
+        case BindablePropertyString::typeKey:
+            if (comparand->is<TransitionPropertyViewModelComparator>())
+            {
+                auto rightValue =
+                    comparand->as<TransitionPropertyViewModelComparator>()
+                        ->value<BindablePropertyString, std::string>(stateMachineInstance);
+                return compareStrings(
+                    value<BindablePropertyString, std::string>(stateMachineInstance),
+                    rightValue,
+                    operation);
+            }
+            else if (comparand->is<TransitionValueStringComparator>())
+            {
+                auto rightValue = comparand->as<TransitionValueStringComparator>()->value();
+                return compareStrings(
+                    value<BindablePropertyString, std::string>(stateMachineInstance),
+                    rightValue,
+                    operation);
+            }
+            break;
+        case BindablePropertyColor::typeKey:
+            if (comparand->is<TransitionPropertyViewModelComparator>())
+            {
+                auto rightValue = comparand->as<TransitionPropertyViewModelComparator>()
+                                      ->value<BindablePropertyColor, int>(stateMachineInstance);
+                return compareColors(value<BindablePropertyColor, int>(stateMachineInstance),
+                                     rightValue,
+                                     operation);
+            }
+            else if (comparand->is<TransitionValueColorComparator>())
+            {
+                auto rightValue = comparand->as<TransitionValueColorComparator>()->value();
+                return compareColors(value<BindablePropertyColor, int>(stateMachineInstance),
+                                     rightValue,
+                                     operation);
+            }
+            break;
+        case BindablePropertyBoolean::typeKey:
+            if (comparand->is<TransitionPropertyViewModelComparator>())
+            {
+                auto rightValue = comparand->as<TransitionPropertyViewModelComparator>()
+                                      ->value<BindablePropertyBoolean, bool>(stateMachineInstance);
+                return compareBooleans(value<BindablePropertyBoolean, bool>(stateMachineInstance),
+                                       rightValue,
+                                       operation);
+            }
+            else if (comparand->is<TransitionValueBooleanComparator>())
+            {
+                auto rightValue = comparand->as<TransitionValueBooleanComparator>()->value();
+                return compareBooleans(value<BindablePropertyBoolean, bool>(stateMachineInstance),
+                                       rightValue,
+                                       operation);
+            }
+            break;
+        case BindablePropertyEnum::typeKey:
+            if (comparand->is<TransitionPropertyViewModelComparator>())
+            {
+                auto rightValue = comparand->as<TransitionPropertyViewModelComparator>()
+                                      ->value<BindablePropertyEnum, uint16_t>(stateMachineInstance);
+                return compareEnums(value<BindablePropertyEnum, uint16_t>(stateMachineInstance),
+                                    rightValue,
+                                    operation);
+            }
+            else if (comparand->is<TransitionValueEnumComparator>())
+            {
+                auto rightValue = comparand->as<TransitionValueEnumComparator>()->value();
+                return compareEnums(value<BindablePropertyEnum, uint16_t>(stateMachineInstance),
+                                    rightValue,
+                                    operation);
+            }
+            break;
+    }
+    return false;
+}
\ No newline at end of file
diff --git a/src/animation/transition_trigger_condition.cpp b/src/animation/transition_trigger_condition.cpp
new file mode 100644
index 0000000..15be1a6
--- /dev/null
+++ b/src/animation/transition_trigger_condition.cpp
@@ -0,0 +1,30 @@
+#include "rive/animation/transition_trigger_condition.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+#include "rive/animation/state_machine_trigger.hpp"
+#include "rive/animation/transition_condition_op.hpp"
+
+using namespace rive;
+
+bool TransitionTriggerCondition::validateInputType(const StateMachineInput* input) const
+{
+    // A null input is valid as the StateMachine can attempt to limp along if we
+    // introduce new input types that old conditions are expected to handle in
+    // newer runtimes. The older runtimes will just evaluate them to true.
+    return input == nullptr || input->is<StateMachineTrigger>();
+}
+
+bool TransitionTriggerCondition::evaluate(const StateMachineInstance* stateMachineInstance) const
+{
+    auto inputInstance = stateMachineInstance->input(inputId());
+    if (inputInstance == nullptr)
+    {
+        return true;
+    }
+    auto triggerInput = static_cast<const SMITrigger*>(inputInstance);
+
+    if (triggerInput->m_fired)
+    {
+        return true;
+    }
+    return false;
+}
diff --git a/src/animation/transition_value_boolean_comparator.cpp b/src/animation/transition_value_boolean_comparator.cpp
new file mode 100644
index 0000000..7477828
--- /dev/null
+++ b/src/animation/transition_value_boolean_comparator.cpp
@@ -0,0 +1,16 @@
+#include "rive/animation/transition_value_boolean_comparator.hpp"
+
+using namespace rive;
+
+bool TransitionValueBooleanComparator::compare(TransitionComparator* comparand,
+                                               TransitionConditionOp operation,
+                                               const StateMachineInstance* stateMachineInstance)
+{
+    if (comparand->is<TransitionValueBooleanComparator>())
+    {
+        return compareBooleans(value(),
+                               comparand->as<TransitionValueBooleanComparator>()->value(),
+                               operation);
+    }
+    return false;
+}
\ No newline at end of file
diff --git a/src/animation/transition_value_color_comparator.cpp b/src/animation/transition_value_color_comparator.cpp
new file mode 100644
index 0000000..9e29e26
--- /dev/null
+++ b/src/animation/transition_value_color_comparator.cpp
@@ -0,0 +1,16 @@
+#include "rive/animation/transition_value_color_comparator.hpp"
+
+using namespace rive;
+
+bool TransitionValueColorComparator::compare(TransitionComparator* comparand,
+                                             TransitionConditionOp operation,
+                                             const StateMachineInstance* stateMachineInstance)
+{
+    if (comparand->is<TransitionValueColorComparator>())
+    {
+        return compareColors(value(),
+                             comparand->as<TransitionValueColorComparator>()->value(),
+                             operation);
+    }
+    return false;
+}
\ No newline at end of file
diff --git a/src/animation/transition_value_enum_comparator.cpp b/src/animation/transition_value_enum_comparator.cpp
new file mode 100644
index 0000000..8742e37
--- /dev/null
+++ b/src/animation/transition_value_enum_comparator.cpp
@@ -0,0 +1,4 @@
+#include "rive/animation/transition_value_enum_comparator.hpp"
+#include "rive/viewmodel/viewmodel_instance_enum.hpp"
+
+using namespace rive;
\ No newline at end of file
diff --git a/src/animation/transition_value_number_comparator.cpp b/src/animation/transition_value_number_comparator.cpp
new file mode 100644
index 0000000..5c88856
--- /dev/null
+++ b/src/animation/transition_value_number_comparator.cpp
@@ -0,0 +1,16 @@
+#include "rive/animation/transition_value_number_comparator.hpp"
+
+using namespace rive;
+
+bool TransitionValueNumberComparator::compare(TransitionComparator* comparand,
+                                              TransitionConditionOp operation,
+                                              const StateMachineInstance* stateMachineInstance)
+{
+    if (comparand->is<TransitionValueNumberComparator>())
+    {
+        return compareNumbers(value(),
+                              comparand->as<TransitionValueNumberComparator>()->value(),
+                              operation);
+    }
+    return false;
+}
\ No newline at end of file
diff --git a/src/animation/transition_value_string_comparator.cpp b/src/animation/transition_value_string_comparator.cpp
new file mode 100644
index 0000000..fe6ac06
--- /dev/null
+++ b/src/animation/transition_value_string_comparator.cpp
@@ -0,0 +1,16 @@
+#include "rive/animation/transition_value_string_comparator.hpp"
+
+using namespace rive;
+
+bool TransitionValueStringComparator::compare(TransitionComparator* comparand,
+                                              TransitionConditionOp operation,
+                                              const StateMachineInstance* stateMachineInstance)
+{
+    if (comparand->is<TransitionValueStringComparator>())
+    {
+        return compareStrings(value(),
+                              comparand->as<TransitionValueStringComparator>()->value(),
+                              operation);
+    }
+    return false;
+}
\ No newline at end of file
diff --git a/src/animation/transition_viewmodel_condition.cpp b/src/animation/transition_viewmodel_condition.cpp
new file mode 100644
index 0000000..5c6ebbf
--- /dev/null
+++ b/src/animation/transition_viewmodel_condition.cpp
@@ -0,0 +1,18 @@
+#include "rive/animation/transition_viewmodel_condition.hpp"
+#include "rive/animation/state_transition.hpp"
+#include "rive/importers/state_transition_importer.hpp"
+#include "rive/importers/state_machine_importer.hpp"
+#include "rive/animation/state_machine.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/component_dirt.hpp"
+
+using namespace rive;
+
+bool TransitionViewModelCondition::evaluate(const StateMachineInstance* stateMachineInstance) const
+{
+    if (leftComparator() != nullptr && rightComparator() != nullptr)
+    {
+        return leftComparator()->compare(rightComparator(), op(), stateMachineInstance);
+    }
+    return false;
+}
\ No newline at end of file
diff --git a/src/artboard.cpp b/src/artboard.cpp
new file mode 100644
index 0000000..b153350
--- /dev/null
+++ b/src/artboard.cpp
@@ -0,0 +1,1397 @@
+#include "rive/artboard.hpp"
+#include "rive/backboard.hpp"
+#include "rive/animation/linear_animation_instance.hpp"
+#include "rive/dependency_sorter.hpp"
+#include "rive/data_bind/data_bind.hpp"
+#include "rive/data_bind/data_bind_context.hpp"
+#include "rive/draw_rules.hpp"
+#include "rive/draw_target.hpp"
+#include "rive/audio_event.hpp"
+#include "rive/draw_target_placement.hpp"
+#include "rive/drawable.hpp"
+#include "rive/animation/keyed_object.hpp"
+#include "rive/factory.hpp"
+#include "rive/renderer.hpp"
+#include "rive/shapes/paint/shape_paint.hpp"
+#include "rive/importers/import_stack.hpp"
+#include "rive/importers/backboard_importer.hpp"
+#include "rive/layout_component.hpp"
+#include "rive/nested_artboard.hpp"
+#include "rive/nested_artboard_leaf.hpp"
+#include "rive/nested_artboard_layout.hpp"
+#include "rive/joystick.hpp"
+#include "rive/data_bind_flags.hpp"
+#include "rive/animation/nested_bool.hpp"
+#include "rive/animation/nested_number.hpp"
+#include "rive/animation/nested_trigger.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/shapes/shape.hpp"
+#include "rive/text/text_value_run.hpp"
+#include "rive/event.hpp"
+#include "rive/assets/audio_asset.hpp"
+
+#include <unordered_map>
+
+using namespace rive;
+
+Artboard::Artboard() {}
+
+Artboard::~Artboard()
+{
+#ifdef WITH_RIVE_AUDIO
+#ifdef EXTERNAL_RIVE_AUDIO_ENGINE
+    auto audioEngine = m_audioEngine;
+#else
+    auto audioEngine = AudioEngine::RuntimeEngine(false);
+#endif
+    if (audioEngine)
+    {
+        audioEngine->stop(this);
+    }
+#endif
+
+    for (auto object : m_Objects)
+    {
+        // First object is artboard
+        if (object == this)
+        {
+            continue;
+        }
+        delete object;
+    }
+
+    // Instances reference back to the original artboard's animations and state
+    // machines, so don't delete them here, they'll get cleaned up when the
+    // source is deleted.
+    // TODO: move this logic into ArtboardInstance destructor???
+    if (!m_IsInstance)
+    {
+        for (auto object : m_Animations)
+        {
+            delete object;
+        }
+        for (auto object : m_StateMachines)
+        {
+            delete object;
+        }
+    }
+}
+
+static bool canContinue(StatusCode code)
+{
+    // We currently only cease loading on invalid object.
+    return code != StatusCode::InvalidObject;
+}
+
+StatusCode Artboard::initialize()
+{
+    StatusCode code;
+
+    // these will be re-built in update() -- are they needed here?
+    m_backgroundPath = factory()->makeEmptyRenderPath();
+    m_clipPath = factory()->makeEmptyRenderPath();
+    m_layoutSizeWidth = width();
+    m_layoutSizeHeight = height();
+
+#ifdef WITH_RIVE_LAYOUT
+    markLayoutDirty(this);
+#endif
+    // onAddedDirty guarantees that all objects are now available so they can be
+    // looked up by index/id. This is where nodes find their parents, but they
+    // can't assume that their parent's parent will have resolved yet.
+    for (auto object : m_Objects)
+    {
+        if (object == nullptr)
+        {
+            // objects can be null if they were not understood by this runtime.
+            continue;
+        }
+        if (!canContinue(code = object->onAddedDirty(this)))
+        {
+            return code;
+        }
+    }
+
+    // Animations and StateMachines initialize only once on the source/origin
+    // Artboard. Instances will hold references to the original Animations and StateMachines, so
+    // running this code for instances will effectively initialize them twice. This can lead to
+    // unpredictable behaviour. One such example was that resolved objects like listener inputs were
+    // being added to lists twice.
+    if (!isInstance())
+    {
+        for (auto object : m_Animations)
+        {
+            if (!canContinue(code = object->onAddedDirty(this)))
+            {
+                return code;
+            }
+        }
+
+        for (auto object : m_StateMachines)
+        {
+            if (!canContinue(code = object->onAddedDirty(this)))
+            {
+                return code;
+            }
+        }
+    }
+
+    // Store a map of the drawRules to make it easier to lookup the matching
+    // rule for a transform component.
+    std::unordered_map<Core*, DrawRules*> componentDrawRules;
+
+    // onAddedClean is called when all individually referenced components have
+    // been found and so components can look at other components' references and
+    // assume that they have resolved too. This is where the whole hierarchy is
+    // linked up and we can traverse it to find other references (my parent's
+    // parent should be type X can be checked now).
+    for (auto object : m_Objects)
+    {
+        if (object == nullptr)
+        {
+            continue;
+        }
+        if (!canContinue(code = object->onAddedClean(this)))
+        {
+            return code;
+        }
+        switch (object->coreType())
+        {
+            case DrawRulesBase::typeKey:
+            {
+                DrawRules* rules = static_cast<DrawRules*>(object);
+                Core* component = resolve(rules->parentId());
+                if (component != nullptr)
+                {
+                    componentDrawRules[component] = rules;
+                }
+                else
+                {
+                    fprintf(stderr,
+                            "Artboard::initialize - Draw rule targets missing "
+                            "component width id %d\n",
+                            rules->parentId());
+                }
+                break;
+            }
+            case NestedArtboardBase::typeKey:
+            case NestedArtboardLeafBase::typeKey:
+            case NestedArtboardLayoutBase::typeKey:
+                m_NestedArtboards.push_back(object->as<NestedArtboard>());
+                break;
+
+            case JoystickBase::typeKey:
+            {
+                Joystick* joystick = object->as<Joystick>();
+                if (!joystick->canApplyBeforeUpdate())
+                {
+                    m_JoysticksApplyBeforeUpdate = false;
+                }
+                m_Joysticks.push_back(joystick);
+                break;
+            }
+        }
+    }
+
+    if (!isInstance())
+    {
+        for (auto object : m_Animations)
+        {
+            if (!canContinue(code = object->onAddedClean(this)))
+            {
+                return code;
+            }
+        }
+
+        for (auto object : m_StateMachines)
+        {
+            if (!canContinue(code = object->onAddedClean(this)))
+            {
+                return code;
+            }
+        }
+    }
+
+    // Multi-level references have been built up, now we can
+    // actually mark what's dependent on what.
+    for (auto object : m_Objects)
+    {
+        if (object == nullptr)
+        {
+            continue;
+        }
+        if (object->is<Component>())
+        {
+            object->as<Component>()->buildDependencies();
+        }
+        if (object->is<Drawable>() && object != this)
+        {
+            Drawable* drawable = object->as<Drawable>();
+            m_Drawables.push_back(drawable);
+
+            for (ContainerComponent* parent = drawable; parent != nullptr;
+                 parent = parent->parent())
+            {
+                auto itr = componentDrawRules.find(parent);
+                if (itr != componentDrawRules.end())
+                {
+                    drawable->flattenedDrawRules = itr->second;
+                    break;
+                }
+            }
+        }
+    }
+    // Iterate over the drawables in order to inject proxies for layouts
+    std::vector<LayoutComponent*> layouts;
+    for (int i = 0; i < m_Drawables.size(); i++)
+    {
+        auto drawable = m_Drawables[i];
+        LayoutComponent* currentLayout = nullptr;
+        bool isInCurrentLayout = true;
+        if (!layouts.empty())
+        {
+            currentLayout = layouts.back();
+            isInCurrentLayout = drawable->isChildOfLayout(currentLayout);
+        }
+        // We inject a DrawableProxy after all of the children of a LayoutComponent
+        // so that we can draw a stroke above and background below the children
+        // This also allows us to clip the children
+        if (currentLayout != nullptr && !isInCurrentLayout)
+        {
+            // This is the first item in the list of drawables that isn't a child
+            // of the layout, so we insert a proxy before it
+            do
+            {
+                m_Drawables.insert(m_Drawables.begin() + i, currentLayout->proxy());
+                layouts.pop_back();
+                if (!layouts.empty())
+                {
+                    currentLayout = layouts.back();
+                }
+                i += 1;
+            } while (!layouts.empty() && !drawable->isChildOfLayout(currentLayout));
+        }
+        if (drawable->is<LayoutComponent>())
+        {
+            layouts.push_back(drawable->as<LayoutComponent>());
+        }
+    }
+    while (!layouts.empty())
+    {
+        auto layout = layouts.back();
+        m_Drawables.push_back(layout->proxy());
+        layouts.pop_back();
+    }
+
+    sortDependencies();
+
+    std::vector<DrawRules*> rulesList;
+    // Build the rules in the right order. We use the map componentDrawRules
+    // to make sure we traverse the objects in the right order from parent
+    // to child, and add the rules accordingly.
+    for (auto object : m_Objects)
+    {
+        if (object == nullptr)
+        {
+            continue;
+        }
+        auto itr = componentDrawRules.find(object);
+        if (itr != componentDrawRules.end())
+        {
+            rulesList.emplace_back(componentDrawRules[object]);
+        }
+    }
+    DrawTarget root;
+    // Build up the draw order. Look for draw targets and build
+    // their dependencies.
+    for (auto rules : rulesList)
+    {
+        for (auto child : rules->children())
+        {
+            auto target = child->as<DrawTarget>();
+            root.addDependent(target);
+            auto dependentRules = target->drawable()->flattenedDrawRules;
+            if (dependentRules != nullptr)
+            {
+                // Because we don't store targets on rules, we need
+                // to find the targets that belong to this rule
+                // here.
+                for (auto object : m_Objects)
+                {
+                    if (object != nullptr && object->is<DrawTarget>())
+                    {
+                        DrawTarget* dependentTarget = object->as<DrawTarget>();
+                        if (dependentTarget->parent() == dependentRules)
+                        {
+                            dependentTarget->addDependent(target);
+                        }
+                    }
+                }
+            }
+        }
+    }
+    DependencySorter sorter;
+    std::vector<Component*> drawTargetOrder;
+    sorter.sort(&root, drawTargetOrder);
+    auto itr = drawTargetOrder.begin();
+    itr++;
+    while (itr != drawTargetOrder.end())
+    {
+        m_DrawTargets.push_back(static_cast<DrawTarget*>(*itr++));
+    }
+
+    // Some default layout dimensions.
+    m_layoutSizeWidth = width();
+    m_layoutSizeHeight = height();
+
+    return StatusCode::Ok;
+}
+
+void Artboard::sortDrawOrder()
+{
+    m_HasChangedDrawOrderInLastUpdate = true;
+    for (auto target : m_DrawTargets)
+    {
+        target->first = target->last = nullptr;
+    }
+
+    m_FirstDrawable = nullptr;
+    Drawable* lastDrawable = nullptr;
+    for (auto drawable : m_Drawables)
+    {
+        auto rules = drawable->flattenedDrawRules;
+        if (rules != nullptr && rules->activeTarget() != nullptr)
+        {
+
+            auto target = rules->activeTarget();
+            if (target->first == nullptr)
+            {
+                target->first = target->last = drawable;
+                drawable->prev = drawable->next = nullptr;
+            }
+            else
+            {
+                target->last->next = drawable;
+                drawable->prev = target->last;
+                target->last = drawable;
+                drawable->next = nullptr;
+            }
+        }
+        else
+        {
+            drawable->prev = lastDrawable;
+            drawable->next = nullptr;
+            if (lastDrawable == nullptr)
+            {
+                lastDrawable = m_FirstDrawable = drawable;
+            }
+            else
+            {
+                lastDrawable->next = drawable;
+                lastDrawable = drawable;
+            }
+        }
+    }
+
+    for (auto rule : m_DrawTargets)
+    {
+        if (rule->first == nullptr)
+        {
+            continue;
+        }
+        auto targetDrawable = rule->drawable();
+        switch (rule->placement())
+        {
+            case DrawTargetPlacement::before:
+            {
+                if (targetDrawable->prev != nullptr)
+                {
+                    targetDrawable->prev->next = rule->first;
+                    rule->first->prev = targetDrawable->prev;
+                }
+                if (targetDrawable == m_FirstDrawable)
+                {
+                    m_FirstDrawable = rule->first;
+                }
+                targetDrawable->prev = rule->last;
+                rule->last->next = targetDrawable;
+                break;
+            }
+            case DrawTargetPlacement::after:
+            {
+                if (targetDrawable->next != nullptr)
+                {
+                    targetDrawable->next->prev = rule->last;
+                    rule->last->next = targetDrawable->next;
+                }
+                if (targetDrawable == lastDrawable)
+                {
+                    lastDrawable = rule->last;
+                }
+                targetDrawable->next = rule->first;
+                rule->first->prev = targetDrawable;
+                break;
+            }
+        }
+    }
+
+    m_FirstDrawable = lastDrawable;
+}
+
+void Artboard::sortDependencies()
+{
+    DependencySorter sorter;
+    sorter.sort(this, m_DependencyOrder);
+    unsigned int graphOrder = 0;
+    for (auto component : m_DependencyOrder)
+    {
+        component->m_GraphOrder = graphOrder++;
+    }
+    m_Dirt |= ComponentDirt::Components;
+}
+
+void Artboard::addObject(Core* object) { m_Objects.push_back(object); }
+
+void Artboard::addAnimation(LinearAnimation* object) { m_Animations.push_back(object); }
+
+void Artboard::addStateMachine(StateMachine* object) { m_StateMachines.push_back(object); }
+
+Core* Artboard::resolve(uint32_t id) const
+{
+    if (id >= static_cast<int>(m_Objects.size()))
+    {
+        return nullptr;
+    }
+    return m_Objects[id];
+}
+
+uint32_t Artboard::idOf(Core* object) const
+{
+    auto it = std::find(m_Objects.begin(), m_Objects.end(), object);
+
+    if (it != m_Objects.end())
+    {
+        return castTo<uint32_t>(it - m_Objects.begin());
+    }
+    else
+    {
+        return 0;
+    }
+}
+
+void Artboard::onComponentDirty(Component* component)
+{
+    m_Dirt |= ComponentDirt::Components;
+
+    /// If the order of the component is less than the current dirt
+    /// depth, update the dirt depth so that the update loop can break
+    /// out early and re-run (something up the tree is dirty).
+    if (component->graphOrder() < m_DirtDepth)
+    {
+        m_DirtDepth = component->graphOrder();
+    }
+}
+
+void Artboard::onDirty(ComponentDirt dirt) { m_Dirt |= ComponentDirt::Components; }
+
+#ifdef WITH_RIVE_LAYOUT
+void Artboard::propagateSize()
+{
+    addDirt(ComponentDirt::Path);
+    if (sharesLayoutWithHost())
+    {
+        m_host->markTransformDirty();
+    }
+#ifdef WITH_RIVE_TOOLS
+    if (m_layoutChangedCallback != nullptr)
+    {
+        m_layoutChangedCallback(this);
+    }
+#endif
+}
+#endif
+
+bool Artboard::sharesLayoutWithHost() const
+{
+    return m_host != nullptr && m_host->is<NestedArtboardLayout>();
+}
+void Artboard::host(NestedArtboard* nestedArtboard)
+{
+    m_host = nestedArtboard;
+#ifdef WITH_RIVE_LAYOUT
+    if (!sharesLayoutWithHost())
+    {
+        return;
+    }
+    Artboard* parent = parentArtboard();
+    if (parent != nullptr)
+    {
+        parent->markLayoutDirty(this);
+        parent->syncLayoutChildren();
+    }
+#endif
+}
+
+NestedArtboard* Artboard::host() const { return m_host; }
+
+Artboard* Artboard::parentArtboard() const
+{
+    if (m_host == nullptr)
+    {
+        return nullptr;
+    }
+    return m_host->artboard();
+}
+
+float Artboard::layoutWidth() const
+{
+#ifdef WITH_RIVE_LAYOUT
+    return m_layoutSizeWidth;
+#else
+    return width();
+#endif
+}
+
+float Artboard::layoutHeight() const
+{
+#ifdef WITH_RIVE_LAYOUT
+    return m_layoutSizeHeight;
+#else
+    return height();
+#endif
+}
+
+float Artboard::layoutX() const
+{
+#ifdef WITH_RIVE_LAYOUT
+    return m_layoutLocationX;
+#else
+    return 0.0f;
+#endif
+}
+
+float Artboard::layoutY() const
+{
+#ifdef WITH_RIVE_LAYOUT
+    return m_layoutLocationY;
+#else
+    return 0.0f;
+#endif
+}
+
+void Artboard::updateRenderPath()
+{
+    AABB bg = AABB::fromLTWH(-layoutWidth() * originX(),
+                             -layoutHeight() * originY(),
+                             layoutWidth(),
+                             layoutHeight());
+    AABB clip;
+    if (m_FrameOrigin)
+    {
+        clip = {0.0f, 0.0f, layoutWidth(), layoutHeight()};
+    }
+    else
+    {
+        clip = bg;
+    }
+    m_clipPath = factory()->makeRenderPath(clip);
+    m_backgroundRawPath.rewind();
+    m_backgroundRawPath.addRect(bg);
+    m_backgroundPath->rewind();
+    m_backgroundRawPath.addTo(m_backgroundPath.get());
+}
+
+void Artboard::update(ComponentDirt value)
+{
+    Super::update(value);
+    if (hasDirt(value, ComponentDirt::DrawOrder))
+    {
+        sortDrawOrder();
+    }
+}
+
+void Artboard::updateDataBinds()
+{
+    for (auto dataBind : m_AllDataBinds)
+    {
+        dataBind->updateSourceBinding();
+        auto d = dataBind->dirt();
+        if (d == ComponentDirt::None)
+        {
+            continue;
+        }
+        dataBind->dirt(ComponentDirt::None);
+        dataBind->update(d);
+    }
+}
+
+bool Artboard::updateComponents()
+{
+    if (hasDirt(ComponentDirt::Components))
+    {
+        const int maxSteps = 100;
+        int step = 0;
+        auto count = m_DependencyOrder.size();
+        while (hasDirt(ComponentDirt::Components) && step < maxSteps)
+        {
+            m_Dirt = m_Dirt & ~ComponentDirt::Components;
+
+            // Track dirt depth here so that if something else marks
+            // dirty, we restart.
+            for (unsigned int i = 0; i < count; i++)
+            {
+                auto component = m_DependencyOrder[i];
+                m_DirtDepth = i;
+                auto d = component->m_Dirt;
+                if (d == ComponentDirt::None ||
+                    (d & ComponentDirt::Collapsed) == ComponentDirt::Collapsed)
+                {
+                    continue;
+                }
+                component->m_Dirt = ComponentDirt::None;
+                component->update(d);
+
+                // If the update changed the dirt depth by adding dirt
+                // to something before us (in the DAG), early out and
+                // re-run the update.
+                if (m_DirtDepth < i)
+                {
+                    break;
+                }
+            }
+            step++;
+        }
+        return true;
+    }
+    return false;
+}
+
+void* Artboard::takeLayoutNode()
+{
+#ifdef WITH_RIVE_LAYOUT
+    m_updatesOwnLayout = false;
+    return static_cast<void*>(&layoutNode());
+#else
+    return nullptr;
+#endif
+}
+
+void Artboard::markLayoutDirty(LayoutComponent* layoutComponent)
+{
+#ifdef WITH_RIVE_TOOLS
+    if (m_dirtyLayout.empty() && m_layoutDirtyCallback != nullptr)
+    {
+        m_layoutDirtyCallback(this);
+    }
+#endif
+    m_dirtyLayout.insert(layoutComponent);
+    if (sharesLayoutWithHost())
+    {
+        // TODO: Follow up with Luigi
+        // This only gets called when the NestedArtboardLayout is in the runtime
+        // but seems to cause an infinite loop in certain cases
+        // m_host->as<NestedArtboardLayout>()->markNestedLayoutDirty();
+    }
+}
+
+bool Artboard::syncStyleChanges()
+{
+    bool updated = false;
+#ifdef WITH_RIVE_LAYOUT
+    if (!m_dirtyLayout.empty())
+    {
+        for (auto layout : m_dirtyLayout)
+        {
+            switch (layout->coreType())
+            {
+                case ArtboardBase::typeKey:
+                {
+                    auto artboard = layout->as<Artboard>();
+                    if (artboard == this)
+                    {
+                        artboard->syncStyle();
+                    }
+                    else
+                    {
+                        // This is a nested artboard, sync its changes too.
+                        artboard->syncStyleChanges();
+                    }
+                    break;
+                }
+
+                default:
+                    layout->syncStyle();
+                    break;
+            }
+        }
+        m_dirtyLayout.clear();
+        updated = true;
+    }
+#endif
+    return updated;
+}
+
+bool Artboard::advanceInternal(double elapsedSeconds, bool isRoot, bool nested)
+{
+    bool didUpdate = false;
+    m_HasChangedDrawOrderInLastUpdate = false;
+#ifdef WITH_RIVE_LAYOUT
+    if (hasDirt(ComponentDirt::LayoutStyle))
+    {
+        cascadeAnimationStyle(interpolation(), interpolator(), interpolationTime());
+    }
+
+    if (syncStyleChanges() && m_updatesOwnLayout)
+    {
+        calculateLayout();
+    }
+
+    for (auto dep : m_DependencyOrder)
+    {
+        if (dep->is<LayoutComponent>())
+        {
+            auto layout = dep->as<LayoutComponent>();
+            layout->updateLayoutBounds();
+            if ((dep == this && Super::advance(elapsedSeconds)) ||
+                (dep != this && layout->advance(elapsedSeconds)))
+            {
+                didUpdate = true;
+            }
+        }
+    }
+
+#endif
+    if (m_JoysticksApplyBeforeUpdate)
+    {
+        for (auto joystick : m_Joysticks)
+        {
+            joystick->apply(this);
+        }
+    }
+    if (isRoot)
+    {
+        updateDataBinds();
+    }
+    if (updateComponents())
+    {
+        didUpdate = true;
+    }
+    if (!m_JoysticksApplyBeforeUpdate)
+    {
+        for (auto joystick : m_Joysticks)
+        {
+            if (!joystick->canApplyBeforeUpdate())
+            {
+                if (isRoot)
+                {
+                    updateDataBinds();
+                }
+                if (updateComponents())
+                {
+                    didUpdate = true;
+                }
+            }
+            joystick->apply(this);
+        }
+        if (isRoot)
+        {
+            updateDataBinds();
+        }
+        if (updateComponents())
+        {
+            didUpdate = true;
+        }
+    }
+    if (nested)
+    {
+        for (auto nestedArtboard : m_NestedArtboards)
+        {
+            if (nestedArtboard->advance((float)elapsedSeconds))
+            {
+                didUpdate = true;
+            }
+        }
+    }
+    return didUpdate;
+}
+
+bool Artboard::advance(double elapsedSeconds, bool nested)
+{
+    return advanceInternal(elapsedSeconds, true, nested);
+}
+
+Core* Artboard::hitTest(HitInfo* hinfo, const Mat2D& xform)
+{
+    if (clip())
+    {
+        // TODO: can we get the rawpath for the clip?
+    }
+
+    auto mx = xform;
+    if (m_FrameOrigin)
+    {
+        mx *= Mat2D::fromTranslate(layoutWidth() * originX(), layoutHeight() * originY());
+    }
+
+    Drawable* last = m_FirstDrawable;
+    if (last)
+    {
+        // walk to the end, so we can visit in reverse-order
+        while (last->prev)
+        {
+            last = last->prev;
+        }
+    }
+    for (auto drawable = last; drawable; drawable = drawable->next)
+    {
+        if (drawable->isHidden())
+        {
+            continue;
+        }
+        if (auto c = drawable->hitTest(hinfo, mx))
+        {
+            return c;
+        }
+    }
+
+    // TODO: should we hit-test the background?
+
+    return nullptr;
+}
+
+void Artboard::draw(Renderer* renderer) { draw(renderer, DrawOption::kNormal); }
+
+void Artboard::draw(Renderer* renderer, DrawOption option)
+{
+    renderer->save();
+    if (clip())
+    {
+        renderer->clipPath(m_clipPath.get());
+    }
+
+    if (m_FrameOrigin)
+    {
+        Mat2D artboardTransform;
+        artboardTransform[4] = layoutWidth() * originX();
+        artboardTransform[5] = layoutHeight() * originY();
+        renderer->transform(artboardTransform);
+    }
+
+    if (option != DrawOption::kHideBG)
+    {
+        for (auto shapePaint : m_ShapePaints)
+        {
+            shapePaint->draw(renderer, m_backgroundPath.get(), &m_backgroundRawPath);
+        }
+    }
+
+    if (option != DrawOption::kHideFG)
+    {
+        for (auto drawable = m_FirstDrawable; drawable != nullptr; drawable = drawable->prev)
+        {
+            if (drawable->isHidden())
+            {
+                continue;
+            }
+            drawable->draw(renderer);
+        }
+    }
+
+    renderer->restore();
+}
+
+void Artboard::addToRenderPath(RenderPath* path, const Mat2D& transform)
+{
+    for (auto drawable = m_FirstDrawable; drawable != nullptr; drawable = drawable->prev)
+    {
+        if (drawable->isHidden() || !drawable->is<Shape>())
+        {
+            continue;
+        }
+        Shape* shape = drawable->as<Shape>();
+        shape->addToRenderPath(path, transform);
+    }
+}
+
+Vec2D Artboard::origin() const
+{
+    return m_FrameOrigin ? Vec2D(0.0f, 0.0f)
+                         : Vec2D(-layoutWidth() * originX(), -layoutHeight() * originY());
+}
+
+AABB Artboard::bounds() const
+{
+    return m_FrameOrigin ? AABB(0.0f, 0.0f, layoutWidth(), layoutHeight())
+                         : AABB::fromLTWH(-layoutWidth() * originX(),
+                                          -layoutHeight() * originY(),
+                                          layoutWidth(),
+                                          layoutHeight());
+}
+
+bool Artboard::isTranslucent() const
+{
+    for (const auto sp : m_ShapePaints)
+    {
+        if (!sp->isTranslucent())
+        {
+            return false; // one opaque fill is sufficient to be opaque
+        }
+    }
+    return true;
+}
+
+bool Artboard::hasAudio() const
+{
+    for (auto object : m_Objects)
+    {
+        if (object != nullptr && object->coreType() == AudioEventBase::typeKey)
+        {
+            return true;
+        }
+    }
+    for (auto nestedArtboard : m_NestedArtboards)
+    {
+        if (nestedArtboard->artboardInstance()->hasAudio())
+        {
+            return true;
+        }
+    }
+    return false;
+}
+
+bool Artboard::isTranslucent(const LinearAnimation* anim) const
+{
+    // For now we're conservative/lazy -- if we see that any of our paints are
+    // animated we assume that might make it non-opaque, so we early out
+    for (const auto& obj : anim->m_KeyedObjects)
+    {
+        const auto ptr = this->resolve(obj->objectId());
+        for (const auto sp : m_ShapePaints)
+        {
+            if (ptr == sp)
+            {
+                return true;
+            }
+        }
+    }
+
+    // If we get here, we have no animations, so just check our paints for
+    // opacity
+    return this->isTranslucent();
+}
+
+bool Artboard::isTranslucent(const LinearAnimationInstance* inst) const
+{
+    return this->isTranslucent(inst->animation());
+}
+
+std::string Artboard::animationNameAt(size_t index) const
+{
+    auto la = this->animation(index);
+    return la ? la->name() : nullptr;
+}
+
+std::string Artboard::stateMachineNameAt(size_t index) const
+{
+    auto sm = this->stateMachine(index);
+    return sm ? sm->name() : nullptr;
+}
+
+LinearAnimation* Artboard::animation(const std::string& name) const
+{
+    for (auto animation : m_Animations)
+    {
+        if (animation->name() == name)
+        {
+            return animation;
+        }
+    }
+    return nullptr;
+}
+
+LinearAnimation* Artboard::animation(size_t index) const
+{
+    if (index >= m_Animations.size())
+    {
+        return nullptr;
+    }
+    return m_Animations[index];
+}
+
+StateMachine* Artboard::stateMachine(const std::string& name) const
+{
+    for (auto machine : m_StateMachines)
+    {
+        if (machine->name() == name)
+        {
+            return machine;
+        }
+    }
+    return nullptr;
+}
+
+StateMachine* Artboard::stateMachine(size_t index) const
+{
+    if (index >= m_StateMachines.size())
+    {
+        return nullptr;
+    }
+    return m_StateMachines[index];
+}
+
+int Artboard::defaultStateMachineIndex() const
+{
+    int index = defaultStateMachineId();
+    if ((size_t)index >= m_StateMachines.size())
+    {
+        index = -1;
+    }
+    return index;
+}
+
+NestedArtboard* Artboard::nestedArtboard(const std::string& name) const
+{
+    for (auto nested : m_NestedArtboards)
+    {
+        if (nested->name() == name)
+        {
+            return nested;
+        }
+    }
+    return nullptr;
+}
+
+NestedArtboard* Artboard::nestedArtboardAtPath(const std::string& path) const
+{
+    // name parameter can be a name or a path to recursively find a nested artboard
+    std::string delimiter = "/";
+    size_t firstDelim = path.find(delimiter);
+    std::string artboardName = firstDelim == std::string::npos ? path : path.substr(0, firstDelim);
+    std::string restOfPath =
+        firstDelim == std::string::npos ? "" : path.substr(firstDelim + 1, path.size());
+
+    // Find the nested artboard at this level
+    if (!artboardName.empty())
+    {
+        auto nested = nestedArtboard(artboardName);
+        if (nested != nullptr)
+        {
+            if (restOfPath.empty())
+            {
+                return nested;
+            }
+            else
+            {
+                auto artboard = nested->artboardInstance();
+                return artboard->nestedArtboardAtPath(restOfPath);
+            }
+        }
+    }
+    return nullptr;
+}
+
+// std::unique_ptr<ArtboardInstance> Artboard::instance() const
+// {
+//     std::unique_ptr<ArtboardInstance> artboardClone(new ArtboardInstance);
+//     artboardClone->copy(*this);
+
+//     artboardClone->m_Factory = m_Factory;
+//     artboardClone->m_FrameOrigin = m_FrameOrigin;
+//     artboardClone->m_IsInstance = true;
+
+//     std::vector<Core*>& cloneObjects = artboardClone->m_Objects;
+//     cloneObjects.push_back(artboardClone.get());
+
+//     if (!m_Objects.empty())
+//     {
+//         // Skip first object (artboard).
+//         auto itr = m_Objects.begin();
+//         while (++itr != m_Objects.end())
+//         {
+//             auto object = *itr;
+//             cloneObjects.push_back(object == nullptr ? nullptr : object->clone());
+//         }
+//     }
+
+//     for (auto animation : m_Animations)
+//     {
+//         artboardClone->m_Animations.push_back(animation);
+//     }
+//     for (auto stateMachine : m_StateMachines)
+//     {
+//         artboardClone->m_StateMachines.push_back(stateMachine);
+//     }
+
+//     if (artboardClone->initialize() != StatusCode::Ok)
+//     {
+//         artboardClone = nullptr;
+//     }
+
+//     assert(artboardClone->isInstance());
+//     return artboardClone;
+// }
+
+void Artboard::frameOrigin(bool value)
+{
+    if (value == m_FrameOrigin)
+    {
+        return;
+    }
+    m_FrameOrigin = value;
+    addDirt(ComponentDirt::Path);
+}
+
+StatusCode Artboard::import(ImportStack& importStack)
+{
+    auto backboardImporter = importStack.latest<BackboardImporter>(Backboard::typeKey);
+    if (backboardImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+
+    StatusCode result = Super::import(importStack);
+    if (result == StatusCode::Ok)
+    {
+        backboardImporter->addArtboard(this);
+    }
+    else
+    {
+        backboardImporter->addMissingArtboard();
+    }
+    return result;
+}
+
+void Artboard::internalDataContext(DataContext* value, DataContext* parent, bool isRoot)
+{
+    m_DataContext = value;
+    m_DataContext->parent(parent);
+    for (auto nestedArtboard : m_NestedArtboards)
+    {
+        if (nestedArtboard->artboardInstance() == nullptr)
+        {
+            continue;
+        }
+        auto value = m_DataContext->getViewModelInstance(nestedArtboard->dataBindPathIds());
+        if (value != nullptr && value->is<ViewModelInstance>())
+        {
+            nestedArtboard->dataContextFromInstance(value, m_DataContext);
+        }
+        else
+        {
+            nestedArtboard->internalDataContext(m_DataContext, m_DataContext->parent());
+        }
+    }
+    for (auto dataBind : m_DataBinds)
+    {
+        if (dataBind->is<DataBindContext>())
+        {
+            dataBind->as<DataBindContext>()->bindFromContext(m_DataContext);
+        }
+    }
+    if (isRoot)
+    {
+        std::vector<DataBind*> dataBinds;
+        populateDataBinds(&dataBinds);
+        sortDataBinds(dataBinds);
+    }
+}
+
+void Artboard::sortDataBinds(std::vector<DataBind*> dataBinds)
+{
+    // TODO: @hernan review this. Should not need to push to a component list to sort.
+
+    for (auto dataBind : dataBinds)
+    {
+        m_AllDataBinds.push_back(dataBind->as<DataBind>());
+    }
+}
+
+float Artboard::volume() const { return m_volume; }
+void Artboard::volume(float value)
+{
+    m_volume = value;
+    for (auto nestedArtboard : m_NestedArtboards)
+    {
+        auto artboard = nestedArtboard->artboardInstance();
+        if (artboard != nullptr)
+        {
+            artboard->volume(value);
+        }
+    }
+}
+
+void Artboard::populateDataBinds(std::vector<DataBind*>* dataBinds)
+{
+    for (auto dataBind : m_DataBinds)
+    {
+        dataBinds->push_back(dataBind);
+    }
+    for (auto nestedArtboard : m_NestedArtboards)
+    {
+        if (nestedArtboard->artboardInstance() != nullptr)
+        {
+            nestedArtboard->artboardInstance()->populateDataBinds(dataBinds);
+        }
+    }
+}
+
+void Artboard::addDataBind(DataBind* dataBind) { m_DataBinds.push_back(dataBind); }
+
+void Artboard::dataContext(DataContext* value, DataContext* parent)
+{
+    internalDataContext(value, parent, true);
+}
+
+void Artboard::dataContextFromInstance(ViewModelInstance* viewModelInstance)
+{
+    dataContextFromInstance(viewModelInstance, nullptr, true);
+}
+
+void Artboard::dataContextFromInstance(ViewModelInstance* viewModelInstance, DataContext* parent)
+{
+    dataContextFromInstance(viewModelInstance, parent, true);
+}
+
+void Artboard::dataContextFromInstance(ViewModelInstance* viewModelInstance,
+                                       DataContext* parent,
+                                       bool isRoot)
+{
+    if (viewModelInstance == nullptr)
+    {
+        return;
+    }
+    if (isRoot)
+    {
+        viewModelInstance->setAsRoot();
+    }
+    internalDataContext(new DataContext(viewModelInstance), parent, isRoot);
+}
+
+////////// ArtboardInstance
+
+#include "rive/animation/linear_animation_instance.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+
+ArtboardInstance::ArtboardInstance() {}
+
+ArtboardInstance::~ArtboardInstance() {}
+
+std::unique_ptr<LinearAnimationInstance> ArtboardInstance::animationAt(size_t index)
+{
+    auto la = this->animation(index);
+    return la ? rivestd::make_unique<LinearAnimationInstance>(la, this) : nullptr;
+}
+
+std::unique_ptr<LinearAnimationInstance> ArtboardInstance::animationNamed(const std::string& name)
+{
+    auto la = this->animation(name);
+    return la ? rivestd::make_unique<LinearAnimationInstance>(la, this) : nullptr;
+}
+
+std::unique_ptr<StateMachineInstance> ArtboardInstance::stateMachineAt(size_t index)
+{
+    auto sm = this->stateMachine(index);
+    return sm ? rivestd::make_unique<StateMachineInstance>(sm, this) : nullptr;
+}
+
+std::unique_ptr<StateMachineInstance> ArtboardInstance::stateMachineNamed(const std::string& name)
+{
+    auto sm = this->stateMachine(name);
+    return sm ? rivestd::make_unique<StateMachineInstance>(sm, this) : nullptr;
+}
+
+std::unique_ptr<StateMachineInstance> ArtboardInstance::defaultStateMachine()
+{
+    const int index = this->defaultStateMachineIndex();
+    return index >= 0 ? this->stateMachineAt(index) : nullptr;
+}
+
+std::unique_ptr<Scene> ArtboardInstance::defaultScene()
+{
+    std::unique_ptr<Scene> scene = this->defaultStateMachine();
+    if (!scene)
+    {
+        scene = this->stateMachineAt(0);
+    }
+    if (!scene)
+    {
+        scene = this->animationAt(0);
+    }
+    return scene;
+}
+
+SMIInput* ArtboardInstance::input(const std::string& name, const std::string& path)
+{
+    return getNamedInput<SMIInput>(name, path);
+}
+
+template <typename InstType>
+InstType* ArtboardInstance::getNamedInput(const std::string& name, const std::string& path)
+{
+    if (!path.empty())
+    {
+        auto nestedArtboard = nestedArtboardAtPath(path);
+        if (nestedArtboard != nullptr)
+        {
+            auto input = nestedArtboard->input(name);
+            if (input != nullptr && input->input() != nullptr)
+            {
+                return static_cast<InstType*>(input->input());
+            }
+        }
+    }
+    return nullptr;
+}
+
+SMIBool* ArtboardInstance::getBool(const std::string& name, const std::string& path)
+{
+    return getNamedInput<SMIBool>(name, path);
+}
+
+SMINumber* ArtboardInstance::getNumber(const std::string& name, const std::string& path)
+{
+    return getNamedInput<SMINumber>(name, path);
+}
+SMITrigger* ArtboardInstance::getTrigger(const std::string& name, const std::string& path)
+{
+    return getNamedInput<SMITrigger>(name, path);
+}
+
+TextValueRun* ArtboardInstance::getTextRun(const std::string& name, const std::string& path)
+{
+    if (path.empty())
+    {
+        return nullptr;
+    }
+
+    auto nestedArtboard = nestedArtboardAtPath(path);
+    if (nestedArtboard == nullptr)
+    {
+        return nullptr;
+    }
+
+    auto artboardInstance = nestedArtboard->artboardInstance();
+    if (artboardInstance == nullptr)
+    {
+        return nullptr;
+    }
+
+    return artboardInstance->find<TextValueRun>(name);
+}
+
+#ifdef EXTERNAL_RIVE_AUDIO_ENGINE
+rcp<AudioEngine> Artboard::audioEngine() const { return m_audioEngine; }
+void Artboard::audioEngine(rcp<AudioEngine> audioEngine)
+{
+    m_audioEngine = audioEngine;
+    for (auto nestedArtboard : m_NestedArtboards)
+    {
+        auto artboard = nestedArtboard->artboardInstance();
+        if (artboard != nullptr)
+        {
+            artboard->audioEngine(audioEngine);
+        }
+    }
+}
+#endif
\ No newline at end of file
diff --git a/src/assets/audio_asset.cpp b/src/assets/audio_asset.cpp
new file mode 100644
index 0000000..de59fae
--- /dev/null
+++ b/src/assets/audio_asset.cpp
@@ -0,0 +1,19 @@
+#include "rive/assets/audio_asset.hpp"
+#include "rive/factory.hpp"
+
+using namespace rive;
+
+AudioAsset::AudioAsset() {}
+
+AudioAsset::~AudioAsset() {}
+
+bool AudioAsset::decode(SimpleArray<uint8_t>& bytes, Factory* factory)
+{
+#ifdef WITH_RIVE_AUDIO
+    // Steal the bytes.
+    m_audioSource = rcp<AudioSource>(new AudioSource(std::move(bytes)));
+#endif
+    return true;
+}
+
+std::string AudioAsset::fileExtension() const { return "wav"; }
\ No newline at end of file
diff --git a/src/assets/file_asset.cpp b/src/assets/file_asset.cpp
new file mode 100644
index 0000000..03a8766
--- /dev/null
+++ b/src/assets/file_asset.cpp
@@ -0,0 +1,72 @@
+#include <sstream>
+#include <iomanip>
+#include <array>
+
+#include "rive/assets/file_asset.hpp"
+#include "rive/backboard.hpp"
+#include "rive/importers/backboard_importer.hpp"
+
+using namespace rive;
+
+StatusCode FileAsset::import(ImportStack& importStack)
+{
+    auto backboardImporter = importStack.latest<BackboardImporter>(Backboard::typeKey);
+    if (backboardImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    backboardImporter->addFileAsset(this);
+
+    return Super::import(importStack);
+}
+
+std::string FileAsset::uniqueName() const
+{
+    // remove final extension
+    std::string uniqueName = name();
+    std::size_t finalDot = uniqueName.rfind('.');
+
+    if (finalDot != std::string::npos)
+    {
+        uniqueName = uniqueName.substr(0, finalDot);
+    }
+    return uniqueName + "-" + std::to_string(assetId());
+}
+
+std::string FileAsset::uniqueFilename() const { return uniqueName() + "." + fileExtension(); }
+
+void FileAsset::copyCdnUuid(const FileAssetBase& object)
+{
+    // Should never be called.
+    assert(false);
+}
+
+void FileAsset::decodeCdnUuid(Span<const uint8_t> value)
+{
+    m_cdnUuid = std::vector<uint8_t>(value.begin(), value.end());
+}
+
+Span<const uint8_t> FileAsset::cdnUuid() const { return m_cdnUuid; }
+
+std::string FileAsset::cdnUuidStr() const
+{
+    constexpr uint8_t uuidSize = 16;
+    if (m_cdnUuid.size() != uuidSize)
+    {
+        return "";
+    }
+    const std::array<const int, uuidSize>
+        indices{3, 2, 1, 0, 5, 4, 7, 6, 9, 8, 15, 14, 13, 12, 11, 10};
+
+    std::stringstream ss;
+    ss << std::hex << std::setfill('0');
+    for (int idx : indices)
+    {
+        ss << std::setw(2) // always 2 chars
+           << static_cast<unsigned int>(m_cdnUuid[idx]);
+        if (idx == 0 || idx == 4 || idx == 6 || idx == 8)
+            ss << '-';
+    }
+
+    return ss.str();
+}
\ No newline at end of file
diff --git a/src/assets/file_asset_contents.cpp b/src/assets/file_asset_contents.cpp
new file mode 100644
index 0000000..fb90573
--- /dev/null
+++ b/src/assets/file_asset_contents.cpp
@@ -0,0 +1,31 @@
+#include <vector>
+#include "rive/assets/file_asset_contents.hpp"
+#include "rive/assets/file_asset.hpp"
+#include "rive/importers/file_asset_importer.hpp"
+
+using namespace rive;
+
+StatusCode FileAssetContents::import(ImportStack& importStack)
+{
+    auto fileAssetImporter = importStack.latest<FileAssetImporter>(FileAsset::typeKey);
+    if (fileAssetImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    fileAssetImporter->onFileAssetContents(std::unique_ptr<FileAssetContents>(this));
+
+    return Super::import(importStack);
+}
+
+void FileAssetContents::decodeBytes(Span<const uint8_t> value)
+{
+    m_bytes = SimpleArray<uint8_t>(value.data(), value.size());
+}
+
+void FileAssetContents::copyBytes(const FileAssetContentsBase& object)
+{
+    // Should never be called.
+    assert(false);
+}
+
+SimpleArray<uint8_t>& FileAssetContents::bytes() { return m_bytes; }
diff --git a/src/assets/file_asset_referencer.cpp b/src/assets/file_asset_referencer.cpp
new file mode 100644
index 0000000..11b7996
--- /dev/null
+++ b/src/assets/file_asset_referencer.cpp
@@ -0,0 +1,39 @@
+#include "rive/assets/file_asset_referencer.hpp"
+#include "rive/backboard.hpp"
+#include "rive/assets/file_asset.hpp"
+#include "rive/importers/backboard_importer.hpp"
+
+using namespace rive;
+
+FileAssetReferencer::~FileAssetReferencer()
+{
+    if (m_fileAsset != nullptr)
+    {
+        m_fileAsset->removeFileAssetReferencer(this);
+    }
+}
+
+StatusCode FileAssetReferencer::registerReferencer(ImportStack& importStack)
+{
+    auto backboardImporter = importStack.latest<BackboardImporter>(Backboard::typeKey);
+    if (backboardImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    backboardImporter->addFileAssetReferencer(this);
+
+    return StatusCode::Ok;
+}
+
+void FileAssetReferencer::setAsset(FileAsset* asset)
+{
+    if (m_fileAsset != nullptr)
+    {
+        m_fileAsset->removeFileAssetReferencer(this);
+    }
+    m_fileAsset = asset;
+    if (asset != nullptr)
+    {
+        asset->addFileAssetReferencer(this);
+    }
+};
\ No newline at end of file
diff --git a/src/assets/font_asset.cpp b/src/assets/font_asset.cpp
new file mode 100644
index 0000000..e499b28
--- /dev/null
+++ b/src/assets/font_asset.cpp
@@ -0,0 +1,27 @@
+#include "rive/text/text_style.hpp"
+#include "rive/assets/file_asset_referencer.hpp"
+#include "rive/assets/font_asset.hpp"
+#include "rive/artboard.hpp"
+#include "rive/factory.hpp"
+
+using namespace rive;
+
+bool FontAsset::decode(SimpleArray<uint8_t>& data, Factory* factory)
+{
+    font(factory->decodeFont(data));
+    return m_font != nullptr;
+}
+std::string FontAsset::fileExtension() const { return "ttf"; }
+
+void FontAsset::font(rcp<Font> font)
+{
+    m_font = std::move(font);
+
+    // We could try to tie this to some generic FileAssetReferencer callback
+    // but for that we'd need this to be behind a more generic setter like
+    // ::asset(rcp<Asset> asset)
+    for (FileAssetReferencer* fileAssetReferencer : fileAssetReferencers())
+    {
+        static_cast<TextStyle*>(fileAssetReferencer)->addDirt(ComponentDirt::TextShape);
+    }
+}
\ No newline at end of file
diff --git a/src/assets/image_asset.cpp b/src/assets/image_asset.cpp
new file mode 100644
index 0000000..cc59030
--- /dev/null
+++ b/src/assets/image_asset.cpp
@@ -0,0 +1,23 @@
+#include "rive/assets/image_asset.hpp"
+#include "rive/artboard.hpp"
+#include "rive/factory.hpp"
+
+using namespace rive;
+
+ImageAsset::~ImageAsset() {}
+
+bool ImageAsset::decode(SimpleArray<uint8_t>& data, Factory* factory)
+{
+#ifdef TESTING
+    decodedByteSize = data.size();
+#endif
+    renderImage(factory->decodeImage(data));
+    return m_RenderImage != nullptr;
+}
+
+void ImageAsset::renderImage(rcp<RenderImage> renderImage)
+{
+    m_RenderImage = std::move(renderImage);
+}
+
+std::string ImageAsset::fileExtension() const { return "png"; }
diff --git a/src/audio/audio_engine.cpp b/src/audio/audio_engine.cpp
new file mode 100644
index 0000000..0bdd9f9
--- /dev/null
+++ b/src/audio/audio_engine.cpp
@@ -0,0 +1,503 @@
+#ifdef WITH_RIVE_AUDIO
+#include "rive/math/simd.hpp"
+#ifdef __APPLE__
+#include <TargetConditionals.h>
+#if TARGET_IPHONE_SIMULATOR || TARGET_OS_MACCATALYST || TARGET_OS_IPHONE
+// Don't define MINIAUDIO_IMPLEMENTATION ON iOS
+#elif TARGET_OS_MAC
+#define MINIAUDIO_IMPLEMENTATION
+#else
+#error "Unknown Apple platform"
+#endif
+#else
+#define MINIAUDIO_IMPLEMENTATION
+#endif
+#include "miniaudio.h"
+
+#include "rive/audio/audio_engine.hpp"
+#include "rive/audio/audio_sound.hpp"
+#include "rive/audio/audio_source.hpp"
+
+#include <algorithm>
+#include <cmath>
+
+using namespace rive;
+
+void AudioEngine::SoundCompleted(void* pUserData, ma_sound* pSound)
+{
+    AudioSound* audioSound = (AudioSound*)pUserData;
+    auto engine = audioSound->m_engine;
+    engine->soundCompleted(ref_rcp(audioSound));
+}
+
+void AudioEngine::unlinkSound(rcp<AudioSound> sound)
+{
+    auto next = sound->m_nextPlaying;
+    auto prev = sound->m_prevPlaying;
+    if (next != nullptr)
+    {
+        next->m_prevPlaying = prev;
+    }
+    if (prev != nullptr)
+    {
+        prev->m_nextPlaying = next;
+    }
+
+    if (m_playingSoundsHead == sound)
+    {
+        m_playingSoundsHead = next;
+    }
+
+    sound->m_nextPlaying = nullptr;
+    sound->m_prevPlaying = nullptr;
+}
+
+void AudioEngine::soundCompleted(rcp<AudioSound> sound)
+{
+    std::unique_lock<std::mutex> lock(m_mutex);
+    m_completedSounds.push_back(sound);
+    unlinkSound(sound);
+}
+
+#ifdef WITH_RIVE_AUDIO_TOOLS
+namespace rive
+{
+class LevelsNode
+{
+public:
+    ma_node_base base;
+    AudioEngine* engine;
+    static void measureLevels(ma_node* pNode,
+                              const float** ppFramesIn,
+                              ma_uint32* pFrameCountIn,
+                              float** ppFramesOut,
+                              ma_uint32* pFrameCountOut)
+    {
+        const float* frames = ppFramesIn[0];
+
+        ma_uint32 frameCount = pFrameCountIn[0];
+
+        static_cast<LevelsNode*>(pNode)->engine->measureLevels(frames, (uint32_t)frameCount);
+    }
+};
+} // namespace rive
+
+void AudioEngine::measureLevels(const float* frames, uint32_t frameCount)
+{
+    uint32_t channelCount = channels();
+
+    for (uint32_t i = 0; i < frameCount; i++)
+    {
+        for (uint32_t c = 0; c < channelCount; c++)
+        {
+            float sample = *frames++;
+            m_levels[c] = std::max(m_levels[c], sample);
+        }
+    }
+}
+
+static ma_node_vtable measure_levels_vtable = {LevelsNode::measureLevels,
+                                               nullptr,
+                                               1,
+                                               1,
+                                               MA_NODE_FLAG_PASSTHROUGH};
+
+void AudioEngine::initLevelMonitor()
+{
+    if (m_levelMonitor == nullptr)
+    {
+        m_levelMonitor = new LevelsNode();
+        m_levelMonitor->engine = this;
+
+        ma_node_config nodeConfig = ma_node_config_init();
+        nodeConfig.vtable = &measure_levels_vtable;
+        uint32_t channelCount = channels();
+        nodeConfig.pInputChannels = &channelCount;
+        nodeConfig.pOutputChannels = &channelCount;
+        m_levels.resize(channelCount);
+
+        auto graph = ma_engine_get_node_graph(m_engine);
+        if (ma_node_init(graph, &nodeConfig, nullptr, &m_levelMonitor->base) != MA_SUCCESS)
+        {
+            delete m_levelMonitor;
+            m_levelMonitor = nullptr;
+            return;
+        }
+        if (ma_node_attach_output_bus(&m_levelMonitor->base,
+                                      0,
+                                      ma_node_graph_get_endpoint(graph),
+                                      0) != MA_SUCCESS)
+        {
+            ma_node_uninit(&m_levelMonitor->base, nullptr);
+            delete m_levelMonitor;
+            m_levelMonitor = nullptr;
+            return;
+        }
+    }
+}
+
+void AudioEngine::levels(Span<float> levels)
+{
+    int size = std::min((int)m_levels.size(), (int)levels.size());
+    for (int i = 0; i < size; i++)
+    {
+        levels[i] = m_levels[i];
+        m_levels[i] = 0.0f;
+    }
+}
+
+float AudioEngine::level(uint32_t channel)
+{
+    if (channel < m_levels.size())
+    {
+        float value = m_levels[channel];
+        m_levels[channel] = 0.0f;
+        return value;
+    }
+    return 0.0f;
+}
+#endif
+
+void AudioEngine::start() { ma_engine_start(m_engine); }
+void AudioEngine::stop() { ma_engine_stop(m_engine); }
+
+rcp<AudioEngine> AudioEngine::Make(uint32_t numChannels, uint32_t sampleRate)
+{
+// I _think_ MA_NO_DEVICE_IO is defined when building for Unity; otherwise, it seems to pass
+// "standard" building When defined, pContext is unavailable, which causes build errors when
+// building Unity for iOS. - David
+#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_MACCATALYST || TARGET_OS_IPHONE) &&                      \
+    !defined(MA_NO_DEVICE_IO)
+    // Used for configuration only, and isn't referenced past the usage of ma_context_init; thus,
+    // can be locally scoped. Uses the "logical" defaults from miniaudio, and updates only what we
+    // need. This should automatically set available backends in priority order based on the target
+    // it's built for, which in the case of Apple is Core Audio first.
+    ma_context_config contextConfig = ma_context_config_init();
+    contextConfig.coreaudio.sessionCategoryOptions = ma_ios_session_category_option_mix_with_others;
+
+    // We only need to initialize space for the context if we're targeting Apple platforms
+    ma_context* context = (ma_context*)malloc(sizeof(ma_context));
+
+    if (ma_context_init(NULL, 0, &contextConfig, context) != MA_SUCCESS)
+    {
+        free(context);
+        context = nullptr;
+    }
+#else
+    ma_context* context = nullptr;
+#endif
+
+    ma_engine_config engineConfig = ma_engine_config_init();
+    engineConfig.channels = numChannels;
+    engineConfig.sampleRate = sampleRate;
+
+#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_MACCATALYST || TARGET_OS_IPHONE) &&                      \
+    !defined(MA_NO_DEVICE_IO)
+    if (context != nullptr)
+    {
+        engineConfig.pContext = context;
+    }
+#endif
+
+#ifdef EXTERNAL_RIVE_AUDIO_ENGINE
+    engineConfig.noDevice = MA_TRUE;
+#endif
+
+    ma_engine* engine = new ma_engine();
+
+    if (ma_engine_init(&engineConfig, engine) != MA_SUCCESS)
+    {
+#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_MACCATALYST || TARGET_OS_IPHONE) &&                      \
+    !defined(MA_NO_DEVICE_IO)
+        if (context != nullptr)
+        {
+            ma_context_uninit(context);
+            free(context);
+            context = nullptr;
+        }
+#endif
+        fprintf(stderr, "AudioEngine::Make - failed to init engine\n");
+        delete engine;
+        return nullptr;
+    }
+
+    return rcp<AudioEngine>(new AudioEngine(engine, context));
+}
+
+uint32_t AudioEngine::channels() const { return ma_engine_get_channels(m_engine); }
+uint32_t AudioEngine::sampleRate() const { return ma_engine_get_sample_rate(m_engine); }
+
+AudioEngine::AudioEngine(ma_engine* engine, ma_context* context) :
+    m_device(ma_engine_get_device(engine)), m_engine(engine), m_context(context)
+{}
+
+rcp<AudioSound> AudioEngine::play(rcp<AudioSource> source,
+                                  uint64_t startTime,
+                                  uint64_t endTime,
+                                  uint64_t soundStartTime,
+                                  Artboard* artboard)
+{
+    if (endTime != 0 && startTime >= endTime)
+    {
+        // Requested to stop sound before start.
+        return nullptr;
+    }
+
+    std::unique_lock<std::mutex> lock(m_mutex);
+    // We have to dispose completed sounds out of the completed callback. So we
+    // do it on next play or at destruct.
+    for (auto sound : m_completedSounds)
+    {
+        sound->dispose();
+    }
+    m_completedSounds.clear();
+
+    rcp<AudioSound> audioSound = rcp<AudioSound>(new AudioSound(this, source, artboard));
+    if (source->isBuffered())
+    {
+        rive::Span<float> samples = source->bufferedSamples();
+        ma_uint64 sizeInFrames = samples.size() / source->channels();
+        if (endTime != 0)
+        {
+            float durationSeconds = (soundStartTime + endTime - startTime) / (float)sampleRate();
+            ma_uint64 clippedFrames = (ma_uint64)std::round(durationSeconds * source->sampleRate());
+            if (clippedFrames < sizeInFrames)
+            {
+                sizeInFrames = clippedFrames;
+            }
+        }
+        ma_audio_buffer_config config = ma_audio_buffer_config_init(ma_format_f32,
+                                                                    source->channels(),
+                                                                    sizeInFrames,
+                                                                    (const void*)samples.data(),
+                                                                    nullptr);
+        if (ma_audio_buffer_init(&config, audioSound->buffer()) != MA_SUCCESS)
+        {
+            fprintf(stderr, "AudioSource::play - Failed to initialize audio buffer.\n");
+            return nullptr;
+        }
+        if (ma_sound_init_from_data_source(m_engine,
+                                           audioSound->buffer(),
+                                           MA_SOUND_FLAG_NO_PITCH | MA_SOUND_FLAG_NO_SPATIALIZATION,
+                                           nullptr,
+                                           audioSound->sound()) != MA_SUCCESS)
+        {
+            return nullptr;
+        }
+    }
+    else
+    {
+        // We wrapped the miniaudio decoder with a custom data source "Clipped
+        // Decoder" which lets us ensure that the end callback for the sound is
+        // called when we reach the end of the clip. This won't happen when
+        // using ma_sound_set_stop_time_in_pcm_frames(audioSound->sound(),
+        // endTime); as this keeps the sound playing/ready to fade back in.
+        auto clip = audioSound->clippedDecoder();
+        ma_decoder_config config = ma_decoder_config_init(ma_format_f32, channels(), sampleRate());
+        auto sourceBytes = source->bytes();
+        if (ma_decoder_init_memory(sourceBytes.data(),
+                                   sourceBytes.size(),
+                                   &config,
+                                   &clip->decoder) != MA_SUCCESS)
+        {
+            fprintf(stderr, "AudioSource::play - Failed to initialize decoder.\n");
+            return nullptr;
+        }
+        clip->frameCursor = 0;
+        clip->endFrame = endTime == 0 ? std::numeric_limits<uint64_t>::max()
+                                      : soundStartTime + endTime - startTime;
+        ma_data_source_config baseConfig = ma_data_source_config_init();
+        baseConfig.vtable = &g_ma_end_clipped_decoder_vtable;
+        if (ma_data_source_init(&baseConfig, &clip->base) != MA_SUCCESS)
+        {
+            return nullptr;
+        }
+
+        if (ma_sound_init_from_data_source(m_engine,
+                                           audioSound->clippedDecoder(),
+                                           MA_SOUND_FLAG_NO_PITCH | MA_SOUND_FLAG_NO_SPATIALIZATION,
+                                           nullptr,
+                                           audioSound->sound()) != MA_SUCCESS)
+        {
+            return nullptr;
+        }
+    }
+
+    if (soundStartTime != 0)
+    {
+        audioSound->seek(soundStartTime);
+    }
+
+    ma_sound_set_end_callback(audioSound->sound(), SoundCompleted, audioSound.get());
+
+    if (startTime != 0)
+    {
+        ma_sound_set_start_time_in_pcm_frames(audioSound->sound(), startTime);
+    }
+#ifdef WITH_RIVE_AUDIO_TOOLS
+    if (m_levelMonitor != nullptr)
+    {
+        ma_node_attach_output_bus(audioSound->sound(), 0, m_levelMonitor, 0);
+    }
+#endif
+    if (ma_sound_start(audioSound->sound()) != MA_SUCCESS)
+    {
+        fprintf(stderr, "AudioSource::play - failed to start sound\n");
+        return nullptr;
+    }
+
+    if (m_playingSoundsHead != nullptr)
+    {
+        m_playingSoundsHead->m_prevPlaying = audioSound;
+    }
+    audioSound->m_nextPlaying = m_playingSoundsHead;
+    m_playingSoundsHead = audioSound;
+
+    return audioSound;
+}
+
+#ifdef TESTING
+size_t AudioEngine::playingSoundCount()
+{
+    std::unique_lock<std::mutex> lock(m_mutex);
+    size_t count = 0;
+    auto sound = m_playingSoundsHead;
+    while (sound != nullptr)
+    {
+        count++;
+        sound = sound->m_nextPlaying;
+    }
+
+    return count;
+}
+#endif
+
+void AudioEngine::stop(Artboard* artboard)
+{
+    std::unique_lock<std::mutex> lock(m_mutex);
+    auto sound = m_playingSoundsHead;
+    while (sound != nullptr)
+    {
+        auto next = sound->m_nextPlaying;
+        if (sound->m_artboard == artboard)
+        {
+            sound->stop();
+            m_completedSounds.push_back(sound);
+            unlinkSound(sound);
+        }
+        sound = next;
+    }
+}
+
+AudioEngine::~AudioEngine()
+{
+    auto sound = m_playingSoundsHead;
+    while (sound != nullptr)
+    {
+        sound->dispose();
+
+        auto next = sound->m_nextPlaying;
+        sound->m_nextPlaying = nullptr;
+        sound->m_prevPlaying = nullptr;
+        sound = next;
+    }
+
+    for (auto sound : m_completedSounds)
+    {
+        sound->dispose();
+    }
+    m_completedSounds.clear();
+
+#if (TARGET_IPHONE_SIMULATOR || TARGET_OS_MACCATALYST || TARGET_OS_IPHONE) &&                      \
+    !defined(MA_NO_DEVICE_IO)
+    // m_context is only set when Core Audio is available
+    if (m_context != nullptr)
+    {
+        ma_context_uninit(m_context);
+        free(m_context);
+        m_context = nullptr;
+    }
+#endif
+
+    ma_engine_uninit(m_engine);
+    delete m_engine;
+
+#ifdef WITH_RIVE_AUDIO_TOOLS
+    if (m_levelMonitor != nullptr)
+    {
+        ma_node_uninit(&m_levelMonitor->base, nullptr);
+        delete m_levelMonitor;
+    }
+
+#endif
+}
+
+uint64_t AudioEngine::timeInFrames()
+{
+    return (uint64_t)ma_engine_get_time_in_pcm_frames(m_engine);
+}
+
+static rcp<AudioEngine> m_runtimeAudioEngine;
+rcp<AudioEngine> AudioEngine::RuntimeEngine(bool makeWhenNecessary)
+{
+    if (!makeWhenNecessary)
+    {
+        return m_runtimeAudioEngine;
+    }
+    else if (m_runtimeAudioEngine == nullptr)
+    {
+        m_runtimeAudioEngine = AudioEngine::Make(defaultNumChannels, defaultSampleRate);
+    }
+    return m_runtimeAudioEngine;
+}
+
+#ifdef EXTERNAL_RIVE_AUDIO_ENGINE
+bool AudioEngine::readAudioFrames(float* frames, uint64_t numFrames, uint64_t* framesRead)
+{
+    return ma_engine_read_pcm_frames(m_engine,
+                                     (void*)frames,
+                                     (ma_uint64)numFrames,
+                                     (ma_uint64*)framesRead) == MA_SUCCESS;
+}
+bool AudioEngine::sumAudioFrames(float* frames, uint64_t numFrames)
+{
+    size_t numChannels = (size_t)channels();
+    size_t count = (size_t)numFrames * numChannels;
+
+    if (m_readFrames.size() < count)
+    {
+        m_readFrames.resize(count);
+    }
+    ma_uint64 framesRead = 0;
+    if (ma_engine_read_pcm_frames(m_engine,
+                                  (void*)m_readFrames.data(),
+                                  (ma_uint64)numFrames,
+                                  &framesRead) != MA_SUCCESS)
+    {
+        return false;
+    }
+
+    count = framesRead * numChannels;
+
+    const size_t alignedCount = count - count % 4;
+    float* src = m_readFrames.data();
+    float* dst = frames;
+    float* srcEnd = src + alignedCount;
+
+    while (src != srcEnd)
+    {
+        float4 sum = simd::load4f(src) + simd::load4f(dst);
+        simd::store(dst, sum);
+
+        src += 4;
+        dst += 4;
+    }
+    for (size_t i = alignedCount; i < count; i++)
+    {
+        frames[i] += m_readFrames[i];
+    }
+    return true;
+}
+#endif
+
+#endif
\ No newline at end of file
diff --git a/src/audio/audio_engine.m b/src/audio/audio_engine.m
new file mode 100644
index 0000000..f272bea
--- /dev/null
+++ b/src/audio/audio_engine.m
@@ -0,0 +1,4 @@
+#ifdef WITH_RIVE_AUDIO
+#define MINIAUDIO_IMPLEMENTATION
+#include "miniaudio.h"
+#endif
\ No newline at end of file
diff --git a/src/audio/audio_reader.cpp b/src/audio/audio_reader.cpp
new file mode 100644
index 0000000..8451b3c
--- /dev/null
+++ b/src/audio/audio_reader.cpp
@@ -0,0 +1,46 @@
+#ifdef WITH_RIVE_AUDIO
+#include "rive/audio/audio_reader.hpp"
+#include "rive/audio/audio_source.hpp"
+#include "rive/audio/audio_engine.hpp"
+
+using namespace rive;
+
+AudioReader::AudioReader(rcp<AudioSource> audioSource, uint32_t channels) :
+    m_source(std::move(audioSource)), m_channels(channels), m_decoder({})
+{}
+
+AudioReader::~AudioReader() { ma_decoder_uninit(&m_decoder); }
+
+uint32_t AudioReader::channels() const { return m_channels; }
+uint32_t AudioReader::sampleRate() const { return m_decoder.outputSampleRate; }
+ma_decoder* AudioReader::decoder() { return &m_decoder; }
+
+uint64_t AudioReader::lengthInFrames()
+{
+    ma_uint64 length;
+
+    ma_result result = ma_data_source_get_length_in_pcm_frames(&m_decoder, &length);
+    if (result != MA_SUCCESS)
+    {
+        fprintf(stderr,
+                "AudioReader::lengthInFrames - audioSourceLength failed to determine length\n");
+        return 0;
+    }
+    return (uint64_t)length;
+}
+
+Span<float> AudioReader::read(uint64_t frameCount)
+{
+    m_readBuffer.resize(frameCount * m_channels);
+
+    ma_uint64 framesRead;
+    if (ma_data_source_read_pcm_frames(&m_decoder,
+                                       m_readBuffer.data(),
+                                       (ma_uint64)frameCount,
+                                       &framesRead) != MA_SUCCESS)
+    {
+        return Span<float>(nullptr, 0);
+    }
+    return Span<float>(m_readBuffer.data(), framesRead * m_channels);
+}
+#endif
\ No newline at end of file
diff --git a/src/audio/audio_sound.cpp b/src/audio/audio_sound.cpp
new file mode 100644
index 0000000..052d178
--- /dev/null
+++ b/src/audio/audio_sound.cpp
@@ -0,0 +1,70 @@
+#ifdef WITH_RIVE_AUDIO
+#include "rive/audio/audio_sound.hpp"
+#include "rive/audio/audio_engine.hpp"
+#include "rive/audio/audio_reader.hpp"
+#include "rive/audio/audio_source.hpp"
+
+using namespace rive;
+
+AudioSound::AudioSound(AudioEngine* engine, rcp<AudioSource> source, Artboard* artboard) :
+    m_decoder({}),
+    m_buffer({}),
+    m_sound({}),
+    m_source(std::move(source)),
+    m_isDisposed(false),
+    m_engine(engine),
+    m_artboard(artboard)
+{}
+
+void AudioSound::dispose()
+{
+    if (m_isDisposed)
+    {
+        return;
+    }
+    m_isDisposed = true;
+    ma_sound_uninit(&m_sound);
+    ma_decoder_uninit(&m_decoder.decoder);
+    ma_audio_buffer_uninit(&m_buffer);
+}
+
+float AudioSound::volume() { return ma_sound_get_volume(&m_sound); }
+void AudioSound::volume(float value) { ma_sound_set_volume(&m_sound, value); }
+
+bool AudioSound::completed() const
+{
+    if (m_isDisposed)
+    {
+        return true;
+    }
+    return (bool)ma_sound_at_end(&m_sound);
+}
+
+AudioSound::~AudioSound() { dispose(); }
+
+void AudioSound::stop(uint64_t fadeTimeInFrames)
+{
+    if (m_isDisposed)
+    {
+        return;
+    }
+    if (fadeTimeInFrames == 0)
+    {
+        ma_sound_stop(&m_sound);
+    }
+    else
+    {
+        ma_sound_stop_with_fade_in_pcm_frames(&m_sound, fadeTimeInFrames);
+    }
+}
+
+bool AudioSound::seek(uint64_t timeInFrames)
+{
+    if (m_isDisposed)
+    {
+        return false;
+    }
+    return ma_sound_seek_to_pcm_frame(&m_sound, (ma_uint64)timeInFrames) == MA_SUCCESS;
+}
+
+#endif
\ No newline at end of file
diff --git a/src/audio/audio_source.cpp b/src/audio/audio_source.cpp
new file mode 100644
index 0000000..8701d9c
--- /dev/null
+++ b/src/audio/audio_source.cpp
@@ -0,0 +1,162 @@
+#include "rive/audio/audio_source.hpp"
+#ifdef WITH_RIVE_AUDIO
+#include "rive/audio/audio_engine.hpp"
+#include "rive/audio/audio_sound.hpp"
+#include "rive/audio/audio_reader.hpp"
+#include "rive/audio/audio_reader.hpp"
+#endif
+
+using namespace rive;
+
+#ifdef WITH_RIVE_AUDIO
+AudioSource::AudioSource(rive::Span<float> samples, uint32_t numChannels, uint32_t sampleRate) :
+    m_isBuffered(true),
+    m_channels(numChannels),
+    m_sampleRate(sampleRate),
+    m_ownedBytes((uint8_t*)samples.data(), samples.size() * sizeof(float))
+{
+    assert(numChannels != 0);
+    assert(sampleRate != 0);
+}
+
+AudioSource::AudioSource(rive::Span<uint8_t> fileBytes) :
+    m_isBuffered(false), m_channels(0), m_sampleRate(0), m_fileBytes(fileBytes)
+{}
+
+AudioSource::AudioSource(rive::SimpleArray<uint8_t> fileBytes) :
+    m_isBuffered(false),
+    m_channels(0),
+    m_sampleRate(0),
+    m_fileBytes(fileBytes.data(), fileBytes.size()),
+    m_ownedBytes(std::move(fileBytes))
+{}
+
+const rive::Span<float> AudioSource::bufferedSamples() const
+{
+    assert(m_isBuffered);
+    return rive::Span<float>((float*)m_ownedBytes.data(), m_ownedBytes.size() / sizeof(float));
+}
+
+class AudioSourceDecoder
+{
+public:
+    AudioSourceDecoder(rive::Span<uint8_t> fileBytes) : m_decoder({})
+    {
+        ma_decoder_config config = ma_decoder_config_init(ma_format_f32, 0, 0);
+        if (ma_decoder_init_memory(fileBytes.data(), fileBytes.size(), &config, &m_decoder) !=
+            MA_SUCCESS)
+        {
+            fprintf(stderr, "AudioSourceDecoder - Failed to initialize decoder.\n");
+        }
+    }
+
+    ~AudioSourceDecoder() { ma_decoder_uninit(&m_decoder); }
+
+    uint32_t channels() { return (uint32_t)m_decoder.outputChannels; }
+
+    uint32_t sampleRate() { return (uint32_t)m_decoder.outputSampleRate; }
+
+private:
+    ma_decoder m_decoder;
+};
+
+uint32_t AudioSource::channels()
+{
+    if (m_channels != 0)
+    {
+        return m_channels;
+    }
+    AudioSourceDecoder audioDecoder(m_fileBytes);
+    return m_channels = audioDecoder.channels();
+}
+
+uint32_t AudioSource::sampleRate()
+{
+    if (m_sampleRate != 0)
+    {
+        return m_sampleRate;
+    }
+    AudioSourceDecoder audioDecoder(m_fileBytes);
+    return m_sampleRate = audioDecoder.sampleRate();
+}
+
+AudioFormat AudioSource::format() const
+{
+    if (m_isBuffered)
+    {
+        return AudioFormat::buffered;
+    }
+    ma_decoder decoder;
+    ma_decoder_config config = ma_decoder_config_init(ma_format_f32, 0, 0);
+    if (ma_decoder_init_memory(m_fileBytes.data(), m_fileBytes.size(), &config, &decoder) !=
+        MA_SUCCESS)
+    {
+        fprintf(stderr, "AudioSource::format - Failed to initialize decoder.\n");
+        return AudioFormat::unknown;
+    }
+    ma_encoding_format encodingFormat;
+
+    AudioFormat format = AudioFormat::unknown;
+    if (ma_decoder_get_encoding_format(&decoder, &encodingFormat) == MA_SUCCESS)
+    {
+        switch (encodingFormat)
+        {
+            case ma_encoding_format_mp3:
+                format = AudioFormat::mp3;
+                break;
+            case ma_encoding_format_wav:
+                format = AudioFormat::wav;
+                break;
+            case ma_encoding_format_vorbis:
+                format = AudioFormat::vorbis;
+                break;
+            case ma_encoding_format_flac:
+                format = AudioFormat::flac;
+                break;
+            default:
+                format = AudioFormat::unknown;
+                break;
+        }
+    }
+
+    ma_decoder_uninit(&decoder);
+
+    return format;
+}
+
+rcp<AudioReader> AudioSource::makeReader(uint32_t numChannels, uint32_t sampleRate)
+{
+    if (m_isBuffered)
+    {
+        return nullptr;
+    }
+
+    rive::rcp<rive::AudioSource> rcSource = rive::rcp<rive::AudioSource>(this);
+    rcSource->ref();
+    auto reader = rcp<AudioReader>(new AudioReader(rcSource, numChannels));
+
+    ma_decoder_config config = ma_decoder_config_init(ma_format_f32, numChannels, sampleRate);
+
+    if (ma_decoder_init_memory(m_fileBytes.data(),
+                               m_fileBytes.size(),
+                               &config,
+                               reader->decoder()) != MA_SUCCESS)
+    {
+        fprintf(stderr, "AudioSource::makeReader - Failed to initialize decoder.\n");
+        return nullptr;
+    }
+
+    return reader;
+}
+#else
+AudioSource::AudioSource(rive::Span<uint8_t> fileBytes) {}
+AudioSource::AudioSource(rive::SimpleArray<uint8_t> fileBytes) {}
+AudioSource::AudioSource(rive::Span<float> samples, uint32_t numChannels, uint32_t sampleRate) {}
+uint32_t AudioSource::channels() { return 0; }
+uint32_t AudioSource::sampleRate() { return 0; }
+AudioFormat AudioSource::format() const { return AudioFormat::unknown; }
+const rive::Span<float> AudioSource::bufferedSamples() const
+{
+    return rive::Span<float>(nullptr, 0);
+}
+#endif
\ No newline at end of file
diff --git a/src/audio_event.cpp b/src/audio_event.cpp
new file mode 100644
index 0000000..53c9812
--- /dev/null
+++ b/src/audio_event.cpp
@@ -0,0 +1,82 @@
+#include "rive/audio_event.hpp"
+#include "rive/assets/audio_asset.hpp"
+#include "rive/audio/audio_engine.hpp"
+#include "rive/audio/audio_sound.hpp"
+#include "rive/artboard.hpp"
+
+using namespace rive;
+
+void AudioEvent::play()
+{
+#ifdef WITH_RIVE_AUDIO
+    auto audioAsset = (AudioAsset*)m_fileAsset;
+    if (audioAsset == nullptr)
+    {
+        return;
+    }
+    auto audioSource = audioAsset->audioSource();
+    if (audioSource == nullptr)
+    {
+        return;
+    }
+
+    auto volume = audioAsset->volume() * artboard()->volume();
+    if (volume <= 0.0f)
+    {
+        return;
+    }
+
+    auto engine =
+#ifdef EXTERNAL_RIVE_AUDIO_ENGINE
+        artboard()->audioEngine() != nullptr ? artboard()->audioEngine() :
+#endif
+                                             AudioEngine::RuntimeEngine();
+
+    auto sound = engine->play(audioSource, engine->timeInFrames(), 0, 0, artboard());
+
+    if (volume != 1.0f)
+    {
+        sound->volume(volume);
+    }
+#endif
+}
+
+void AudioEvent::trigger(const CallbackData& value)
+{
+    Super::trigger(value);
+    if (!value.context()->playsAudio())
+    {
+        // Context won't play audio, we'll do it ourselves.
+        play();
+    }
+}
+
+StatusCode AudioEvent::import(ImportStack& importStack)
+{
+    auto result = registerReferencer(importStack);
+    if (result != StatusCode::Ok)
+    {
+        return result;
+    }
+    return Super::import(importStack);
+}
+
+void AudioEvent::setAsset(FileAsset* asset)
+{
+    if (asset->is<AudioAsset>())
+    {
+        FileAssetReferencer::setAsset(asset);
+    }
+}
+
+Core* AudioEvent::clone() const
+{
+    AudioEvent* twin = AudioEventBase::clone()->as<AudioEvent>();
+    if (m_fileAsset != nullptr)
+    {
+        twin->setAsset(m_fileAsset);
+    }
+    return twin;
+}
+
+uint32_t AudioEvent::assetId() { return AudioEventBase::assetId(); }
\ No newline at end of file
diff --git a/src/bones/bone.cpp b/src/bones/bone.cpp
new file mode 100644
index 0000000..0cb0edc
--- /dev/null
+++ b/src/bones/bone.cpp
@@ -0,0 +1,39 @@
+#include "rive/bones/bone.hpp"
+#include "rive/math/vec2d.hpp"
+#include <algorithm>
+
+using namespace rive;
+
+void Bone::addChildBone(Bone* bone) { m_ChildBones.push_back(bone); }
+
+StatusCode Bone::onAddedClean(CoreContext* context)
+{
+    Super::onAddedClean(context);
+    if (!parent()->is<Bone>())
+    {
+        return StatusCode::MissingObject;
+    }
+    parent()->as<Bone>()->addChildBone(this);
+    return StatusCode::Ok;
+}
+
+void Bone::lengthChanged()
+{
+    for (auto bone : m_ChildBones)
+    {
+        bone->markTransformDirty();
+    }
+}
+
+float Bone::x() const { return parent()->as<Bone>()->length(); }
+
+float Bone::y() const { return 0.0f; }
+
+Vec2D Bone::tipWorldTranslation() const { return worldTransform() * Vec2D(length(), 0); }
+
+void Bone::addPeerConstraint(Constraint* peer)
+{
+    assert(std::find(m_PeerConstraints.begin(), m_PeerConstraints.end(), peer) ==
+           m_PeerConstraints.end());
+    m_PeerConstraints.push_back(peer);
+}
diff --git a/src/bones/root_bone.cpp b/src/bones/root_bone.cpp
new file mode 100644
index 0000000..ff592f9
--- /dev/null
+++ b/src/bones/root_bone.cpp
@@ -0,0 +1,15 @@
+#include "rive/bones/root_bone.hpp"
+
+using namespace rive;
+
+StatusCode RootBone::onAddedClean(CoreContext* context)
+{
+    // Intentionally doesn't call Super(Bone)::onAddedClean and goes straight to
+    // the super.super TransformComponent as that assumes the parent must be a
+    // Bone while a root bone is a special case Bone that can be parented to
+    // other TransformComponents.
+    return TransformComponent::onAddedClean(context);
+}
+
+void RootBone::xChanged() { markTransformDirty(); }
+void RootBone::yChanged() { markTransformDirty(); }
\ No newline at end of file
diff --git a/src/bones/skin.cpp b/src/bones/skin.cpp
new file mode 100644
index 0000000..2041876
--- /dev/null
+++ b/src/bones/skin.cpp
@@ -0,0 +1,94 @@
+#include "rive/bones/skin.hpp"
+#include "rive/bones/bone.hpp"
+#include "rive/bones/skinnable.hpp"
+#include "rive/bones/tendon.hpp"
+#include "rive/shapes/vertex.hpp"
+#include "rive/shapes/path_vertex.hpp"
+#include "rive/constraints/constraint.hpp"
+
+using namespace rive;
+
+Skin::~Skin() { delete[] m_BoneTransforms; }
+
+StatusCode Skin::onAddedDirty(CoreContext* context)
+{
+    StatusCode code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+    m_WorldTransform[0] = xx();
+    m_WorldTransform[1] = xy();
+    m_WorldTransform[2] = yx();
+    m_WorldTransform[3] = yy();
+    m_WorldTransform[4] = tx();
+    m_WorldTransform[5] = ty();
+
+    m_Skinnable = Skinnable::from(parent());
+    if (m_Skinnable == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+
+    m_Skinnable->skin(this);
+
+    return StatusCode::Ok;
+}
+
+void Skin::update(ComponentDirt value)
+{
+    int bidx = 6;
+    for (auto tendon : m_Tendons)
+    {
+        auto world = tendon->bone()->worldTransform() * tendon->inverseBind();
+        m_BoneTransforms[bidx++] = world[0];
+        m_BoneTransforms[bidx++] = world[1];
+        m_BoneTransforms[bidx++] = world[2];
+        m_BoneTransforms[bidx++] = world[3];
+        m_BoneTransforms[bidx++] = world[4];
+        m_BoneTransforms[bidx++] = world[5];
+    }
+}
+
+void Skin::buildDependencies()
+{
+    // depend on bones from tendons
+    for (auto tendon : m_Tendons)
+    {
+        auto bone = tendon->bone();
+        bone->addDependent(this);
+        for (auto constraint : bone->peerConstraints())
+        {
+            constraint->parent()->addDependent(this);
+        }
+    }
+
+    // Make sure no-one is calling this twice.
+    assert(m_BoneTransforms == nullptr);
+    // We can now init the bone buffer.
+    auto size = (m_Tendons.size() + 1) * 6;
+    m_BoneTransforms = new float[size];
+    m_BoneTransforms[0] = 1;
+    m_BoneTransforms[1] = 0;
+    m_BoneTransforms[2] = 0;
+    m_BoneTransforms[3] = 1;
+    m_BoneTransforms[4] = 0;
+    m_BoneTransforms[5] = 0;
+}
+
+void Skin::deform(Span<Vertex*> vertices)
+{
+    for (auto vertex : vertices)
+    {
+        vertex->deform(m_WorldTransform, m_BoneTransforms);
+    }
+}
+void Skin::addTendon(Tendon* tendon) { m_Tendons.push_back(tendon); }
+
+void Skin::onDirty(ComponentDirt dirt)
+{
+    if (m_Skinnable != nullptr)
+    {
+        m_Skinnable->markSkinDirty();
+    }
+}
diff --git a/src/bones/skinnable.cpp b/src/bones/skinnable.cpp
new file mode 100644
index 0000000..74b33cb
--- /dev/null
+++ b/src/bones/skinnable.cpp
@@ -0,0 +1,19 @@
+#include "rive/bones/skinnable.hpp"
+#include "rive/shapes/points_path.hpp"
+#include "rive/shapes/mesh.hpp"
+
+using namespace rive;
+
+Skinnable* Skinnable::from(Component* component)
+{
+    switch (component->coreType())
+    {
+        case PointsPath::typeKey:
+            return component->as<PointsPath>();
+        case Mesh::typeKey:
+            return component->as<Mesh>();
+    }
+    return nullptr;
+}
+
+void Skinnable::skin(Skin* skin) { m_Skin = skin; }
\ No newline at end of file
diff --git a/src/bones/tendon.cpp b/src/bones/tendon.cpp
new file mode 100644
index 0000000..279b6c2
--- /dev/null
+++ b/src/bones/tendon.cpp
@@ -0,0 +1,51 @@
+#include "rive/bones/tendon.hpp"
+#include "rive/bones/bone.hpp"
+#include "rive/bones/skin.hpp"
+#include "rive/core_context.hpp"
+
+using namespace rive;
+
+StatusCode Tendon::onAddedDirty(CoreContext* context)
+{
+    Mat2D bind;
+    bind[0] = xx();
+    bind[1] = xy();
+    bind[2] = yx();
+    bind[3] = yy();
+    bind[4] = tx();
+    bind[5] = ty();
+
+    // Internally, this inversion can fail because of a division by 0.
+    // 'm_InverseBind' will default to an identity matrix. Although
+    // this can be treated as undefined behavior, the editor lets it go
+    // through, and nothing really breaks, it just can look odd. So we
+    // allow the runtime to behave in the same way and return StatusCode::Ok.
+    bind.invert(&m_InverseBind);
+
+    StatusCode code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+    auto coreObject = context->resolve(boneId());
+    if (coreObject == nullptr || !coreObject->is<Bone>())
+    {
+        return StatusCode::MissingObject;
+    }
+
+    m_Bone = static_cast<Bone*>(coreObject);
+
+    return StatusCode::Ok;
+}
+
+StatusCode Tendon::onAddedClean(CoreContext* context)
+{
+    if (!parent()->is<Skin>())
+    {
+        return StatusCode::MissingObject;
+    }
+
+    parent()->as<Skin>()->addTendon(this);
+
+    return StatusCode::Ok;
+}
diff --git a/src/bones/weight.cpp b/src/bones/weight.cpp
new file mode 100644
index 0000000..642a6e4
--- /dev/null
+++ b/src/bones/weight.cpp
@@ -0,0 +1,56 @@
+#include "rive/bones/weight.hpp"
+#include "rive/container_component.hpp"
+#include "rive/shapes/vertex.hpp"
+
+using namespace rive;
+
+StatusCode Weight::onAddedDirty(CoreContext* context)
+{
+    StatusCode code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+    if (!parent()->is<Vertex>())
+    {
+        return StatusCode::MissingObject;
+    }
+
+    parent()->as<Vertex>()->weight(this);
+
+    return StatusCode::Ok;
+}
+
+static int encodedWeightValue(unsigned int index, unsigned int data)
+{
+    return (data >> (index * 8)) & 0xFF;
+}
+
+Vec2D Weight::deform(Vec2D inPoint,
+                     unsigned int indices,
+                     unsigned int weights,
+                     const Mat2D& world,
+                     const float* boneTransforms)
+{
+    float xx = 0, xy = 0, yx = 0, yy = 0, tx = 0, ty = 0;
+    for (int i = 0; i < 4; i++)
+    {
+        int weight = encodedWeightValue(i, weights);
+        if (weight == 0)
+        {
+            continue;
+        }
+
+        float normalizedWeight = weight / 255.0f;
+        int index = encodedWeightValue(i, indices);
+        int startBoneTransformIndex = index * 6;
+        xx += boneTransforms[startBoneTransformIndex++] * normalizedWeight;
+        xy += boneTransforms[startBoneTransformIndex++] * normalizedWeight;
+        yx += boneTransforms[startBoneTransformIndex++] * normalizedWeight;
+        yy += boneTransforms[startBoneTransformIndex++] * normalizedWeight;
+        tx += boneTransforms[startBoneTransformIndex++] * normalizedWeight;
+        ty += boneTransforms[startBoneTransformIndex++] * normalizedWeight;
+    }
+
+    return Mat2D(xx, xy, yx, yy, tx, ty) * (world * inPoint);
+}
diff --git a/src/component.cpp b/src/component.cpp
new file mode 100644
index 0000000..8765b9e
--- /dev/null
+++ b/src/component.cpp
@@ -0,0 +1,92 @@
+#include "rive/component.hpp"
+#include "rive/artboard.hpp"
+#include "rive/container_component.hpp"
+#include "rive/core_context.hpp"
+#include "rive/importers/artboard_importer.hpp"
+#include "rive/importers/import_stack.hpp"
+#include <algorithm>
+
+using namespace rive;
+
+StatusCode Component::onAddedDirty(CoreContext* context)
+{
+    m_Artboard = static_cast<Artboard*>(context);
+    m_DependencyHelper.dependecyRoot(m_Artboard);
+    if (this == m_Artboard)
+    {
+        // We're the artboard, don't parent to ourselves.
+        return StatusCode::Ok;
+    }
+    auto coreObject = context->resolve(parentId());
+    if (coreObject == nullptr || !coreObject->is<ContainerComponent>())
+    {
+        return StatusCode::MissingObject;
+    }
+    m_Parent = static_cast<ContainerComponent*>(coreObject);
+    m_Parent->addChild(this);
+    return StatusCode::Ok;
+}
+
+void Component::addDependent(Component* component) { m_DependencyHelper.addDependent(component); }
+
+bool Component::addDirt(ComponentDirt value, bool recurse)
+{
+    if ((m_Dirt & value) == value)
+    {
+        // Already marked.
+        return false;
+    }
+
+    // Make sure dirt is set before calling anything that can set more dirt.
+    m_Dirt |= value;
+
+    onDirty(m_Dirt);
+
+    m_DependencyHelper.onComponentDirty(this);
+
+    if (!recurse)
+    {
+        return true;
+    }
+
+    m_DependencyHelper.addDirt(value);
+    return true;
+}
+
+StatusCode Component::import(ImportStack& importStack)
+{
+    if (is<Artboard>())
+    {
+        // Artboards are always their first object.
+        assert(as<Artboard>()->objects().size() == 0);
+        as<Artboard>()->addObject(this);
+        return Super::import(importStack);
+    }
+
+    auto artboardImporter = importStack.latest<ArtboardImporter>(ArtboardBase::typeKey);
+    if (artboardImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    artboardImporter->addComponent(this);
+    return Super::import(importStack);
+}
+
+bool Component::collapse(bool value)
+{
+    if (isCollapsed() == value)
+    {
+        return false;
+    }
+    if (value)
+    {
+        m_Dirt |= ComponentDirt::Collapsed;
+    }
+    else
+    {
+        m_Dirt &= ~ComponentDirt::Collapsed;
+    }
+    onDirty(m_Dirt);
+    m_DependencyHelper.onComponentDirty(this);
+    return true;
+}
\ No newline at end of file
diff --git a/src/constraints/constraint.cpp b/src/constraints/constraint.cpp
new file mode 100644
index 0000000..a8c5e60
--- /dev/null
+++ b/src/constraints/constraint.cpp
@@ -0,0 +1,47 @@
+#include "rive/constraints/constraint.hpp"
+#include "rive/container_component.hpp"
+#include "rive/transform_component.hpp"
+#include "rive/core_context.hpp"
+#include "rive/math/mat2d.hpp"
+
+using namespace rive;
+
+StatusCode Constraint::onAddedClean(CoreContext* context)
+{
+    if (!parent()->is<TransformComponent>())
+    {
+        return StatusCode::InvalidObject;
+    }
+
+    parent()->as<TransformComponent>()->addConstraint(this);
+
+    return StatusCode::Ok;
+}
+
+void Constraint::markConstraintDirty() { parent()->as<TransformComponent>()->markTransformDirty(); }
+
+void Constraint::strengthChanged() { markConstraintDirty(); }
+
+void Constraint::buildDependencies()
+{
+    Super::buildDependencies();
+    parent()->addDependent(this);
+}
+
+void Constraint::onDirty(ComponentDirt dirt)
+{
+    // Whenever the constraint gets any dirt, make sure to mark the constrained
+    // component dirty.
+    markConstraintDirty();
+}
+
+static Mat2D identity;
+const Mat2D& rive::getParentWorld(const TransformComponent& component)
+{
+    auto parent = component.parent();
+    if (parent->is<WorldTransformComponent>())
+    {
+        return parent->as<WorldTransformComponent>()->worldTransform();
+    }
+    return identity;
+}
\ No newline at end of file
diff --git a/src/constraints/distance_constraint.cpp b/src/constraints/distance_constraint.cpp
new file mode 100644
index 0000000..3ea5e66
--- /dev/null
+++ b/src/constraints/distance_constraint.cpp
@@ -0,0 +1,61 @@
+#include "rive/constraints/distance_constraint.hpp"
+#include "rive/bones/bone.hpp"
+#include "rive/artboard.hpp"
+#include <algorithm>
+
+using namespace rive;
+
+enum class Mode
+{
+    Closer = 0,
+    Further = 1,
+    Exact = 2
+};
+
+void DistanceConstraint::constrain(TransformComponent* component)
+{
+    if (m_Target == nullptr || m_Target->isCollapsed())
+    {
+        return;
+    }
+
+    const Vec2D targetTranslation = m_Target->worldTranslation();
+    const Vec2D ourTranslation = component->worldTranslation();
+
+    Vec2D toTarget = ourTranslation - targetTranslation;
+    float currentDistance = toTarget.length();
+
+    switch (static_cast<Mode>(modeValue()))
+    {
+        case Mode::Closer:
+            if (currentDistance < distance())
+            {
+                return;
+            }
+            break;
+        case Mode::Further:
+            if (currentDistance > distance())
+            {
+                return;
+            }
+            break;
+        case Mode::Exact:
+            break;
+    }
+    if (currentDistance < 0.001f)
+    {
+        return;
+    }
+
+    toTarget *= (distance() / currentDistance);
+
+    Mat2D& world = component->mutableWorldTransform();
+    Vec2D position = targetTranslation + toTarget;
+    position = Vec2D::lerp(ourTranslation, position, strength());
+    world[4] = position.x;
+    world[5] = position.y;
+}
+
+void DistanceConstraint::distanceChanged() { markConstraintDirty(); }
+
+void DistanceConstraint::modeValueChanged() { markConstraintDirty(); }
\ No newline at end of file
diff --git a/src/constraints/follow_path_constraint.cpp b/src/constraints/follow_path_constraint.cpp
new file mode 100644
index 0000000..8c74d9a
--- /dev/null
+++ b/src/constraints/follow_path_constraint.cpp
@@ -0,0 +1,199 @@
+#include "rive/artboard.hpp"
+#include "rive/command_path.hpp"
+#include "rive/constraints/follow_path_constraint.hpp"
+#include "rive/factory.hpp"
+#include "rive/math/contour_measure.hpp"
+#include "rive/math/mat2d.hpp"
+#include "rive/math/math_types.hpp"
+#include "rive/shapes/path.hpp"
+#include "rive/shapes/shape.hpp"
+#include "rive/transform_component.hpp"
+#include <algorithm>
+#include <iostream>
+#include <typeinfo>
+
+using namespace rive;
+
+static float positiveMod(float value, float range)
+{
+    assert(range > 0.0f);
+    float v = fmodf(value, range);
+    if (v < 0.0f)
+    {
+        v += range;
+    }
+    return v;
+}
+
+void FollowPathConstraint::distanceChanged() { markConstraintDirty(); }
+void FollowPathConstraint::orientChanged() { markConstraintDirty(); }
+
+const Mat2D FollowPathConstraint::targetTransform() const
+{
+    if (m_Target->is<Shape>() || m_Target->is<Path>())
+    {
+        float totalLength = 0.0f;
+        for (auto contour : m_contours)
+        {
+            totalLength += contour->length();
+        }
+
+        float actualDistance = positiveMod(distance(), 1.0f);
+        if (distance() != 0 && actualDistance == 0)
+        {
+            actualDistance = 1;
+        }
+        float distanceUnits = totalLength * std::min(1.0f, std::max(0.0f, actualDistance));
+        float runningLength = 0;
+        ContourMeasure::PosTan posTan = ContourMeasure::PosTan();
+        for (auto contour : m_contours)
+        {
+            float pathLength = contour->length();
+            if (distanceUnits <= pathLength + runningLength)
+            {
+                posTan = contour->getPosTan(distanceUnits - runningLength);
+                break;
+            }
+            runningLength += pathLength;
+        }
+        Vec2D position = Vec2D(posTan.pos.x, posTan.pos.y);
+        Mat2D transformB = Mat2D(m_Target->worldTransform());
+
+        if (orient())
+        {
+            transformB = Mat2D::fromRotation(std::atan2(posTan.tan.y, posTan.tan.x));
+        }
+        Vec2D offsetPosition = Vec2D();
+        if (offset())
+        {
+            if (parent()->is<TransformComponent>())
+            {
+                Mat2D components = parent()->as<TransformComponent>()->transform();
+                offsetPosition.x = components[4];
+                offsetPosition.y = components[5];
+            }
+        }
+        transformB[4] = position.x + offsetPosition.x;
+        transformB[5] = position.y + offsetPosition.y;
+        return transformB;
+    }
+    else
+    {
+        return m_Target->worldTransform();
+    }
+}
+
+void FollowPathConstraint::constrain(TransformComponent* component)
+{
+    if (m_Target == nullptr || m_Target->isCollapsed())
+    {
+        return;
+    }
+    const Mat2D& transformA = component->worldTransform();
+    Mat2D transformB(targetTransform());
+    if (sourceSpace() == TransformSpace::local)
+    {
+        const Mat2D& targetParentWorld = getParentWorld(*m_Target);
+
+        Mat2D inverse;
+        if (!targetParentWorld.invert(&inverse))
+        {
+            return;
+        }
+        transformB = inverse * transformB;
+    }
+    if (destSpace() == TransformSpace::local)
+    {
+        const Mat2D& targetParentWorld = getParentWorld(*component);
+        transformB = targetParentWorld * transformB;
+    }
+
+    m_ComponentsA = transformA.decompose();
+    m_ComponentsB = transformB.decompose();
+
+    float t = strength();
+    float ti = 1.0f - t;
+
+    if (!orient())
+    {
+        float angleA = std::fmod(m_ComponentsA.rotation(), math::PI * 2);
+        m_ComponentsB.rotation(angleA);
+    }
+    m_ComponentsB.x(m_ComponentsA.x() * ti + m_ComponentsB.x() * t);
+    m_ComponentsB.y(m_ComponentsA.y() * ti + m_ComponentsB.y() * t);
+    m_ComponentsB.scaleX(m_ComponentsA.scaleX());
+    m_ComponentsB.scaleY(m_ComponentsA.scaleY());
+    m_ComponentsB.skew(m_ComponentsA.skew());
+
+    component->mutableWorldTransform() = Mat2D::compose(m_ComponentsB);
+}
+
+void FollowPathConstraint::update(ComponentDirt value)
+{
+    std::vector<Path*> paths;
+    if (m_Target->is<Shape>())
+    {
+        auto shape = m_Target->as<Shape>();
+        for (auto path : shape->paths())
+        {
+            paths.push_back(path);
+        }
+    }
+    else if (m_Target->is<Path>())
+    {
+        paths.push_back(m_Target->as<Path>());
+    }
+    if (paths.size() > 0)
+    {
+        m_rawPath.rewind();
+        m_contours.clear();
+        for (auto path : paths)
+        {
+            m_rawPath.addPath(path->rawPath(), &path->pathTransform());
+        }
+
+        auto measure = ContourMeasureIter(&m_rawPath);
+        for (auto contour = measure.next(); contour != nullptr; contour = measure.next())
+        {
+            m_contours.push_back(contour);
+        }
+    }
+}
+
+StatusCode FollowPathConstraint::onAddedClean(CoreContext* context)
+{
+    if (m_Target != nullptr)
+    {
+        if (m_Target->is<Shape>())
+        {
+            Shape* shape = static_cast<Shape*>(m_Target);
+            shape->addFlags(PathFlags::followPath);
+        }
+        else if (m_Target->is<Path>())
+        {
+            Path* path = static_cast<Path*>(m_Target);
+            path->addFlags(PathFlags::followPath);
+        }
+    }
+    return Super::onAddedClean(context);
+}
+
+void FollowPathConstraint::buildDependencies()
+{
+
+    if (m_Target != nullptr && m_Target->is<Shape>()) // which should never happen
+    {
+        // Follow path should update after the target's path composer
+        Shape* shape = static_cast<Shape*>(m_Target);
+        shape->pathComposer()->addDependent(this);
+    }
+    // ok this appears to be enough to get the inital layout & animations to be working.
+    else if (m_Target != nullptr && m_Target->is<Path>()) // which should never happen
+    {
+        // or do we need to be dependent on the shape still???
+        Path* path = static_cast<Path*>(m_Target);
+        path->addDependent(this);
+    }
+    // The constrained component should update after follow path
+    addDependent(parent());
+}
diff --git a/src/constraints/ik_constraint.cpp b/src/constraints/ik_constraint.cpp
new file mode 100644
index 0000000..c8638f4
--- /dev/null
+++ b/src/constraints/ik_constraint.cpp
@@ -0,0 +1,287 @@
+#include "rive/constraints/ik_constraint.hpp"
+#include "rive/bones/bone.hpp"
+#include "rive/artboard.hpp"
+#include "rive/math/math_types.hpp"
+#include <algorithm>
+
+using namespace rive;
+
+static float atan2(Vec2D v) { return std::atan2(v.y, v.x); }
+
+void IKConstraint::buildDependencies()
+{
+    Super::buildDependencies();
+
+    // IK Constraint needs to depend on the target so that world transform
+    // changes can propagate to the bones (and they can be reset before IK
+    // runs).
+    if (m_Target != nullptr)
+    {
+        m_Target->addDependent(this);
+    }
+}
+
+StatusCode IKConstraint::onAddedClean(CoreContext* context)
+{
+    if (!parent()->is<Bone>())
+    {
+        return StatusCode::InvalidObject;
+    }
+
+    auto boneCount = parentBoneCount();
+    auto bone = parent()->as<Bone>();
+    std::vector<Bone*> bones;
+    bones.push_back(bone);
+    // Get the reverse FK chain of bones (from tip up).
+    while (bone->parent()->is<Bone>() && boneCount > 0)
+    {
+        boneCount--;
+        bone = bone->parent()->as<Bone>();
+        bone->addPeerConstraint(this);
+        bones.push_back(bone);
+    }
+    int numBones = (int)bones.size();
+    m_FkChain.resize(numBones);
+    // Now put them in FK order (top to bottom).
+    int idx = 0;
+    for (auto boneItr = bones.rbegin(); boneItr != bones.rend(); ++boneItr)
+    {
+        BoneChainLink& link = m_FkChain[idx];
+        link.index = idx++;
+        link.bone = *boneItr;
+        link.angle = 0.0f;
+    }
+
+    // Make sure all of the first level children of each bone depend on the
+    // tip (constrainedComponent).
+    auto tip = parent()->as<Bone>();
+
+    auto artboard = static_cast<Artboard*>(context);
+
+    // Find all children of this bone (we don't directly build up
+    // hierarchy at runtime, so we have to traverse everything and check
+    // parents).
+    for (auto core : artboard->objects())
+    {
+        if (core == nullptr || !core->is<TransformComponent>())
+        {
+            continue;
+        }
+        auto transformComponent = core->as<TransformComponent>();
+
+        for (int i = 1; i < numBones; i++)
+        {
+            auto childBone = bones[i];
+            if (transformComponent->parent() == childBone &&
+                std::find(bones.begin(), bones.end(), transformComponent) == bones.end())
+            {
+                tip->addDependent(transformComponent);
+            }
+        }
+    }
+    return Super::onAddedClean(context);
+}
+
+void IKConstraint::markConstraintDirty()
+{
+    Super::markConstraintDirty();
+    // We automatically propagate dirt to the parent constrained bone, but we
+    // also need to make sure the other bones we influence above it rebuild
+    // their transforms.
+    for (int i = 0, length = (int)m_FkChain.size() - 1; i < length; i++)
+    {
+        m_FkChain[i].bone->markTransformDirty();
+    }
+}
+
+void IKConstraint::solve1(BoneChainLink* fk1, const Vec2D& worldTargetTranslation)
+{
+    Mat2D iworld = fk1->parentWorldInverse;
+    Vec2D pA = fk1->bone->worldTranslation();
+    Vec2D pBT(worldTargetTranslation);
+
+    // To target in worldspace
+    const Vec2D toTarget = pBT - pA;
+
+    // Note this is directional, hence not transformMat2d
+    Vec2D toTargetLocal = Vec2D::transformDir(toTarget, iworld);
+    float r = atan2(toTargetLocal);
+
+    constrainRotation(*fk1, r);
+    fk1->angle = r;
+}
+
+void IKConstraint::solve2(BoneChainLink* fk1,
+                          BoneChainLink* fk2,
+                          const Vec2D& worldTargetTranslation)
+{
+    Bone* b1 = fk1->bone;
+    Bone* b2 = fk2->bone;
+    BoneChainLink* firstChild = &(m_FkChain[fk1->index + 1]);
+
+    const Mat2D& iworld = fk1->parentWorldInverse;
+
+    Vec2D pA = b1->worldTranslation();
+    Vec2D pC = firstChild->bone->worldTranslation();
+    Vec2D pB = b2->tipWorldTranslation();
+    Vec2D pBT(worldTargetTranslation);
+
+    pA = iworld * pA;
+    pC = iworld * pC;
+    pB = iworld * pB;
+    pBT = iworld * pBT;
+
+    // http://mathworld.wolfram.com/LawofCosines.html
+    Vec2D av = pB - pC, bv = pC - pA, cv = pBT - pA;
+    float a = av.length(), b = bv.length(), c = cv.length();
+
+    float A = std::acos(std::max(-1.0f, std::min(1.0f, (-a * a + b * b + c * c) / (2.0f * b * c))));
+    float C = std::acos(std::max(-1.0f, std::min(1.0f, (a * a + b * b - c * c) / (2.0f * a * b))));
+
+    float r1, r2;
+    if (b2->parent() != b1)
+    {
+        BoneChainLink& secondChild = m_FkChain[fk1->index + 2];
+
+        const Mat2D& secondChildWorldInverse = secondChild.parentWorldInverse;
+
+        pC = firstChild->bone->worldTranslation();
+        pB = b2->tipWorldTranslation();
+
+        Vec2D avLocal = Vec2D::transformDir(pB - pC, secondChildWorldInverse);
+        float angleCorrection = -atan2(avLocal);
+
+        if (invertDirection())
+        {
+            r1 = atan2(cv) - A;
+            r2 = -C + math::PI + angleCorrection;
+        }
+        else
+        {
+            r1 = A + atan2(cv);
+            r2 = C - math::PI + angleCorrection;
+        }
+    }
+    else if (invertDirection())
+    {
+        r1 = atan2(cv) - A;
+        r2 = -C + math::PI;
+    }
+    else
+    {
+        r1 = A + atan2(cv);
+        r2 = C - math::PI;
+    }
+
+    constrainRotation(*fk1, r1);
+    constrainRotation(*firstChild, r2);
+    if (firstChild != fk2)
+    {
+        Bone* bone = fk2->bone;
+        bone->mutableWorldTransform() = getParentWorld(*bone) * bone->transform();
+    }
+
+    // Simple storage, need this for interpolation.
+    fk1->angle = r1;
+    firstChild->angle = r2;
+}
+
+void IKConstraint::constrainRotation(BoneChainLink& fk, float rotation)
+{
+    Bone* bone = fk.bone;
+    const Mat2D& parentWorld = getParentWorld(*bone);
+    Mat2D& transform = bone->mutableTransform();
+    TransformComponents& c = fk.transformComponents;
+
+    transform = Mat2D::fromRotation(rotation);
+
+    // Translate
+    transform[4] = c.x();
+    transform[5] = c.y();
+    // Scale
+    float scaleX = c.scaleX();
+    float scaleY = c.scaleY();
+    transform[0] *= scaleX;
+    transform[1] *= scaleX;
+    transform[2] *= scaleY;
+    transform[3] *= scaleY;
+
+    // Skew
+    const float skew = c.skew();
+    if (skew != 0.0f)
+    {
+        transform[2] = transform[0] * skew + transform[2];
+        transform[3] = transform[1] * skew + transform[3];
+    }
+
+    bone->mutableWorldTransform() = parentWorld * transform;
+}
+
+void IKConstraint::constrain(TransformComponent* component)
+{
+    if (m_Target == nullptr || m_Target->isCollapsed())
+    {
+        return;
+    }
+
+    Vec2D worldTargetTranslation = m_Target->worldTranslation();
+
+    // Decompose the chain.
+    for (BoneChainLink& item : m_FkChain)
+    {
+        auto bone = item.bone;
+        const Mat2D& parentWorld = getParentWorld(*bone);
+        item.parentWorldInverse = parentWorld.invertOrIdentity();
+
+        Mat2D& boneTransform = bone->mutableTransform();
+        boneTransform = item.parentWorldInverse * bone->worldTransform();
+        item.transformComponents = boneTransform.decompose();
+    }
+
+    int count = (int)m_FkChain.size();
+    switch (count)
+    {
+        case 1:
+            solve1(&m_FkChain[0], worldTargetTranslation);
+            break;
+        case 2:
+            solve2(&m_FkChain[0], &m_FkChain[1], worldTargetTranslation);
+            break;
+        default:
+        {
+            auto last = count - 1;
+            BoneChainLink* tip = &m_FkChain[last];
+            for (int i = 0; i < last; i++)
+            {
+                BoneChainLink* item = &m_FkChain[i];
+                solve2(item, tip, worldTargetTranslation);
+                for (int j = item->index + 1, end = (int)m_FkChain.size() - 1; j < end; j++)
+                {
+                    BoneChainLink& fk = m_FkChain[j];
+                    fk.parentWorldInverse = getParentWorld(*fk.bone).invertOrIdentity();
+                }
+            }
+            break;
+        }
+    }
+    // At the end, mix the FK angle with the IK angle by strength
+    if (strength() != 1.0f)
+    {
+        for (BoneChainLink& fk : m_FkChain)
+        {
+            float fromAngle = std::fmod(fk.transformComponents.rotation(), math::PI * 2);
+            float toAngle = std::fmod(fk.angle, math::PI * 2);
+            float diff = toAngle - fromAngle;
+            if (diff > math::PI)
+            {
+                diff -= math::PI * 2;
+            }
+            else if (diff < -math::PI)
+            {
+                diff += math::PI * 2;
+            }
+            float angle = fromAngle + diff * strength();
+            constrainRotation(fk, angle);
+        }
+    }
+}
diff --git a/src/constraints/rotation_constraint.cpp b/src/constraints/rotation_constraint.cpp
new file mode 100644
index 0000000..b37dc4a
--- /dev/null
+++ b/src/constraints/rotation_constraint.cpp
@@ -0,0 +1,113 @@
+#include "rive/constraints/rotation_constraint.hpp"
+#include "rive/transform_component.hpp"
+#include "rive/math/mat2d.hpp"
+#include "rive/math/math_types.hpp"
+
+using namespace rive;
+
+void RotationConstraint::constrain(TransformComponent* component)
+{
+    if (m_Target != nullptr && m_Target->isCollapsed())
+    {
+        return;
+    }
+    const Mat2D& transformA = component->worldTransform();
+    Mat2D transformB;
+    m_ComponentsA = transformA.decompose();
+    if (m_Target == nullptr)
+    {
+        transformB = transformA;
+        m_ComponentsB = m_ComponentsA;
+    }
+    else
+    {
+        transformB = m_Target->worldTransform();
+        if (sourceSpace() == TransformSpace::local)
+        {
+            Mat2D inverse;
+
+            if (!getParentWorld(*m_Target).invert(&inverse))
+            {
+                return;
+            }
+            transformB = inverse * transformB;
+        }
+
+        m_ComponentsB = transformB.decompose();
+
+        if (!doesCopy())
+        {
+            m_ComponentsB.rotation(destSpace() == TransformSpace::local ? 0.0f
+                                                                        : m_ComponentsA.rotation());
+        }
+        else
+        {
+            m_ComponentsB.rotation(m_ComponentsB.rotation() * copyFactor());
+            if (offset())
+            {
+                m_ComponentsB.rotation(m_ComponentsB.rotation() + component->rotation());
+            }
+        }
+
+        if (destSpace() == TransformSpace::local)
+        {
+            // Destination space is in parent transform coordinates. Recompose
+            // the parent local transform and get it in world, then decompose
+            // the world for interpolation.
+
+            transformB = Mat2D::compose(m_ComponentsB);
+            transformB = getParentWorld(*component) * transformB;
+            m_ComponentsB = transformB.decompose();
+        }
+    }
+    bool clampLocal = minMaxSpace() == TransformSpace::local;
+    if (clampLocal)
+    {
+        // Apply min max in local space, so transform to local coordinates
+        // first.
+        transformB = Mat2D::compose(m_ComponentsB);
+        Mat2D inverse;
+        if (!getParentWorld(*component).invert(&inverse))
+        {
+            return;
+        }
+        m_ComponentsB = (inverse * transformB).decompose();
+    }
+    if (max() && m_ComponentsB.rotation() > maxValue())
+    {
+        m_ComponentsB.rotation(maxValue());
+    }
+    if (min() && m_ComponentsB.rotation() < minValue())
+    {
+        m_ComponentsB.rotation(minValue());
+    }
+    if (clampLocal)
+    {
+        // Transform back to world.
+        transformB = Mat2D::compose(m_ComponentsB);
+        transformB = getParentWorld(*component) * transformB;
+        m_ComponentsB = transformB.decompose();
+    }
+
+    float angleA = std::fmod(m_ComponentsA.rotation(), math::PI * 2);
+    float angleB = std::fmod(m_ComponentsB.rotation(), math::PI * 2);
+    float diff = angleB - angleA;
+
+    if (diff > math::PI)
+    {
+        diff -= math::PI * 2;
+    }
+    else if (diff < -math::PI)
+    {
+        diff += math::PI * 2;
+    }
+
+    m_ComponentsB.rotation(m_ComponentsA.rotation() + diff * strength());
+    m_ComponentsB.x(m_ComponentsA.x());
+    m_ComponentsB.y(m_ComponentsA.y());
+    m_ComponentsB.scaleX(m_ComponentsA.scaleX());
+    m_ComponentsB.scaleY(m_ComponentsA.scaleY());
+    m_ComponentsB.skew(m_ComponentsA.skew());
+
+    component->mutableWorldTransform() = Mat2D::compose(m_ComponentsB);
+}
diff --git a/src/constraints/scale_constraint.cpp b/src/constraints/scale_constraint.cpp
new file mode 100644
index 0000000..f4bb455
--- /dev/null
+++ b/src/constraints/scale_constraint.cpp
@@ -0,0 +1,125 @@
+#include "rive/constraints/scale_constraint.hpp"
+#include "rive/transform_component.hpp"
+#include "rive/math/mat2d.hpp"
+#include <cmath>
+
+using namespace rive;
+
+void ScaleConstraint::constrain(TransformComponent* component)
+{
+    if (m_Target != nullptr && m_Target->isCollapsed())
+    {
+        return;
+    }
+    const Mat2D& transformA = component->worldTransform();
+    Mat2D transformB;
+    m_ComponentsA = transformA.decompose();
+    if (m_Target == nullptr)
+    {
+        transformB = transformA;
+        m_ComponentsB = m_ComponentsA;
+    }
+    else
+    {
+        transformB = m_Target->worldTransform();
+        if (sourceSpace() == TransformSpace::local)
+        {
+            Mat2D inverse;
+            if (!getParentWorld(*m_Target).invert(&inverse))
+            {
+                return;
+            }
+            transformB = inverse * transformB;
+        }
+        m_ComponentsB = transformB.decompose();
+
+        if (!doesCopy())
+        {
+            m_ComponentsB.scaleX(destSpace() == TransformSpace::local ? 1.0f
+                                                                      : m_ComponentsA.scaleX());
+        }
+        else
+        {
+            m_ComponentsB.scaleX(m_ComponentsB.scaleX() * copyFactor());
+            if (offset())
+            {
+                m_ComponentsB.scaleX(m_ComponentsB.scaleX() * component->scaleX());
+            }
+        }
+
+        if (!doesCopyY())
+        {
+            m_ComponentsB.scaleY(destSpace() == TransformSpace::local ? 1.0f
+                                                                      : m_ComponentsA.scaleY());
+        }
+        else
+        {
+            m_ComponentsB.scaleY(m_ComponentsB.scaleY() * copyFactorY());
+            if (offset())
+            {
+                m_ComponentsB.scaleY(m_ComponentsB.scaleY() * component->scaleY());
+            }
+        }
+
+        if (destSpace() == TransformSpace::local)
+        {
+            // Destination space is in parent transform coordinates. Recompose
+            // the parent local transform and get it in world, then decompose
+            // the world for interpolation.
+
+            transformB = Mat2D::compose(m_ComponentsB);
+            transformB = getParentWorld(*component) * transformB;
+            m_ComponentsB = transformB.decompose();
+        }
+    }
+
+    bool clamplocal = minMaxSpace() == TransformSpace::local;
+    if (clamplocal)
+    {
+        // Apply min max in local space, so transform to local coordinates
+        // first.
+        transformB = Mat2D::compose(m_ComponentsB);
+        Mat2D inverse;
+        if (!getParentWorld(*component).invert(&inverse))
+        {
+            return;
+        }
+        transformB = inverse * transformB;
+        m_ComponentsB = transformB.decompose();
+    }
+    if (max() && m_ComponentsB.scaleX() > maxValue())
+    {
+        m_ComponentsB.scaleX(maxValue());
+    }
+    if (min() && m_ComponentsB.scaleX() < minValue())
+    {
+        m_ComponentsB.scaleX(minValue());
+    }
+    if (maxY() && m_ComponentsB.scaleY() > maxValueY())
+    {
+        m_ComponentsB.scaleY(maxValueY());
+    }
+    if (minY() && m_ComponentsB.scaleY() < minValueY())
+    {
+        m_ComponentsB.scaleY(minValueY());
+    }
+    if (clamplocal)
+    {
+        // Transform back to world.
+        transformB = Mat2D::compose(m_ComponentsB);
+        transformB = getParentWorld(*component) * transformB;
+        m_ComponentsB = transformB.decompose();
+    }
+
+    float t = strength();
+    float ti = 1.0f - t;
+
+    m_ComponentsB.rotation(m_ComponentsA.rotation());
+    m_ComponentsB.x(m_ComponentsA.x());
+    m_ComponentsB.y(m_ComponentsA.y());
+    m_ComponentsB.scaleX(m_ComponentsA.scaleX() * ti + m_ComponentsB.scaleX() * t);
+    m_ComponentsB.scaleY(m_ComponentsA.scaleY() * ti + m_ComponentsB.scaleY() * t);
+    m_ComponentsB.skew(m_ComponentsA.skew());
+
+    component->mutableWorldTransform() = Mat2D::compose(m_ComponentsB);
+}
diff --git a/src/constraints/targeted_constraint.cpp b/src/constraints/targeted_constraint.cpp
new file mode 100644
index 0000000..4b3750f
--- /dev/null
+++ b/src/constraints/targeted_constraint.cpp
@@ -0,0 +1,33 @@
+#include "rive/constraints/targeted_constraint.hpp"
+#include "rive/transform_component.hpp"
+#include "rive/core_context.hpp"
+
+using namespace rive;
+
+StatusCode TargetedConstraint::onAddedDirty(CoreContext* context)
+{
+    StatusCode code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+    auto coreObject = context->resolve(targetId());
+    if (coreObject == nullptr || !coreObject->is<TransformComponent>())
+    {
+        return StatusCode::MissingObject;
+    }
+
+    m_Target = static_cast<TransformComponent*>(coreObject);
+
+    return StatusCode::Ok;
+}
+
+void TargetedConstraint::buildDependencies()
+{
+    // Targeted constraints must have their constrained component (parent)
+    // update after the target.
+    if (m_Target != nullptr)
+    {
+        m_Target->addDependent(parent());
+    }
+}
diff --git a/src/constraints/transform_constraint.cpp b/src/constraints/transform_constraint.cpp
new file mode 100644
index 0000000..a6b64cb
--- /dev/null
+++ b/src/constraints/transform_constraint.cpp
@@ -0,0 +1,69 @@
+#include "rive/constraints/transform_constraint.hpp"
+#include "rive/transform_component.hpp"
+#include "rive/math/mat2d.hpp"
+#include "rive/math/math_types.hpp"
+#include "rive/math/aabb.hpp"
+
+using namespace rive;
+
+const Mat2D TransformConstraint::targetTransform() const
+{
+    AABB bounds = m_Target->localBounds();
+    Mat2D local = Mat2D::fromTranslate(bounds.left() + bounds.width() * originX(),
+                                       bounds.top() + bounds.height() * originY());
+    return m_Target->worldTransform() * local;
+}
+
+void TransformConstraint::constrain(TransformComponent* component)
+{
+    if (m_Target == nullptr || m_Target->isCollapsed())
+    {
+        return;
+    }
+
+    const Mat2D& transformA = component->worldTransform();
+    Mat2D transformB(targetTransform());
+    if (sourceSpace() == TransformSpace::local)
+    {
+        const Mat2D& targetParentWorld = getParentWorld(*m_Target);
+
+        Mat2D inverse;
+        if (!targetParentWorld.invert(&inverse))
+        {
+            return;
+        }
+        transformB = inverse * transformB;
+    }
+    if (destSpace() == TransformSpace::local)
+    {
+        const Mat2D& targetParentWorld = getParentWorld(*component);
+        transformB = targetParentWorld * transformB;
+    }
+
+    m_ComponentsA = transformA.decompose();
+    m_ComponentsB = transformB.decompose();
+
+    float angleA = std::fmod(m_ComponentsA.rotation(), math::PI * 2);
+    float angleB = std::fmod(m_ComponentsB.rotation(), math::PI * 2);
+    float diff = angleB - angleA;
+    if (diff > math::PI)
+    {
+        diff -= math::PI * 2;
+    }
+    else if (diff < -math::PI)
+    {
+        diff += math::PI * 2;
+    }
+
+    float t = strength();
+    float ti = 1.0f - t;
+
+    m_ComponentsB.rotation(angleA + diff * t);
+    m_ComponentsB.x(m_ComponentsA.x() * ti + m_ComponentsB.x() * t);
+    m_ComponentsB.y(m_ComponentsA.y() * ti + m_ComponentsB.y() * t);
+    m_ComponentsB.scaleX(m_ComponentsA.scaleX() * ti + m_ComponentsB.scaleX() * t);
+    m_ComponentsB.scaleY(m_ComponentsA.scaleY() * ti + m_ComponentsB.scaleY() * t);
+    m_ComponentsB.skew(m_ComponentsA.skew() * ti + m_ComponentsB.skew() * t);
+
+    component->mutableWorldTransform() = Mat2D::compose(m_ComponentsB);
+}
diff --git a/src/constraints/translation_constraint.cpp b/src/constraints/translation_constraint.cpp
new file mode 100644
index 0000000..8febfb6
--- /dev/null
+++ b/src/constraints/translation_constraint.cpp
@@ -0,0 +1,113 @@
+#include "rive/constraints/translation_constraint.hpp"
+#include "rive/transform_component.hpp"
+#include "rive/math/mat2d.hpp"
+#include "rive/math/vec2d.hpp"
+#include <cmath>
+
+using namespace rive;
+
+void TranslationConstraint::constrain(TransformComponent* component)
+{
+    if (m_Target != nullptr && m_Target->isCollapsed())
+    {
+        return;
+    }
+    Mat2D& transformA = component->mutableWorldTransform();
+    Vec2D translationA(transformA[4], transformA[5]);
+    Vec2D translationB;
+    if (m_Target == nullptr)
+    {
+        translationB = translationA;
+    }
+    else
+    {
+        Mat2D transformB(m_Target->worldTransform());
+        if (sourceSpace() == TransformSpace::local)
+        {
+            const Mat2D& targetParentWorld = getParentWorld(*m_Target);
+
+            Mat2D inverse;
+            if (!targetParentWorld.invert(&inverse))
+            {
+                return;
+            }
+            transformB = inverse * transformB;
+        }
+        translationB = transformB.translation();
+
+        if (!doesCopy())
+        {
+            translationB.x = destSpace() == TransformSpace::local ? 0.0f : translationA.x;
+        }
+        else
+        {
+            translationB.x *= copyFactor();
+            if (offset())
+            {
+                translationB.x += component->x();
+            }
+        }
+
+        if (!doesCopyY())
+        {
+            translationB.y = destSpace() == TransformSpace::local ? 0.0f : translationA.y;
+        }
+        else
+        {
+            translationB.y *= copyFactorY();
+
+            if (offset())
+            {
+                translationB.y += component->y();
+            }
+        }
+
+        if (destSpace() == TransformSpace::local)
+        {
+            // Destination space is in parent transform coordinates.
+            translationB = getParentWorld(*component) * translationB;
+        }
+    }
+
+    bool clampLocal = minMaxSpace() == TransformSpace::local;
+    if (clampLocal)
+    {
+        // Apply min max in local space, so transform to local coordinates
+        // first.
+        Mat2D inverse;
+        if (!getParentWorld(*component).invert(&inverse))
+        {
+            return;
+        }
+        // Get our target world coordinates in parent local.
+        translationB = inverse * translationB;
+    }
+    if (max() && translationB.x > maxValue())
+    {
+        translationB.x = maxValue();
+    }
+    if (min() && translationB.x < minValue())
+    {
+        translationB.x = minValue();
+    }
+    if (maxY() && translationB.y > maxValueY())
+    {
+        translationB.y = maxValueY();
+    }
+    if (minY() && translationB.y < minValueY())
+    {
+        translationB.y = minValueY();
+    }
+    if (clampLocal)
+    {
+        // Transform back to world.
+        translationB = getParentWorld(*component) * translationB;
+    }
+
+    float t = strength();
+    float ti = 1.0f - t;
+
+    // Just interpolate world translation
+    transformA[4] = translationA.x * ti + translationB.x * t;
+    transformA[5] = translationA.y * ti + translationB.y * t;
+}
diff --git a/src/container_component.cpp b/src/container_component.cpp
new file mode 100644
index 0000000..2ed1bfe
--- /dev/null
+++ b/src/container_component.cpp
@@ -0,0 +1,44 @@
+#include "rive/container_component.hpp"
+using namespace rive;
+
+void ContainerComponent::addChild(Component* component) { m_children.push_back(component); }
+
+bool ContainerComponent::collapse(bool value)
+{
+    if (!Super::collapse(value))
+    {
+        return false;
+    }
+    for (Component* child : m_children)
+    {
+        child->collapse(value);
+    }
+    return true;
+}
+
+bool ContainerComponent::forAll(std::function<bool(Component*)> predicate)
+{
+    if (!predicate(this))
+    {
+        return false;
+    }
+    forEachChild(predicate);
+    return true;
+}
+
+bool ContainerComponent::forEachChild(std::function<bool(Component*)> predicate)
+{
+    for (Component* child : m_children)
+    {
+        if (!predicate(child))
+        {
+            return false;
+        }
+        if (child->is<ContainerComponent>() &&
+            !child->as<ContainerComponent>()->forEachChild(predicate))
+        {
+            return false;
+        }
+    }
+    return true;
+}
\ No newline at end of file
diff --git a/src/core/binary_data_reader.cpp b/src/core/binary_data_reader.cpp
new file mode 100644
index 0000000..1fa7f33
--- /dev/null
+++ b/src/core/binary_data_reader.cpp
@@ -0,0 +1,102 @@
+#include "rive/core/binary_data_reader.hpp"
+#include "rive/core/reader.h"
+
+using namespace rive;
+
+BinaryDataReader::BinaryDataReader(uint8_t* bytes, size_t length) :
+    m_Position(bytes), m_End(bytes + length), m_Overflowed(false), m_Length(length)
+{}
+
+size_t BinaryDataReader::lengthInBytes() const { return m_Length; }
+
+bool BinaryDataReader::didOverflow() const { return m_Overflowed; }
+
+void BinaryDataReader::overflow()
+{
+    m_Overflowed = true;
+    m_Position = m_End;
+}
+
+uint64_t BinaryDataReader::readVarUint()
+{
+    uint64_t value;
+    auto readBytes = decode_uint_leb(m_Position, m_End, &value);
+    if (readBytes == 0)
+    {
+        overflow();
+        return 0;
+    }
+    m_Position += readBytes;
+    return value;
+}
+
+uint32_t BinaryDataReader::readVarUint32()
+{
+    uint32_t value;
+    auto readBytes = decode_uint_leb32(m_Position, m_End, &value);
+    if (readBytes == 0)
+    {
+        overflow();
+        return 0;
+    }
+    m_Position += readBytes;
+    return value;
+}
+
+double BinaryDataReader::readFloat64()
+{
+    double value;
+    auto readBytes = decode_double(m_Position, m_End, &value);
+    if (readBytes == 0)
+    {
+        overflow();
+        return 0.0;
+    }
+    m_Position += readBytes;
+    return value;
+}
+
+float BinaryDataReader::readFloat32()
+{
+    float value;
+    auto readBytes = decode_float(m_Position, m_End, &value);
+    if (readBytes == 0)
+    {
+        overflow();
+        return 0.0f;
+    }
+    m_Position += readBytes;
+    return value;
+}
+
+uint8_t BinaryDataReader::readByte()
+{
+    if (m_End - m_Position < 1)
+    {
+        overflow();
+        return 0;
+    }
+    return *m_Position++;
+}
+
+uint32_t BinaryDataReader::readUint32()
+{
+    uint32_t value;
+    auto readBytes = decode_uint_32(m_Position, m_End, &value);
+    if (readBytes == 0)
+    {
+        overflow();
+        return 0;
+    }
+    m_Position += readBytes;
+    return value;
+}
+
+void BinaryDataReader::complete(uint8_t* bytes, size_t length)
+{
+    m_Position = bytes;
+    m_End = bytes + length;
+    m_Length = length;
+}
+
+void BinaryDataReader::reset(uint8_t* bytes) { m_Position = bytes; }
diff --git a/src/core/binary_reader.cpp b/src/core/binary_reader.cpp
new file mode 100644
index 0000000..ee3ca01
--- /dev/null
+++ b/src/core/binary_reader.cpp
@@ -0,0 +1,117 @@
+#include "rive/core/binary_reader.hpp"
+#include "rive/core/reader.h"
+#include "rive/span.hpp"
+#include <vector>
+
+using namespace rive;
+
+BinaryReader::BinaryReader(Span<const uint8_t> bytes) :
+    m_Bytes(bytes), m_Position(bytes.begin()), m_Overflowed(false), m_IntRangeError(false)
+{}
+
+bool BinaryReader::reachedEnd() const
+{
+    return m_Position == m_Bytes.end() || didOverflow() || didIntRangeError();
+}
+
+size_t BinaryReader::lengthInBytes() const { return m_Bytes.size(); }
+const uint8_t* BinaryReader::position() const { return m_Position; }
+
+bool BinaryReader::didOverflow() const { return m_Overflowed; }
+
+bool BinaryReader::didIntRangeError() const { return m_IntRangeError; }
+
+void BinaryReader::overflow()
+{
+    m_Overflowed = true;
+    m_Position = m_Bytes.end();
+}
+
+void BinaryReader::intRangeError()
+{
+    m_IntRangeError = true;
+    m_Position = m_Bytes.end();
+}
+
+uint64_t BinaryReader::readVarUint64()
+{
+    uint64_t value;
+    auto readBytes = decode_uint_leb(m_Position, m_Bytes.end(), &value);
+    if (readBytes == 0)
+    {
+        overflow();
+        return 0;
+    }
+    m_Position += readBytes;
+    return value;
+}
+
+std::string BinaryReader::readString()
+{
+    uint64_t length = readVarUint64();
+    if (didOverflow())
+    {
+        return std::string();
+    }
+
+    std::vector<char> rawValue((size_t)length + 1);
+    auto readBytes = decode_string(length, m_Position, m_Bytes.end(), &rawValue[0]);
+    if (readBytes != length)
+    {
+        overflow();
+        return std::string();
+    }
+    m_Position += readBytes;
+    return std::string(rawValue.data(), (size_t)length);
+}
+
+Span<const uint8_t> BinaryReader::readBytes()
+{
+    uint64_t length = readVarUint64();
+    if (didOverflow())
+    {
+        return Span<const uint8_t>(m_Position, 0);
+    }
+
+    const uint8_t* start = m_Position;
+    m_Position += length;
+    return {start, (size_t)length};
+}
+
+float BinaryReader::readFloat32()
+{
+    float value;
+    auto readBytes = decode_float(m_Position, m_Bytes.end(), &value);
+    if (readBytes == 0)
+    {
+        overflow();
+        return 0.0f;
+    }
+    m_Position += readBytes;
+    return value;
+}
+
+uint8_t BinaryReader::readByte()
+{
+    if (m_Bytes.end() - m_Position < 1)
+    {
+        overflow();
+        return 0;
+    }
+    return *m_Position++;
+}
+
+uint32_t BinaryReader::readUint32()
+{
+    uint32_t value;
+    auto readBytes = decode_uint_32(m_Position, m_Bytes.end(), &value);
+    if (readBytes == 0)
+    {
+        overflow();
+        return 0;
+    }
+    m_Position += readBytes;
+    return value;
+}
+
+void BinaryReader::reset() { m_Position = m_Bytes.begin(); }
diff --git a/src/core/binary_writer.cpp b/src/core/binary_writer.cpp
new file mode 100644
index 0000000..323e586
--- /dev/null
+++ b/src/core/binary_writer.cpp
@@ -0,0 +1,124 @@
+#include "rive/core/binary_writer.hpp"
+#include "rive/core/binary_stream.hpp"
+#include "rive/core/reader.h"
+#include <stdio.h>
+
+using namespace rive;
+
+BinaryWriter::BinaryWriter(BinaryStream* stream) : m_Stream(stream) {}
+BinaryWriter::~BinaryWriter() { m_Stream->flush(); }
+
+void BinaryWriter::write(float value)
+{
+    auto bytes = reinterpret_cast<uint8_t*>(&value);
+    if (is_big_endian())
+    {
+        uint8_t backwards[4] = {bytes[3], bytes[2], bytes[1], bytes[0]};
+        m_Stream->write(backwards, 4);
+    }
+    else
+    {
+        m_Stream->write(bytes, 4);
+    }
+}
+
+void BinaryWriter::writeFloat(float value)
+{
+    auto bytes = reinterpret_cast<uint8_t*>(&value);
+    if (is_big_endian())
+    {
+        uint8_t backwards[4] = {bytes[3], bytes[2], bytes[1], bytes[0]};
+        m_Stream->write(backwards, 4);
+    }
+    else
+    {
+        m_Stream->write(bytes, 4);
+    }
+}
+
+void BinaryWriter::write(double value)
+{
+    auto bytes = reinterpret_cast<uint8_t*>(&value);
+    if (is_big_endian())
+    {
+        uint8_t backwards[8] =
+            {bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0]};
+        m_Stream->write(backwards, 8);
+    }
+    else
+    {
+        m_Stream->write(bytes, 8);
+    }
+}
+
+void BinaryWriter::writeVarUint(uint64_t value)
+{
+    uint8_t buffer[16];
+    int index = 0;
+    do
+    {
+        uint8_t byte = value & 0x7f;
+        value >>= 7;
+
+        if (value != 0)
+        {
+            // more bytes follow
+            byte |= 0x80;
+        }
+
+        buffer[index++] = byte;
+    } while (value != 0);
+    m_Stream->write(buffer, index);
+}
+
+void BinaryWriter::writeVarUint(uint32_t value)
+{
+    uint8_t buffer[16];
+    int index = 0;
+    do
+    {
+        uint8_t byte = value & 0x7f;
+        value >>= 7;
+
+        if (value != 0)
+        {
+            // more bytes follow
+            byte |= 0x80;
+        }
+
+        buffer[index++] = byte;
+    } while (value != 0);
+    m_Stream->write(buffer, index);
+}
+
+void BinaryWriter::write(const uint8_t* bytes, size_t length)
+{
+    if (length == 0)
+    {
+        return;
+    }
+    m_Stream->write(bytes, length);
+}
+
+void BinaryWriter::writeDouble(double value)
+{
+    auto bytes = reinterpret_cast<uint8_t*>(&value);
+    if (is_big_endian())
+    {
+        uint8_t backwards[8] =
+            {bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0]};
+        m_Stream->write(backwards, 8);
+    }
+    else
+    {
+        m_Stream->write(bytes, 8);
+    }
+}
+
+void BinaryWriter::write(uint8_t value) { m_Stream->write((const uint8_t*)&value, 1); }
+
+void BinaryWriter::write(uint16_t value) { m_Stream->write((const uint8_t*)&value, 2); }
+
+void BinaryWriter::write(uint32_t value) { m_Stream->write((const uint8_t*)&value, 4); }
+
+void BinaryWriter::clear() { m_Stream->clear(); }
\ No newline at end of file
diff --git a/src/core/field_types/core_bool_type.cpp b/src/core/field_types/core_bool_type.cpp
new file mode 100644
index 0000000..705043d
--- /dev/null
+++ b/src/core/field_types/core_bool_type.cpp
@@ -0,0 +1,6 @@
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/core/binary_reader.hpp"
+
+using namespace rive;
+
+bool CoreBoolType::deserialize(BinaryReader& reader) { return reader.readByte() == 1; }
\ No newline at end of file
diff --git a/src/core/field_types/core_bytes_type.cpp b/src/core/field_types/core_bytes_type.cpp
new file mode 100644
index 0000000..78929fb
--- /dev/null
+++ b/src/core/field_types/core_bytes_type.cpp
@@ -0,0 +1,6 @@
+#include "rive/core/field_types/core_bytes_type.hpp"
+#include "rive/core/binary_reader.hpp"
+
+using namespace rive;
+
+Span<const uint8_t> CoreBytesType::deserialize(BinaryReader& reader) { return reader.readBytes(); }
\ No newline at end of file
diff --git a/src/core/field_types/core_color_type.cpp b/src/core/field_types/core_color_type.cpp
new file mode 100644
index 0000000..c1cda27
--- /dev/null
+++ b/src/core/field_types/core_color_type.cpp
@@ -0,0 +1,6 @@
+#include "rive/core/field_types/core_color_type.hpp"
+#include "rive/core/binary_reader.hpp"
+
+using namespace rive;
+
+int CoreColorType::deserialize(BinaryReader& reader) { return reader.readUint32(); }
\ No newline at end of file
diff --git a/src/core/field_types/core_double_type.cpp b/src/core/field_types/core_double_type.cpp
new file mode 100644
index 0000000..cbd3a0d
--- /dev/null
+++ b/src/core/field_types/core_double_type.cpp
@@ -0,0 +1,6 @@
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/binary_reader.hpp"
+
+using namespace rive;
+
+float CoreDoubleType::deserialize(BinaryReader& reader) { return reader.readFloat32(); }
\ No newline at end of file
diff --git a/src/core/field_types/core_string_type.cpp b/src/core/field_types/core_string_type.cpp
new file mode 100644
index 0000000..2e87b99
--- /dev/null
+++ b/src/core/field_types/core_string_type.cpp
@@ -0,0 +1,6 @@
+#include "rive/core/field_types/core_string_type.hpp"
+#include "rive/core/binary_reader.hpp"
+
+using namespace rive;
+
+std::string CoreStringType::deserialize(BinaryReader& reader) { return reader.readString(); }
\ No newline at end of file
diff --git a/src/core/field_types/core_uint_type.cpp b/src/core/field_types/core_uint_type.cpp
new file mode 100644
index 0000000..4009f50
--- /dev/null
+++ b/src/core/field_types/core_uint_type.cpp
@@ -0,0 +1,9 @@
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/core/binary_reader.hpp"
+
+using namespace rive;
+
+unsigned int CoreUintType::deserialize(BinaryReader& reader)
+{
+    return reader.readVarUintAs<unsigned int>();
+}
diff --git a/src/data_bind/context/context_value.cpp b/src/data_bind/context/context_value.cpp
new file mode 100644
index 0000000..5a12954
--- /dev/null
+++ b/src/data_bind/context/context_value.cpp
@@ -0,0 +1,122 @@
+#include "rive/data_bind/context/context_value.hpp"
+#include "rive/data_bind/context/context_value_color.hpp"
+#include "rive/data_bind/data_values/data_type.hpp"
+#include "rive/data_bind/data_values/data_value.hpp"
+#include "rive/data_bind/data_values/data_value_number.hpp"
+#include "rive/data_bind/data_values/data_value_string.hpp"
+#include "rive/data_bind/data_values/data_value_enum.hpp"
+#include "rive/data_bind/data_values/data_value_color.hpp"
+#include "rive/data_bind/data_values/data_value_boolean.hpp"
+#include "rive/generated/core_registry.hpp"
+
+using namespace rive;
+
+DataBindContextValue::DataBindContextValue(ViewModelInstanceValue* source,
+                                           DataConverter* converter) :
+    m_source(source), m_converter(converter)
+{
+    if (m_source != nullptr)
+    {
+        switch (m_source->coreType())
+        {
+            case ViewModelInstanceNumberBase::typeKey:
+                m_dataValue =
+                    new DataValueNumber(m_source->as<ViewModelInstanceNumber>()->propertyValue());
+                break;
+            case ViewModelInstanceStringBase::typeKey:
+                m_dataValue =
+                    new DataValueString(m_source->as<ViewModelInstanceString>()->propertyValue());
+                break;
+            case ViewModelInstanceColorBase::typeKey:
+                m_dataValue =
+                    new DataValueColor(m_source->as<ViewModelInstanceColor>()->propertyValue());
+                break;
+            case ViewModelInstanceBooleanBase::typeKey:
+                m_dataValue =
+                    new DataValueBoolean(m_source->as<ViewModelInstanceBoolean>()->propertyValue());
+                break;
+            case ViewModelInstanceEnumBase::typeKey:
+            {
+                auto viewmodelInstanceEnum = m_source->as<ViewModelInstanceEnum>();
+                auto viewModelPropertyEnum =
+                    viewmodelInstanceEnum->viewModelProperty()->as<ViewModelPropertyEnum>();
+                m_dataValue = new DataValueEnum(viewmodelInstanceEnum->propertyValue(),
+                                                viewModelPropertyEnum->dataEnum());
+            }
+            break;
+            default:
+                m_dataValue = new DataValue();
+        }
+    }
+}
+
+void DataBindContextValue::updateSourceValue()
+{
+    if (m_source != nullptr)
+    {
+        switch (m_source->coreType())
+        {
+            case ViewModelInstanceNumberBase::typeKey:
+                m_dataValue->as<DataValueNumber>()->value(
+                    m_source->as<ViewModelInstanceNumber>()->propertyValue());
+                break;
+            case ViewModelInstanceStringBase::typeKey:
+                m_dataValue->as<DataValueString>()->value(
+                    m_source->as<ViewModelInstanceString>()->propertyValue());
+                break;
+            case ViewModelInstanceColorBase::typeKey:
+                m_dataValue->as<DataValueColor>()->value(
+                    m_source->as<ViewModelInstanceColor>()->propertyValue());
+                break;
+            case ViewModelInstanceBooleanBase::typeKey:
+                m_dataValue->as<DataValueBoolean>()->value(
+                    m_source->as<ViewModelInstanceBoolean>()->propertyValue());
+                break;
+            case ViewModelInstanceEnumBase::typeKey:
+                m_dataValue->as<DataValueEnum>()->value(
+                    m_source->as<ViewModelInstanceEnum>()->propertyValue());
+                break;
+        }
+    }
+}
+
+void DataBindContextValue::applyToSource(Core* component,
+                                         uint32_t propertyKey,
+                                         bool isMainDirection)
+{
+    auto targetValue = getTargetValue(component, propertyKey);
+    switch (m_source->coreType())
+    {
+        case ViewModelInstanceNumberBase::typeKey:
+        {
+
+            auto value = calculateValue<DataValueNumber, float>(targetValue, isMainDirection);
+            m_source->as<ViewModelInstanceNumber>()->propertyValue(value);
+        }
+        break;
+        case ViewModelInstanceStringBase::typeKey:
+        {
+            auto value = calculateValue<DataValueString, std::string>(targetValue, isMainDirection);
+            m_source->as<ViewModelInstanceString>()->propertyValue(value);
+        }
+        break;
+        case ViewModelInstanceColorBase::typeKey:
+        {
+            auto value = calculateValue<DataValueColor, int>(targetValue, isMainDirection);
+            m_source->as<ViewModelInstanceColor>()->propertyValue(value);
+        }
+        break;
+        case ViewModelInstanceBooleanBase::typeKey:
+        {
+            auto value = calculateValue<DataValueBoolean, bool>(targetValue, isMainDirection);
+            m_source->as<ViewModelInstanceBoolean>()->propertyValue(value);
+        }
+        break;
+        case ViewModelInstanceEnumBase::typeKey:
+        {
+            auto value = calculateValue<DataValueEnum, uint32_t>(targetValue, isMainDirection);
+            m_source->as<ViewModelInstanceEnum>()->propertyValue(value);
+        }
+        break;
+    }
+}
\ No newline at end of file
diff --git a/src/data_bind/context/context_value_boolean.cpp b/src/data_bind/context/context_value_boolean.cpp
new file mode 100644
index 0000000..b08b8f5
--- /dev/null
+++ b/src/data_bind/context/context_value_boolean.cpp
@@ -0,0 +1,23 @@
+#include "rive/data_bind/context/context_value_boolean.hpp"
+#include "rive/data_bind/data_values/data_value_boolean.hpp"
+#include "rive/generated/core_registry.hpp"
+
+using namespace rive;
+
+DataBindContextValueBoolean::DataBindContextValueBoolean(ViewModelInstanceValue* source,
+                                                         DataConverter* converter) :
+    DataBindContextValue(source, converter)
+{}
+
+void DataBindContextValueBoolean::apply(Core* target, uint32_t propertyKey, bool isMainDirection)
+{
+    updateSourceValue();
+    auto value = calculateValue<DataValueBoolean, bool>(m_dataValue, isMainDirection);
+    CoreRegistry::setBool(target, propertyKey, value);
+}
+
+DataValue* DataBindContextValueBoolean::getTargetValue(Core* target, uint32_t propertyKey)
+{
+    auto value = CoreRegistry::getBool(target, propertyKey);
+    return new DataValueBoolean(value);
+}
\ No newline at end of file
diff --git a/src/data_bind/context/context_value_color.cpp b/src/data_bind/context/context_value_color.cpp
new file mode 100644
index 0000000..c66171d
--- /dev/null
+++ b/src/data_bind/context/context_value_color.cpp
@@ -0,0 +1,23 @@
+#include "rive/data_bind/context/context_value_color.hpp"
+#include "rive/data_bind/data_values/data_value_color.hpp"
+#include "rive/generated/core_registry.hpp"
+
+using namespace rive;
+
+DataBindContextValueColor::DataBindContextValueColor(ViewModelInstanceValue* source,
+                                                     DataConverter* converter) :
+    DataBindContextValue(source, converter)
+{}
+
+void DataBindContextValueColor::apply(Core* target, uint32_t propertyKey, bool isMainDirection)
+{
+    updateSourceValue();
+    auto value = calculateValue<DataValueColor, int>(m_dataValue, isMainDirection);
+    CoreRegistry::setColor(target, propertyKey, value);
+}
+
+DataValue* DataBindContextValueColor::getTargetValue(Core* target, uint32_t propertyKey)
+{
+    auto value = CoreRegistry::getColor(target, propertyKey);
+    return new DataValueColor(value);
+}
\ No newline at end of file
diff --git a/src/data_bind/context/context_value_enum.cpp b/src/data_bind/context/context_value_enum.cpp
new file mode 100644
index 0000000..dac5920
--- /dev/null
+++ b/src/data_bind/context/context_value_enum.cpp
@@ -0,0 +1,27 @@
+#include "rive/data_bind/context/context_value_enum.hpp"
+#include "rive/data_bind/data_values/data_value_enum.hpp"
+#include "rive/generated/core_registry.hpp"
+
+using namespace rive;
+
+DataBindContextValueEnum::DataBindContextValueEnum(ViewModelInstanceValue* source,
+                                                   DataConverter* converter) :
+    DataBindContextValue(source, converter)
+{}
+
+void DataBindContextValueEnum::apply(Core* target, uint32_t propertyKey, bool isMainDirection)
+{
+
+    updateSourceValue();
+    auto value = calculateValue<DataValueEnum, uint32_t>(m_dataValue, isMainDirection);
+    CoreRegistry::setUint(target, propertyKey, value);
+}
+
+DataValue* DataBindContextValueEnum::getTargetValue(Core* target, uint32_t propertyKey)
+{
+    auto value = CoreRegistry::getUint(target, propertyKey);
+    auto viewmodelInstanceEnum = m_source->as<ViewModelInstanceEnum>();
+    auto viewModelPropertyEnum =
+        viewmodelInstanceEnum->viewModelProperty()->as<ViewModelPropertyEnum>();
+    return new DataValueEnum(value, viewModelPropertyEnum->dataEnum());
+}
\ No newline at end of file
diff --git a/src/data_bind/context/context_value_list.cpp b/src/data_bind/context/context_value_list.cpp
new file mode 100644
index 0000000..92fc2fd
--- /dev/null
+++ b/src/data_bind/context/context_value_list.cpp
@@ -0,0 +1,135 @@
+#include "rive/data_bind/context/context_value_list.hpp"
+#include "rive/data_bind/context/context_value_list_item.hpp"
+#include "rive/generated/core_registry.hpp"
+#include "rive/node.hpp"
+
+using namespace rive;
+
+DataBindContextValueList::DataBindContextValueList(ViewModelInstanceValue* source,
+                                                   DataConverter* converter) :
+    DataBindContextValue(source, converter)
+{}
+
+std::unique_ptr<ArtboardInstance> DataBindContextValueList::createArtboard(
+    Component* target,
+    Artboard* artboard,
+    ViewModelInstanceListItem* listItem) const
+{
+    if (artboard != nullptr)
+    {
+
+        auto mainArtboard = target->artboard();
+        auto dataContext = mainArtboard->dataContext();
+        auto artboardCopy = artboard->instance();
+        artboardCopy->advanceInternal(0.0f, false);
+        artboardCopy->dataContextFromInstance(listItem->viewModelInstance(), dataContext, false);
+        return artboardCopy;
+    }
+    return nullptr;
+}
+
+std::unique_ptr<StateMachineInstance> DataBindContextValueList::createStateMachineInstance(
+    ArtboardInstance* artboard)
+{
+    if (artboard != nullptr)
+    {
+        auto stateMachineInstance = artboard->stateMachineAt(0);
+        stateMachineInstance->advance(0.0f);
+        return stateMachineInstance;
+    }
+    return nullptr;
+}
+
+void DataBindContextValueList::insertItem(Core* target,
+                                          ViewModelInstanceListItem* listItem,
+                                          int index)
+{
+    auto artboard = listItem->artboard();
+    auto artboardCopy = createArtboard(target->as<Component>(), artboard, listItem);
+    auto stateMachineInstance = createStateMachineInstance(artboardCopy.get());
+    std::unique_ptr<DataBindContextValueListItem> cacheListItem =
+        rivestd::make_unique<DataBindContextValueListItem>(std::move(artboardCopy),
+                                                           std::move(stateMachineInstance),
+                                                           listItem);
+    if (index == -1)
+    {
+        m_ListItemsCache.push_back(std::move(cacheListItem));
+    }
+    else
+    {
+        m_ListItemsCache.insert(m_ListItemsCache.begin() + index, std::move(cacheListItem));
+    }
+}
+
+void DataBindContextValueList::swapItems(Core* target, int index1, int index2)
+{
+    std::iter_swap(m_ListItemsCache.begin() + index1, m_ListItemsCache.begin() + index2);
+}
+
+void DataBindContextValueList::popItem(Core* target) { m_ListItemsCache.pop_back(); }
+
+void DataBindContextValueList::update(Core* target)
+{
+    if (target != nullptr)
+    {
+        auto sourceList = m_source->as<ViewModelInstanceList>();
+        auto listItems = sourceList->listItems();
+
+        int listIndex = 0;
+        while (listIndex < listItems.size())
+        {
+            auto listItem = listItems[listIndex];
+            if (listIndex < m_ListItemsCache.size())
+            {
+                if (m_ListItemsCache[listIndex]->listItem() == listItem)
+                {
+                    // Same item in same position: do nothing
+                }
+                else
+                {
+                    int cacheIndex = listIndex + 1;
+                    bool found = false;
+                    while (cacheIndex < m_ListItemsCache.size())
+                    {
+                        if (m_ListItemsCache[cacheIndex]->listItem() == listItem)
+                        {
+                            // swap cache position with new item
+                            swapItems(target, listIndex, cacheIndex);
+                            found = true;
+                            break;
+                        }
+                        cacheIndex++;
+                    }
+                    if (!found)
+                    {
+                        // create new element and insert it in listIndex
+                        insertItem(target, listItem, listIndex);
+                    }
+                }
+            }
+            else
+            {
+                // create new element and cache the listItem in listIndex
+                insertItem(target, listItem, -1);
+            }
+
+            listIndex++;
+        }
+        // remove remaining cached elements backwars to pop from the vector.
+        listIndex = m_ListItemsCache.size() - 1;
+        while (listIndex >= listItems.size())
+        {
+            popItem(target);
+            listIndex--;
+        }
+    }
+}
+
+void DataBindContextValueList::apply(Core* target, uint32_t propertyKey, bool isMainDirection) {}
+
+void DataBindContextValueList::applyToSource(Core* target,
+                                             uint32_t propertyKey,
+                                             bool isMainDirection)
+{
+    // TODO: @hernan does applyToSource make sense? Should we block it somehow?
+}
\ No newline at end of file
diff --git a/src/data_bind/context/context_value_list_item.cpp b/src/data_bind/context/context_value_list_item.cpp
new file mode 100644
index 0000000..07f1a0f
--- /dev/null
+++ b/src/data_bind/context/context_value_list_item.cpp
@@ -0,0 +1,11 @@
+#include "rive/data_bind/context/context_value_list_item.hpp"
+
+using namespace rive;
+
+DataBindContextValueListItem::DataBindContextValueListItem(
+    std::unique_ptr<ArtboardInstance> artboard,
+    std::unique_ptr<StateMachineInstance> stateMachine,
+    ViewModelInstanceListItem* listItem) :
+    m_Artboard(std::move(artboard)),
+    m_StateMachine(std::move(stateMachine)),
+    m_ListItem(listItem){};
\ No newline at end of file
diff --git a/src/data_bind/context/context_value_number.cpp b/src/data_bind/context/context_value_number.cpp
new file mode 100644
index 0000000..4f40997
--- /dev/null
+++ b/src/data_bind/context/context_value_number.cpp
@@ -0,0 +1,23 @@
+#include "rive/data_bind/context/context_value_number.hpp"
+#include "rive/data_bind/data_values/data_value_number.hpp"
+#include "rive/generated/core_registry.hpp"
+
+using namespace rive;
+
+DataBindContextValueNumber::DataBindContextValueNumber(ViewModelInstanceValue* source,
+                                                       DataConverter* converter) :
+    DataBindContextValue(source, converter)
+{}
+
+void DataBindContextValueNumber::apply(Core* target, uint32_t propertyKey, bool isMainDirection)
+{
+    updateSourceValue();
+    auto value = calculateValue<DataValueNumber, float>(m_dataValue, isMainDirection);
+    CoreRegistry::setDouble(target, propertyKey, value);
+}
+
+DataValue* DataBindContextValueNumber::getTargetValue(Core* target, uint32_t propertyKey)
+{
+    auto value = CoreRegistry::getDouble(target, propertyKey);
+    return new DataValueNumber(value);
+}
\ No newline at end of file
diff --git a/src/data_bind/context/context_value_string.cpp b/src/data_bind/context/context_value_string.cpp
new file mode 100644
index 0000000..2c73dc8
--- /dev/null
+++ b/src/data_bind/context/context_value_string.cpp
@@ -0,0 +1,23 @@
+#include "rive/data_bind/context/context_value_string.hpp"
+#include "rive/data_bind/data_values/data_value_string.hpp"
+#include "rive/generated/core_registry.hpp"
+
+using namespace rive;
+
+DataBindContextValueString::DataBindContextValueString(ViewModelInstanceValue* source,
+                                                       DataConverter* converter) :
+    DataBindContextValue(source, converter)
+{}
+
+void DataBindContextValueString::apply(Core* target, uint32_t propertyKey, bool isMainDirection)
+{
+    updateSourceValue();
+    auto value = calculateValue<DataValueString, std::string>(m_dataValue, isMainDirection);
+    CoreRegistry::setString(target, propertyKey, value);
+}
+
+DataValue* DataBindContextValueString::getTargetValue(Core* target, uint32_t propertyKey)
+{
+    auto value = CoreRegistry::getString(target, propertyKey);
+    return new DataValueString(value);
+}
\ No newline at end of file
diff --git a/src/data_bind/converters/data_converter.cpp b/src/data_bind/converters/data_converter.cpp
new file mode 100644
index 0000000..d9645b9
--- /dev/null
+++ b/src/data_bind/converters/data_converter.cpp
@@ -0,0 +1,18 @@
+#include "rive/data_bind/converters/data_converter.hpp"
+#include "rive/importers/import_stack.hpp"
+#include "rive/importers/backboard_importer.hpp"
+#include "rive/backboard.hpp"
+
+using namespace rive;
+
+StatusCode DataConverter::import(ImportStack& importStack)
+{
+    auto backboardImporter = importStack.latest<BackboardImporter>(Backboard::typeKey);
+    if (backboardImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    backboardImporter->addDataConverter(this);
+
+    return Super::import(importStack);
+}
\ No newline at end of file
diff --git a/src/data_bind/converters/data_converter_group.cpp b/src/data_bind/converters/data_converter_group.cpp
new file mode 100644
index 0000000..aab150f
--- /dev/null
+++ b/src/data_bind/converters/data_converter_group.cpp
@@ -0,0 +1,28 @@
+#include "rive/math/math_types.hpp"
+#include "rive/data_bind/converters/data_converter_group.hpp"
+#include "rive/data_bind/data_values/data_value_number.hpp"
+#include "rive/data_bind/data_values/data_value_string.hpp"
+
+using namespace rive;
+
+void DataConverterGroup::addItem(DataConverterGroupItem* item) { m_items.push_back(item); }
+
+DataValue* DataConverterGroup::convert(DataValue* input)
+{
+    DataValue* value = input;
+    for (auto item : m_items)
+    {
+        value = item->converter()->convert(value);
+    }
+    return value;
+}
+
+DataValue* DataConverterGroup::reverseConvert(DataValue* input)
+{
+    DataValue* value = input;
+    for (auto it = m_items.rbegin(); it != m_items.rend(); ++it)
+    {
+        value = (*it)->converter()->reverseConvert(value);
+    }
+    return value;
+}
\ No newline at end of file
diff --git a/src/data_bind/converters/data_converter_group_item.cpp b/src/data_bind/converters/data_converter_group_item.cpp
new file mode 100644
index 0000000..3a6fa65
--- /dev/null
+++ b/src/data_bind/converters/data_converter_group_item.cpp
@@ -0,0 +1,26 @@
+#include "rive/backboard.hpp"
+#include "rive/math/math_types.hpp"
+#include "rive/data_bind/converters/data_converter_group_item.hpp"
+#include "rive/data_bind/converters/data_converter_group.hpp"
+#include "rive/importers/data_converter_group_importer.hpp"
+#include "rive/importers/backboard_importer.hpp"
+
+using namespace rive;
+
+StatusCode DataConverterGroupItem::import(ImportStack& importStack)
+{
+    auto backboardImporter = importStack.latest<BackboardImporter>(Backboard::typeKey);
+    if (backboardImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    backboardImporter->addDataConverterGroupItemReferencer(this);
+    auto dataConveterGroupImporter =
+        importStack.latest<DataConverterGroupImporter>(DataConverterGroupBase::typeKey);
+    if (dataConveterGroupImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    dataConveterGroupImporter->group()->addItem(this);
+    return Super::import(importStack);
+}
\ No newline at end of file
diff --git a/src/data_bind/converters/data_converter_operation.cpp b/src/data_bind/converters/data_converter_operation.cpp
new file mode 100644
index 0000000..dfb43d8
--- /dev/null
+++ b/src/data_bind/converters/data_converter_operation.cpp
@@ -0,0 +1,65 @@
+#include "rive/math/math_types.hpp"
+#include "rive/data_bind/converters/data_converter_operation.hpp"
+#include "rive/data_bind/data_values/data_value_number.hpp"
+
+using namespace rive;
+
+DataValue* DataConverterOperation::convert(DataValue* input)
+{
+    auto output = new DataValueNumber();
+    if (input->is<DataValueNumber>())
+    {
+        float inputValue = input->as<DataValueNumber>()->value();
+        float resultValue = value();
+        switch (op())
+        {
+            case ArithmeticOperation::add:
+                resultValue = inputValue + resultValue;
+                break;
+            case ArithmeticOperation::subtract:
+                resultValue = inputValue - resultValue;
+                break;
+            case ArithmeticOperation::multiply:
+                resultValue = inputValue * resultValue;
+                break;
+            case ArithmeticOperation::divide:
+                resultValue = inputValue / resultValue;
+                break;
+            case ArithmeticOperation::modulo:
+                resultValue = fmodf(inputValue, resultValue);
+                break;
+        }
+        output->value(resultValue);
+    }
+    return output;
+}
+
+DataValue* DataConverterOperation::reverseConvert(DataValue* input)
+{
+    auto output = new DataValueNumber();
+    if (input->is<DataValueNumber>())
+    {
+        float inputValue = input->as<DataValueNumber>()->value();
+        float resultValue = value();
+        switch (op())
+        {
+            case ArithmeticOperation::add:
+                resultValue = inputValue - resultValue;
+                break;
+            case ArithmeticOperation::subtract:
+                resultValue = inputValue + resultValue;
+                break;
+            case ArithmeticOperation::multiply:
+                resultValue = inputValue / resultValue;
+                break;
+            case ArithmeticOperation::divide:
+                resultValue = inputValue * resultValue;
+                break;
+                // No reverse operation for modulo
+            case ArithmeticOperation::modulo:
+                break;
+        }
+        output->value(resultValue);
+    }
+    return output;
+}
\ No newline at end of file
diff --git a/src/data_bind/converters/data_converter_rounder.cpp b/src/data_bind/converters/data_converter_rounder.cpp
new file mode 100644
index 0000000..86422ff
--- /dev/null
+++ b/src/data_bind/converters/data_converter_rounder.cpp
@@ -0,0 +1,19 @@
+#include "rive/math/math_types.hpp"
+#include "rive/data_bind/converters/data_converter_rounder.hpp"
+#include "rive/data_bind/data_values/data_value_number.hpp"
+
+using namespace rive;
+
+DataValue* DataConverterRounder::convert(DataValue* input)
+{
+    auto output = new DataValueNumber();
+    if (input->is<DataValueNumber>())
+    {
+        float value = input->as<DataValueNumber>()->value();
+        auto numberOfPlaces = decimals();
+        // TODO: @hernan review this way of rounding
+        float rounder = pow(10.0f, (float)numberOfPlaces);
+        output->value(std::round(value * rounder) / rounder);
+    }
+    return output;
+}
\ No newline at end of file
diff --git a/src/data_bind/converters/data_converter_to_string.cpp b/src/data_bind/converters/data_converter_to_string.cpp
new file mode 100644
index 0000000..6c62f38
--- /dev/null
+++ b/src/data_bind/converters/data_converter_to_string.cpp
@@ -0,0 +1,36 @@
+#include "rive/math/math_types.hpp"
+#include "rive/data_bind/converters/data_converter_to_string.hpp"
+#include "rive/data_bind/data_values/data_value_number.hpp"
+#include "rive/data_bind/data_values/data_value_enum.hpp"
+#include "rive/data_bind/data_values/data_value_string.hpp"
+
+using namespace rive;
+
+DataValue* DataConverterToString::convert(DataValue* input)
+{
+    auto output = new DataValueString();
+    if (input->is<DataValueNumber>())
+    {
+        float value = input->as<DataValueNumber>()->value();
+        std::string str = std::to_string(value);
+        if (str.find('.') != std::string::npos)
+        {
+            // Remove trailing zeroes
+            str = str.substr(0, str.find_last_not_of('0') + 1);
+            // If the decimal point is now the last character, remove that as well
+            if (str.find('.') == str.size() - 1)
+            {
+                str = str.substr(0, str.size() - 1);
+            }
+        }
+        output->value(str);
+    }
+    else if (input->is<DataValueEnum>())
+    {
+        auto dataEnum = input->as<DataValueEnum>()->dataEnum();
+        auto index = input->as<DataValueEnum>()->value();
+        auto enumValue = dataEnum->value(index);
+        output->value(enumValue);
+    }
+    return output;
+}
\ No newline at end of file
diff --git a/src/data_bind/data_bind.cpp b/src/data_bind/data_bind.cpp
new file mode 100644
index 0000000..e4c52c9
--- /dev/null
+++ b/src/data_bind/data_bind.cpp
@@ -0,0 +1,191 @@
+#include "rive/data_bind/data_bind.hpp"
+#include "rive/artboard.hpp"
+#include "rive/data_bind_flags.hpp"
+#include "rive/generated/core_registry.hpp"
+#include "rive/data_bind/bindable_property_number.hpp"
+#include "rive/data_bind/bindable_property_string.hpp"
+#include "rive/data_bind/bindable_property_color.hpp"
+#include "rive/data_bind/bindable_property_enum.hpp"
+#include "rive/data_bind/bindable_property_boolean.hpp"
+#include "rive/data_bind/context/context_value.hpp"
+#include "rive/data_bind/context/context_value_boolean.hpp"
+#include "rive/data_bind/context/context_value_number.hpp"
+#include "rive/data_bind/context/context_value_string.hpp"
+#include "rive/data_bind/context/context_value_enum.hpp"
+#include "rive/data_bind/context/context_value_list.hpp"
+#include "rive/data_bind/context/context_value_color.hpp"
+#include "rive/data_bind/data_values/data_type.hpp"
+#include "rive/animation/transition_viewmodel_condition.hpp"
+#include "rive/animation/state_machine.hpp"
+#include "rive/importers/artboard_importer.hpp"
+#include "rive/importers/state_machine_importer.hpp"
+#include "rive/importers/backboard_importer.hpp"
+
+using namespace rive;
+
+StatusCode DataBind::onAddedDirty(CoreContext* context)
+{
+    StatusCode code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+
+    return StatusCode::Ok;
+}
+
+StatusCode DataBind::import(ImportStack& importStack)
+{
+
+    auto backboardImporter = importStack.latest<BackboardImporter>(Backboard::typeKey);
+    if (backboardImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    backboardImporter->addDataConverterReferencer(this);
+    if (target())
+    {
+        switch (target()->coreType())
+        {
+            case BindablePropertyNumberBase::typeKey:
+            case BindablePropertyStringBase::typeKey:
+            case BindablePropertyBooleanBase::typeKey:
+            case BindablePropertyEnumBase::typeKey:
+            case BindablePropertyColorBase::typeKey:
+            case TransitionPropertyViewModelComparatorBase::typeKey:
+            {
+                auto stateMachineImporter =
+                    importStack.latest<StateMachineImporter>(StateMachineBase::typeKey);
+                if (stateMachineImporter != nullptr)
+                {
+                    stateMachineImporter->addDataBind(std::unique_ptr<DataBind>(this));
+                    return Super::import(importStack);
+                }
+                break;
+            }
+            default:
+            {
+                auto artboardImporter = importStack.latest<ArtboardImporter>(ArtboardBase::typeKey);
+                if (artboardImporter != nullptr)
+                {
+                    artboardImporter->addDataBind(this);
+                    return Super::import(importStack);
+                }
+                break;
+            }
+        }
+    }
+
+    return Super::import(importStack);
+}
+
+DataType DataBind::outputType()
+{
+    if (converter())
+    {
+        return converter()->outputType();
+    }
+    switch (m_Source->coreType())
+    {
+        case ViewModelInstanceNumberBase::typeKey:
+            return DataType::number;
+        case ViewModelInstanceStringBase::typeKey:
+            return DataType::string;
+        case ViewModelInstanceEnumBase::typeKey:
+            return DataType::enumType;
+        case ViewModelInstanceColorBase::typeKey:
+            return DataType::color;
+        case ViewModelInstanceBooleanBase::typeKey:
+            return DataType::boolean;
+        case ViewModelInstanceListBase::typeKey:
+            return DataType::list;
+    }
+    return DataType::none;
+}
+
+void DataBind::bind()
+{
+    switch (outputType())
+    {
+        case DataType::number:
+            m_ContextValue =
+                rivestd::make_unique<DataBindContextValueNumber>(m_Source, converter());
+            break;
+        case DataType::string:
+            m_ContextValue =
+                rivestd::make_unique<DataBindContextValueString>(m_Source, converter());
+            break;
+        case DataType::boolean:
+            m_ContextValue =
+                rivestd::make_unique<DataBindContextValueBoolean>(m_Source, converter());
+            break;
+        case DataType::color:
+            m_ContextValue = rivestd::make_unique<DataBindContextValueColor>(m_Source, converter());
+            break;
+        case DataType::enumType:
+            m_ContextValue = rivestd::make_unique<DataBindContextValueEnum>(m_Source, converter());
+            break;
+        case DataType::list:
+            m_ContextValue = rivestd::make_unique<DataBindContextValueList>(m_Source, converter());
+            m_ContextValue->update(m_target);
+            break;
+        default:
+            break;
+    }
+}
+
+void DataBind::update(ComponentDirt value)
+{
+    if (m_Source != nullptr && m_ContextValue != nullptr)
+    {
+
+        // Use the ComponentDirt::Components flag to indicate the viewmodel has added or removed
+        // an element to a list.
+        if ((value & ComponentDirt::Components) == ComponentDirt::Components)
+        {
+            m_ContextValue->update(m_target);
+        }
+        if ((value & ComponentDirt::Bindings) == ComponentDirt::Bindings)
+        {
+            // TODO: @hernan review how dirt and mode work together. If dirt is not set for
+            // certain modes, we might be able to skip the mode validation.
+            auto flagsValue = static_cast<DataBindFlags>(flags());
+            if (((flagsValue & DataBindFlags::Direction) == DataBindFlags::ToTarget) ||
+                ((flagsValue & DataBindFlags::TwoWay) == DataBindFlags::TwoWay))
+            {
+                m_ContextValue->apply(m_target,
+                                      propertyKey(),
+                                      (flagsValue & DataBindFlags::Direction) ==
+                                          DataBindFlags::ToTarget);
+            }
+        }
+    }
+}
+
+void DataBind::updateSourceBinding()
+{
+    auto flagsValue = static_cast<DataBindFlags>(flags());
+    if (((flagsValue & DataBindFlags::Direction) == DataBindFlags::ToSource) ||
+        ((flagsValue & DataBindFlags::TwoWay) == DataBindFlags::TwoWay))
+    {
+        if (m_ContextValue != nullptr)
+        {
+            m_ContextValue->applyToSource(m_target,
+                                          propertyKey(),
+                                          (flagsValue & DataBindFlags::Direction) ==
+                                              DataBindFlags::ToSource);
+        }
+    }
+}
+
+bool DataBind::addDirt(ComponentDirt value, bool recurse)
+{
+    if ((m_Dirt & value) == value)
+    {
+        // Already marked.
+        return false;
+    }
+
+    m_Dirt |= value;
+    return true;
+}
\ No newline at end of file
diff --git a/src/data_bind/data_bind_context.cpp b/src/data_bind/data_bind_context.cpp
new file mode 100644
index 0000000..62eae73
--- /dev/null
+++ b/src/data_bind/data_bind_context.cpp
@@ -0,0 +1,41 @@
+#include "rive/data_bind_flags.hpp"
+#include "rive/data_bind/data_bind_context.hpp"
+#include "rive/data_bind/context/context_value_number.hpp"
+#include "rive/data_bind/context/context_value_string.hpp"
+#include "rive/data_bind/context/context_value_enum.hpp"
+#include "rive/data_bind/context/context_value_list.hpp"
+#include "rive/data_bind/context/context_value_color.hpp"
+#include "rive/artboard.hpp"
+#include "rive/generated/core_registry.hpp"
+#include <iostream>
+
+using namespace rive;
+
+void DataBindContext::decodeSourcePathIds(Span<const uint8_t> value)
+{
+    BinaryReader reader(value);
+    while (!reader.reachedEnd())
+    {
+        auto val = reader.readVarUintAs<uint32_t>();
+        m_SourcePathIdsBuffer.push_back(val);
+    }
+}
+
+void DataBindContext::copySourcePathIds(const DataBindContextBase& object)
+{
+    m_SourcePathIdsBuffer = object.as<DataBindContext>()->m_SourcePathIdsBuffer;
+}
+
+void DataBindContext::bindFromContext(DataContext* dataContext)
+{
+    if (dataContext != nullptr)
+    {
+        auto value = dataContext->getViewModelProperty(m_SourcePathIdsBuffer);
+        if (value != nullptr)
+        {
+            value->addDependent(this);
+            m_Source = value;
+            bind();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/data_bind/data_context.cpp b/src/data_bind/data_context.cpp
new file mode 100644
index 0000000..a8b3f30
--- /dev/null
+++ b/src/data_bind/data_context.cpp
@@ -0,0 +1,90 @@
+#include "rive/data_bind/data_context.hpp"
+#include "rive/viewmodel/viewmodel_instance_viewmodel.hpp"
+
+using namespace rive;
+
+DataContext::DataContext(ViewModelInstance* viewModelInstance) :
+    m_ViewModelInstance(viewModelInstance)
+{}
+
+DataContext::DataContext() : m_ViewModelInstances({}) {}
+
+DataContext::~DataContext() {}
+
+void DataContext::addViewModelInstance(ViewModelInstance* value)
+{
+    m_ViewModelInstances.push_back(value);
+}
+
+void DataContext::viewModelInstance(ViewModelInstance* value) { m_ViewModelInstance = value; }
+
+ViewModelInstanceValue* DataContext::getViewModelProperty(const std::vector<uint32_t> path) const
+{
+    std::vector<uint32_t>::const_iterator it;
+    if (path.size() == 0)
+    {
+        return nullptr;
+    }
+    // TODO: @hernan review. We should probably remove the std::vector and only keep the instance
+    for (auto viewModel : m_ViewModelInstances)
+    {
+        if (viewModel->viewModelId() == path[0])
+        {
+            ViewModelInstance* instance = viewModel;
+            for (it = path.begin() + 1; it != path.end() - 1; it++)
+            {
+                instance = instance->propertyValue(*it)
+                               ->as<ViewModelInstanceViewModel>()
+                               ->referenceViewModelInstance();
+            }
+            ViewModelInstanceValue* value = instance->propertyValue(*it++);
+            return value;
+        }
+    }
+    if (m_ViewModelInstance != nullptr && m_ViewModelInstance->viewModelId() == path[0])
+    {
+        ViewModelInstance* instance = m_ViewModelInstance;
+        for (it = path.begin() + 1; it != path.end() - 1; it++)
+        {
+            instance = instance->propertyValue(*it)
+                           ->as<ViewModelInstanceViewModel>()
+                           ->referenceViewModelInstance();
+        }
+        ViewModelInstanceValue* value = instance->propertyValue(*it++);
+        return value;
+    }
+    if (m_Parent != nullptr)
+    {
+        return m_Parent->getViewModelProperty(path);
+    }
+    return nullptr;
+}
+
+ViewModelInstance* DataContext::getViewModelInstance(const std::vector<uint32_t> path) const
+{
+    std::vector<uint32_t>::const_iterator it;
+    if (path.size() == 0)
+    {
+        return nullptr;
+    }
+    if (m_ViewModelInstance != nullptr && m_ViewModelInstance->viewModelId() == path[0])
+    {
+        ViewModelInstance* instance = m_ViewModelInstance;
+        for (it = path.begin() + 1; it != path.end(); it++)
+        {
+            instance = instance->propertyValue(*it)
+                           ->as<ViewModelInstanceViewModel>()
+                           ->referenceViewModelInstance();
+            if (instance == nullptr)
+            {
+                return instance;
+            }
+        }
+        return instance;
+    }
+    if (m_Parent != nullptr)
+    {
+        return m_Parent->getViewModelInstance(path);
+    }
+    return nullptr;
+}
\ No newline at end of file
diff --git a/src/dependency_sorter.cpp b/src/dependency_sorter.cpp
new file mode 100644
index 0000000..dcfa266
--- /dev/null
+++ b/src/dependency_sorter.cpp
@@ -0,0 +1,47 @@
+#include "rive/dependency_sorter.hpp"
+#include "rive/component.hpp"
+
+using namespace rive;
+
+void DependencySorter::sort(Component* root, std::vector<Component*>& order)
+{
+    order.clear();
+    visit(root, order);
+}
+
+void DependencySorter::sort(std::vector<Component*> roots, std::vector<Component*>& order)
+{
+    order.clear();
+    for (auto root : roots)
+    {
+        visit(root, order);
+    }
+}
+
+bool DependencySorter::visit(Component* component, std::vector<Component*>& order)
+{
+    if (m_Perm.find(component) != m_Perm.end())
+    {
+        return true;
+    }
+    if (m_Temp.find(component) != m_Temp.end())
+    {
+        fprintf(stderr, "Dependency cycle!\n");
+        return false;
+    }
+
+    m_Temp.emplace(component);
+
+    auto dependents = component->dependents();
+    for (auto dependent : dependents)
+    {
+        if (!visit(dependent, order))
+        {
+            return false;
+        }
+    }
+    m_Perm.emplace(component);
+    order.insert(order.begin(), component);
+
+    return true;
+}
\ No newline at end of file
diff --git a/src/draw_rules.cpp b/src/draw_rules.cpp
new file mode 100644
index 0000000..8731f81
--- /dev/null
+++ b/src/draw_rules.cpp
@@ -0,0 +1,38 @@
+#include "rive/draw_rules.hpp"
+#include "rive/artboard.hpp"
+#include "rive/core_context.hpp"
+#include "rive/draw_target.hpp"
+
+using namespace rive;
+
+StatusCode DrawRules::onAddedDirty(CoreContext* context)
+{
+    StatusCode result = Super::onAddedDirty(context);
+    if (result != StatusCode::Ok)
+    {
+        return result;
+    }
+    auto coreObject = context->resolve(drawTargetId());
+    if (coreObject != nullptr && coreObject->is<DrawTarget>())
+    {
+        m_ActiveTarget = static_cast<DrawTarget*>(coreObject);
+    }
+
+    return StatusCode::Ok;
+}
+
+StatusCode DrawRules::onAddedClean(CoreContext* context) { return StatusCode::Ok; }
+
+void DrawRules::drawTargetIdChanged()
+{
+    auto coreObject = artboard()->resolve(drawTargetId());
+    if (coreObject == nullptr || !coreObject->is<DrawTarget>())
+    {
+        m_ActiveTarget = nullptr;
+    }
+    else
+    {
+        m_ActiveTarget = static_cast<DrawTarget*>(coreObject);
+    }
+    artboard()->addDirt(ComponentDirt::DrawOrder);
+}
diff --git a/src/draw_target.cpp b/src/draw_target.cpp
new file mode 100644
index 0000000..04b43ec
--- /dev/null
+++ b/src/draw_target.cpp
@@ -0,0 +1,26 @@
+#include "rive/draw_target.hpp"
+#include "rive/core_context.hpp"
+#include "rive/drawable.hpp"
+#include "rive/artboard.hpp"
+
+using namespace rive;
+
+StatusCode DrawTarget::onAddedDirty(CoreContext* context)
+{
+    StatusCode code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+    auto coreObject = context->resolve(drawableId());
+    if (coreObject == nullptr || !coreObject->is<Drawable>())
+    {
+        return StatusCode::MissingObject;
+    }
+    m_Drawable = static_cast<Drawable*>(coreObject);
+    return StatusCode::Ok;
+}
+
+StatusCode DrawTarget::onAddedClean(CoreContext* context) { return StatusCode::Ok; }
+
+void DrawTarget::placementValueChanged() { artboard()->addDirt(ComponentDirt::DrawOrder); }
diff --git a/src/drawable.cpp b/src/drawable.cpp
new file mode 100644
index 0000000..0dc9ca0
--- /dev/null
+++ b/src/drawable.cpp
@@ -0,0 +1,86 @@
+#include "rive/drawable.hpp"
+#include "rive/artboard.hpp"
+#include "rive/layout_component.hpp"
+#include "rive/shapes/clipping_shape.hpp"
+#include "rive/shapes/path_composer.hpp"
+#include "rive/shapes/shape.hpp"
+#include "rive/clip_result.hpp"
+
+using namespace rive;
+
+StatusCode Drawable::onAddedDirty(CoreContext* context)
+{
+    auto code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+    auto blendMode = static_cast<rive::BlendMode>(blendModeValue());
+    switch (blendMode)
+    {
+        case rive::BlendMode::srcOver:
+        case rive::BlendMode::screen:
+        case rive::BlendMode::overlay:
+        case rive::BlendMode::darken:
+        case rive::BlendMode::lighten:
+        case rive::BlendMode::colorDodge:
+        case rive::BlendMode::colorBurn:
+        case rive::BlendMode::hardLight:
+        case rive::BlendMode::softLight:
+        case rive::BlendMode::difference:
+        case rive::BlendMode::exclusion:
+        case rive::BlendMode::multiply:
+        case rive::BlendMode::hue:
+        case rive::BlendMode::saturation:
+        case rive::BlendMode::color:
+        case rive::BlendMode::luminosity:
+            return StatusCode::Ok;
+    }
+    return StatusCode::InvalidObject;
+}
+
+void Drawable::addClippingShape(ClippingShape* shape) { m_ClippingShapes.push_back(shape); }
+
+ClipResult Drawable::applyClip(Renderer* renderer) const
+{
+    if (m_ClippingShapes.size() == 0)
+    {
+        return ClipResult::noClip;
+    }
+
+    renderer->save();
+
+    for (auto clippingShape : m_ClippingShapes)
+    {
+        if (!clippingShape->isVisible())
+        {
+            continue;
+        }
+
+        RenderPath* renderPath = clippingShape->renderPath();
+        // Can intentionally be null if all the clipping shapes are hidden.
+        if (renderPath != nullptr)
+        {
+            renderer->clipPath(renderPath);
+        }
+        else
+        {
+            // If one renderPath is null we exit early because we are treating it
+            // as an empty path and its intersection will always be an empty path
+            return ClipResult::emptyClip;
+        }
+    }
+    return ClipResult::clip;
+}
+
+bool Drawable::isChildOfLayout(LayoutComponent* layout)
+{
+    for (ContainerComponent* parent = this; parent != nullptr; parent = parent->parent())
+    {
+        if (parent->is<LayoutComponent>() && parent->as<LayoutComponent>() == layout)
+        {
+            return true;
+        }
+    }
+    return false;
+}
\ No newline at end of file
diff --git a/src/event.cpp b/src/event.cpp
new file mode 100644
index 0000000..ad1582e
--- /dev/null
+++ b/src/event.cpp
@@ -0,0 +1,11 @@
+#include "rive/event.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/artboard.hpp"
+#include "rive/importers/artboard_importer.hpp"
+
+using namespace rive;
+
+void Event::trigger(const CallbackData& value)
+{
+    value.context()->reportEvent(this, value.delaySeconds());
+}
\ No newline at end of file
diff --git a/src/factory.cpp b/src/factory.cpp
new file mode 100644
index 0000000..09c670a
--- /dev/null
+++ b/src/factory.cpp
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "rive/factory.hpp"
+#include "rive/math/aabb.hpp"
+#include "rive/math/raw_path.hpp"
+#include "rive/text/raw_text.hpp"
+#ifdef WITH_RIVE_TEXT
+#include "rive/text/font_hb.hpp"
+#endif
+
+using namespace rive;
+
+rcp<RenderPath> Factory::makeRenderPath(const AABB& r)
+{
+    RawPath rawPath;
+    rawPath.addRect(r);
+    return makeRenderPath(rawPath, FillRule::nonZero);
+}
+
+rcp<Font> Factory::decodeFont(Span<const uint8_t> span)
+{
+#ifdef WITH_RIVE_TEXT
+    return HBFont::Decode(span);
+#else
+    return nullptr;
+#endif
+}
+
+rcp<AudioSource> Factory::decodeAudio(Span<const uint8_t> span)
+{
+#ifdef WITH_RIVE_AUDIO
+    return rcp<AudioSource>(new AudioSource(SimpleArray<uint8_t>(span.data(), span.size())));
+#else
+    return nullptr;
+#endif
+}
diff --git a/src/file.cpp b/src/file.cpp
new file mode 100644
index 0000000..db3e98f
--- /dev/null
+++ b/src/file.cpp
@@ -0,0 +1,675 @@
+#include "rive/file.hpp"
+#include "rive/runtime_header.hpp"
+#include "rive/animation/animation.hpp"
+#include "rive/core/field_types/core_color_type.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+#include "rive/core/field_types/core_string_type.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+#include "rive/generated/core_registry.hpp"
+#include "rive/importers/artboard_importer.hpp"
+#include "rive/importers/backboard_importer.hpp"
+#include "rive/importers/bindable_property_importer.hpp"
+#include "rive/importers/data_converter_group_importer.hpp"
+#include "rive/importers/enum_importer.hpp"
+#include "rive/importers/file_asset_importer.hpp"
+#include "rive/importers/import_stack.hpp"
+#include "rive/importers/keyed_object_importer.hpp"
+#include "rive/importers/keyed_property_importer.hpp"
+#include "rive/importers/linear_animation_importer.hpp"
+#include "rive/importers/state_machine_importer.hpp"
+#include "rive/importers/state_machine_listener_importer.hpp"
+#include "rive/importers/state_machine_layer_importer.hpp"
+#include "rive/importers/layer_state_importer.hpp"
+#include "rive/importers/state_transition_importer.hpp"
+#include "rive/importers/state_machine_layer_component_importer.hpp"
+#include "rive/importers/transition_viewmodel_condition_importer.hpp"
+#include "rive/importers/viewmodel_importer.hpp"
+#include "rive/importers/viewmodel_instance_importer.hpp"
+#include "rive/importers/viewmodel_instance_list_importer.hpp"
+#include "rive/animation/blend_state_transition.hpp"
+#include "rive/animation/any_state.hpp"
+#include "rive/animation/entry_state.hpp"
+#include "rive/animation/exit_state.hpp"
+#include "rive/animation/animation_state.hpp"
+#include "rive/animation/blend_state_1d.hpp"
+#include "rive/animation/blend_state_direct.hpp"
+#include "rive/animation/transition_property_viewmodel_comparator.hpp"
+#include "rive/data_bind/bindable_property.hpp"
+#include "rive/data_bind/bindable_property_number.hpp"
+#include "rive/data_bind/bindable_property_string.hpp"
+#include "rive/data_bind/bindable_property_color.hpp"
+#include "rive/data_bind/bindable_property_enum.hpp"
+#include "rive/data_bind/bindable_property_boolean.hpp"
+#include "rive/data_bind/converters/data_converter_group.hpp"
+#include "rive/assets/file_asset.hpp"
+#include "rive/assets/audio_asset.hpp"
+#include "rive/assets/file_asset_contents.hpp"
+#include "rive/viewmodel/viewmodel.hpp"
+#include "rive/viewmodel/data_enum.hpp"
+#include "rive/viewmodel/viewmodel_instance.hpp"
+#include "rive/viewmodel/viewmodel_instance_list.hpp"
+#include "rive/viewmodel/viewmodel_instance_viewmodel.hpp"
+#include "rive/viewmodel/viewmodel_instance_number.hpp"
+#include "rive/viewmodel/viewmodel_instance_string.hpp"
+#include "rive/viewmodel/viewmodel_property_viewmodel.hpp"
+#include "rive/viewmodel/viewmodel_property_string.hpp"
+#include "rive/viewmodel/viewmodel_property_number.hpp"
+#include "rive/viewmodel/viewmodel_property_enum.hpp"
+#include "rive/viewmodel/viewmodel_property_list.hpp"
+
+// Default namespace for Rive Cpp code
+using namespace rive;
+
+#if !defined(RIVE_FMT_U64)
+#if defined(__ANDROID__)
+#if INTPTR_MAX == INT64_MAX
+#define RIVE_FMT_U64 "%lu"
+#define RIVE_FMT_I64 "%ld"
+#else
+#define RIVE_FMT_U64 "%llu"
+#define RIVE_FMT_I64 "%lld"
+#endif
+#elif defined(_WIN32)
+#define RIVE_FMT_U64 "%lld"
+#define RIVE_FMT_I64 "%llu"
+#else
+#include <inttypes.h>
+#define RIVE_FMT_U64 "%" PRIu64
+#define RIVE_FMT_I64 "%" PRId64
+#endif
+#endif
+
+// Import a single Rive runtime object.
+// Used by the file importer.
+static Core* readRuntimeObject(BinaryReader& reader, const RuntimeHeader& header)
+{
+    auto coreObjectKey = reader.readVarUintAs<int>();
+    auto object = CoreRegistry::makeCoreInstance(coreObjectKey);
+    while (true)
+    {
+        auto propertyKey = reader.readVarUintAs<uint16_t>();
+        if (propertyKey == 0)
+        {
+            // Terminator. https://media.giphy.com/media/7TtvTUMm9mp20/giphy.gif
+            break;
+        }
+
+        if (reader.hasError())
+        {
+            delete object;
+            return nullptr;
+        }
+        if (object == nullptr || !object->deserialize(propertyKey, reader))
+        {
+            // We have an unknown object or property, first see if core knows
+            // the property type.
+            int id = CoreRegistry::propertyFieldId(propertyKey);
+            if (id == -1)
+            {
+                // No, check if it's in toc.
+                id = header.propertyFieldId(propertyKey);
+            }
+
+            if (id == -1)
+            {
+                // Still couldn't find it, give up.
+                fprintf(stderr,
+                        "Unknown property key %d, missing from property ToC.\n",
+                        propertyKey);
+                delete object;
+                return nullptr;
+            }
+
+            switch (id)
+            {
+                case CoreUintType::id:
+                    CoreUintType::deserialize(reader);
+                    break;
+                case CoreStringType::id:
+                    CoreStringType::deserialize(reader);
+                    break;
+                case CoreDoubleType::id:
+                    CoreDoubleType::deserialize(reader);
+                    break;
+                case CoreColorType::id:
+                    CoreColorType::deserialize(reader);
+                    break;
+            }
+        }
+    }
+    if (object == nullptr)
+    {
+        // fprintf(stderr,
+        //         "File contains an unknown object with coreType " RIVE_FMT_U64
+        //         ", which " "this runtime doesn't understand.\n",
+        //         coreObjectKey);
+        return nullptr;
+    }
+    return object;
+}
+
+File::File(Factory* factory, FileAssetLoader* assetLoader) :
+    m_factory(factory), m_assetLoader(assetLoader)
+{
+    assert(factory);
+}
+
+File::~File()
+{
+    for (auto artboard : m_artboards)
+    {
+        delete artboard;
+    }
+    // Assets delete after artboards as they reference them.
+    for (auto asset : m_fileAssets)
+    {
+        delete asset;
+    }
+    delete m_backboard;
+}
+
+std::unique_ptr<File> File::import(Span<const uint8_t> bytes,
+                                   Factory* factory,
+                                   ImportResult* result,
+                                   FileAssetLoader* assetLoader)
+{
+    BinaryReader reader(bytes);
+    RuntimeHeader header;
+    if (!RuntimeHeader::read(reader, header))
+    {
+        fprintf(stderr, "Bad header\n");
+        if (result)
+        {
+            *result = ImportResult::malformed;
+        }
+        return nullptr;
+    }
+    if (header.majorVersion() != majorVersion)
+    {
+        fprintf(stderr,
+                "Unsupported version %u.%u expected %u.%u.\n",
+                header.majorVersion(),
+                header.minorVersion(),
+                majorVersion,
+                minorVersion);
+        if (result)
+        {
+            *result = ImportResult::unsupportedVersion;
+        }
+        return nullptr;
+    }
+    auto file = rivestd::make_unique<File>(factory, assetLoader);
+
+    auto readResult = file->read(reader, header);
+    if (result)
+    {
+        *result = readResult;
+    }
+    if (readResult != ImportResult::success)
+    {
+        file.reset(nullptr);
+    }
+    return file;
+}
+
+ImportResult File::read(BinaryReader& reader, const RuntimeHeader& header)
+{
+    ImportStack importStack;
+    // TODO: @hernan consider moving this to a special importer. It's not that
+    // simple because Core doesn't have a typeKey, so it should be treated as
+    // a special case. In any case, it's not that bad having it here for now.
+    Core* lastBindableObject;
+    while (!reader.reachedEnd())
+    {
+        auto object = readRuntimeObject(reader, header);
+        if (object == nullptr)
+        {
+            importStack.readNullObject();
+            continue;
+        }
+        if (!object->is<DataBind>())
+        {
+            lastBindableObject = object;
+        }
+        else if (lastBindableObject != nullptr)
+        {
+            object->as<DataBind>()->target(lastBindableObject);
+        }
+        if (object->import(importStack) == StatusCode::Ok)
+        {
+            switch (object->coreType())
+            {
+                case Backboard::typeKey:
+                    m_backboard = object->as<Backboard>();
+                    break;
+                case Artboard::typeKey:
+                {
+                    Artboard* ab = object->as<Artboard>();
+                    ab->m_Factory = m_factory;
+                    m_artboards.push_back(ab);
+                }
+                break;
+                case ImageAsset::typeKey:
+                case FontAsset::typeKey:
+                case AudioAsset::typeKey:
+                {
+                    auto fa = object->as<FileAsset>();
+                    m_fileAssets.push_back(fa);
+                }
+                break;
+                case ViewModel::typeKey:
+                {
+                    auto vmc = object->as<ViewModel>();
+                    m_ViewModels.push_back(vmc);
+                    break;
+                }
+                case DataEnum::typeKey:
+                {
+                    auto de = object->as<DataEnum>();
+                    m_Enums.push_back(de);
+                    break;
+                }
+                case ViewModelPropertyEnum::typeKey:
+                {
+                    auto vme = object->as<ViewModelPropertyEnum>();
+                    vme->dataEnum(m_Enums[vme->enumId()]);
+                }
+                break;
+            }
+        }
+        else
+        {
+            fprintf(stderr, "Failed to import object of type %d\n", object->coreType());
+            delete object;
+            continue;
+        }
+        std::unique_ptr<ImportStackObject> stackObject = nullptr;
+        auto stackType = object->coreType();
+
+        switch (stackType)
+        {
+            case Backboard::typeKey:
+                stackObject = rivestd::make_unique<BackboardImporter>(object->as<Backboard>());
+                break;
+            case Artboard::typeKey:
+                stackObject = rivestd::make_unique<ArtboardImporter>(object->as<Artboard>());
+                break;
+            case DataEnum::typeKey:
+                stackObject = rivestd::make_unique<EnumImporter>(object->as<DataEnum>());
+                break;
+            case LinearAnimation::typeKey:
+                stackObject =
+                    rivestd::make_unique<LinearAnimationImporter>(object->as<LinearAnimation>());
+                break;
+            case KeyedObject::typeKey:
+                stackObject = rivestd::make_unique<KeyedObjectImporter>(object->as<KeyedObject>());
+                break;
+            case KeyedProperty::typeKey:
+            {
+                auto importer =
+                    importStack.latest<LinearAnimationImporter>(LinearAnimation::typeKey);
+                if (importer == nullptr)
+                {
+                    return ImportResult::malformed;
+                }
+                stackObject =
+                    rivestd::make_unique<KeyedPropertyImporter>(importer->animation(),
+                                                                object->as<KeyedProperty>());
+                break;
+            }
+            case StateMachine::typeKey:
+                stackObject =
+                    rivestd::make_unique<StateMachineImporter>(object->as<StateMachine>());
+                break;
+            case StateMachineLayer::typeKey:
+            {
+                auto artboardImporter = importStack.latest<ArtboardImporter>(ArtboardBase::typeKey);
+                if (artboardImporter == nullptr)
+                {
+                    return ImportResult::malformed;
+                }
+
+                stackObject =
+                    rivestd::make_unique<StateMachineLayerImporter>(object->as<StateMachineLayer>(),
+                                                                    artboardImporter->artboard());
+
+                break;
+            }
+            case EntryState::typeKey:
+            case ExitState::typeKey:
+            case AnyState::typeKey:
+            case AnimationState::typeKey:
+            case BlendState1D::typeKey:
+            case BlendStateDirect::typeKey:
+                stackObject = rivestd::make_unique<LayerStateImporter>(object->as<LayerState>());
+                stackType = LayerState::typeKey;
+                break;
+            case StateTransition::typeKey:
+            case BlendStateTransition::typeKey:
+                stackObject =
+                    rivestd::make_unique<StateTransitionImporter>(object->as<StateTransition>());
+                stackType = StateTransition::typeKey;
+                break;
+            case StateMachineListener::typeKey:
+                stackObject = rivestd::make_unique<StateMachineListenerImporter>(
+                    object->as<StateMachineListener>());
+                break;
+            case ImageAsset::typeKey:
+            case FontAsset::typeKey:
+            case AudioAsset::typeKey:
+                stackObject = rivestd::make_unique<FileAssetImporter>(object->as<FileAsset>(),
+                                                                      m_assetLoader,
+                                                                      m_factory);
+                stackType = FileAsset::typeKey;
+                break;
+            case ViewModel::typeKey:
+                stackObject = rivestd::make_unique<ViewModelImporter>(object->as<ViewModel>());
+                stackType = ViewModel::typeKey;
+                break;
+            case ViewModelInstance::typeKey:
+                stackObject = rivestd::make_unique<ViewModelInstanceImporter>(
+                    object->as<ViewModelInstance>());
+                stackType = ViewModelInstance::typeKey;
+                break;
+            case ViewModelInstanceList::typeKey:
+                stackObject = rivestd::make_unique<ViewModelInstanceListImporter>(
+                    object->as<ViewModelInstanceList>());
+                stackType = ViewModelInstanceList::typeKey;
+                break;
+            case TransitionViewModelCondition::typeKey:
+            case TransitionArtboardCondition::typeKey:
+                stackObject = rivestd::make_unique<TransitionViewModelConditionImporter>(
+                    object->as<TransitionViewModelCondition>());
+                stackType = TransitionViewModelCondition::typeKey;
+                break;
+            case BindablePropertyNumber::typeKey:
+            case BindablePropertyString::typeKey:
+            case BindablePropertyColor::typeKey:
+            case BindablePropertyEnum::typeKey:
+            case BindablePropertyBoolean::typeKey:
+                stackObject =
+                    rivestd::make_unique<BindablePropertyImporter>(object->as<BindableProperty>());
+                stackType = BindablePropertyBase::typeKey;
+                break;
+            case DataConverterGroupBase::typeKey:
+                stackObject = rivestd::make_unique<DataConverterGroupImporter>(
+                    object->as<DataConverterGroup>());
+                stackType = DataConverterGroupBase::typeKey;
+                break;
+        }
+        if (importStack.makeLatest(stackType, std::move(stackObject)) != StatusCode::Ok)
+        {
+            // Some previous stack item didn't resolve.
+            return ImportResult::malformed;
+        }
+        if (object->is<StateMachineLayerComponent>() &&
+            importStack.makeLatest(StateMachineLayerComponent::typeKey,
+                                   rivestd::make_unique<StateMachineLayerComponentImporter>(
+                                       object->as<StateMachineLayerComponent>())) != StatusCode::Ok)
+        {
+            return ImportResult::malformed;
+        }
+    }
+
+    return !reader.hasError() && importStack.resolve() == StatusCode::Ok ? ImportResult::success
+                                                                         : ImportResult::malformed;
+}
+
+Artboard* File::artboard(std::string name) const
+{
+    for (const auto& artboard : m_artboards)
+    {
+        if (artboard->name() == name)
+        {
+            return artboard;
+        }
+    }
+    return nullptr;
+}
+
+Artboard* File::artboard() const
+{
+    if (m_artboards.empty())
+    {
+        return nullptr;
+    }
+    return m_artboards[0];
+}
+
+Artboard* File::artboard(size_t index) const
+{
+    if (index >= m_artboards.size())
+    {
+        return nullptr;
+    }
+    return m_artboards[index];
+}
+
+std::string File::artboardNameAt(size_t index) const
+{
+    auto ab = this->artboard(index);
+    return ab ? ab->name() : "";
+}
+
+std::unique_ptr<ArtboardInstance> File::artboardDefault() const
+{
+    auto ab = this->artboard();
+    return ab ? ab->instance() : nullptr;
+}
+
+std::unique_ptr<ArtboardInstance> File::artboardAt(size_t index) const
+{
+    auto ab = this->artboard(index);
+    return ab ? ab->instance() : nullptr;
+}
+
+std::unique_ptr<ArtboardInstance> File::artboardNamed(std::string name) const
+{
+    auto ab = this->artboard(name);
+    return ab ? ab->instance() : nullptr;
+}
+
+void File::completeViewModelInstance(ViewModelInstance* viewModelInstance)
+{
+    auto viewModel = m_ViewModels[viewModelInstance->viewModelId()];
+    auto propertyValues = viewModelInstance->propertyValues();
+    for (auto value : propertyValues)
+    {
+        if (value->is<ViewModelInstanceViewModel>())
+        {
+            auto property = viewModel->property(value->viewModelPropertyId());
+            if (property->is<ViewModelPropertyViewModel>())
+            {
+                auto valueViewModel = value->as<ViewModelInstanceViewModel>();
+                auto propertViewModel = property->as<ViewModelPropertyViewModel>();
+                auto viewModelReference = m_ViewModels[propertViewModel->viewModelReferenceId()];
+                auto viewModelInstance =
+                    viewModelReference->instance(valueViewModel->propertyValue());
+                if (viewModelInstance != nullptr)
+                {
+                    valueViewModel->referenceViewModelInstance(
+                        copyViewModelInstance(viewModelInstance));
+                }
+            }
+        }
+        else if (value->is<ViewModelInstanceList>())
+        {
+            auto viewModelList = value->as<ViewModelInstanceList>();
+            for (auto listItem : viewModelList->listItems())
+            {
+                auto viewModel = m_ViewModels[listItem->viewModelId()];
+                auto viewModelInstance = viewModel->instance(listItem->viewModelInstanceId());
+                listItem->viewModelInstance(copyViewModelInstance(viewModelInstance));
+                if (listItem->artboardId() < m_artboards.size())
+                {
+                    listItem->artboard(m_artboards[listItem->artboardId()]);
+                }
+            }
+        }
+        value->viewModelProperty(viewModel->property(value->viewModelPropertyId()));
+    }
+}
+
+ViewModelInstance* File::copyViewModelInstance(ViewModelInstance* viewModelInstance)
+{
+    auto copy = viewModelInstance->clone()->as<ViewModelInstance>();
+    completeViewModelInstance(copy);
+    return copy;
+}
+
+ViewModelInstance* File::createViewModelInstance(std::string name)
+{
+    for (auto viewModel : m_ViewModels)
+    {
+        if (viewModel->is<ViewModel>())
+        {
+            if (viewModel->name() == name)
+            {
+                return createViewModelInstance(viewModel);
+            }
+        }
+    }
+    return nullptr;
+}
+
+ViewModelInstance* File::createViewModelInstance(std::string name, std::string instanceName)
+{
+    for (auto viewModel : m_ViewModels)
+    {
+        if (viewModel->is<ViewModel>())
+        {
+            if (viewModel->name() == name)
+            {
+                auto instance = viewModel->instance(instanceName);
+                if (instance != nullptr)
+                {
+                    return copyViewModelInstance(instance);
+                }
+            }
+        }
+    }
+    return nullptr;
+}
+
+ViewModelInstance* File::createViewModelInstance(ViewModel* viewModel)
+{
+    if (viewModel != nullptr)
+    {
+        auto viewModelInstance = viewModel->defaultInstance();
+        return copyViewModelInstance(viewModelInstance);
+    }
+    return nullptr;
+}
+
+ViewModelInstance* File::createViewModelInstance(Artboard* artboard)
+{
+    if ((size_t)artboard->viewModelId() < m_ViewModels.size())
+    {
+        auto viewModel = m_ViewModels[artboard->viewModelId()];
+        if (viewModel != nullptr)
+        {
+            return createViewModelInstance(viewModel);
+        }
+    }
+    return nullptr;
+}
+
+ViewModelInstanceListItem* File::viewModelInstanceListItem(ViewModelInstance* viewModelInstance)
+{
+    // Search for an implicit artboard linked to the viewModel.
+    // It will return the first one it finds, but there could be more.
+    // We should decide if we want to be more restrictive and only return
+    // an artboard if one and only one is found.
+    for (auto artboard : m_artboards)
+    {
+        if (artboard->viewModelId() == viewModelInstance->viewModelId())
+        {
+            return viewModelInstanceListItem(viewModelInstance, artboard);
+        }
+    }
+    return nullptr;
+}
+
+ViewModelInstanceListItem* File::viewModelInstanceListItem(ViewModelInstance* viewModelInstance,
+                                                           Artboard* artboard)
+{
+    auto viewModelInstanceListItem = new ViewModelInstanceListItem();
+    viewModelInstanceListItem->viewModelInstance(viewModelInstance);
+    viewModelInstanceListItem->artboard(artboard);
+    return viewModelInstanceListItem;
+}
+
+ViewModel* File::viewModel(std::string name)
+{
+    for (auto viewModel : m_ViewModels)
+    {
+        if (viewModel->name() == name)
+        {
+            return viewModel;
+        }
+    }
+    return nullptr;
+}
+
+const std::vector<FileAsset*>& File::assets() const { return m_fileAssets; }
+
+#ifdef WITH_RIVE_TOOLS
+const std::vector<uint8_t> File::stripAssets(Span<const uint8_t> bytes,
+                                             std::set<uint16_t> typeKeys,
+                                             ImportResult* result)
+{
+    std::vector<uint8_t> strippedData;
+    strippedData.reserve(bytes.size());
+    BinaryReader reader(bytes);
+    RuntimeHeader header;
+    if (!RuntimeHeader::read(reader, header))
+    {
+        if (result)
+        {
+            *result = ImportResult::malformed;
+        }
+    }
+    else if (header.majorVersion() != majorVersion)
+    {
+        if (result)
+        {
+            *result = ImportResult::unsupportedVersion;
+        }
+    }
+    else
+    {
+        strippedData.insert(strippedData.end(), bytes.data(), reader.position());
+        const uint8_t* from = reader.position();
+        const uint8_t* to = reader.position();
+        uint16_t lastAssetType = 0;
+        while (!reader.reachedEnd())
+        {
+            auto object = readRuntimeObject(reader, header);
+            if (object == nullptr)
+            {
+                continue;
+            }
+            if (object->is<FileAssetBase>())
+            {
+                lastAssetType = object->coreType();
+            }
+            if (object->is<FileAssetContents>() && typeKeys.find(lastAssetType) != typeKeys.end())
+            {
+                if (from != to)
+                {
+                    strippedData.insert(strippedData.end(), from, to);
+                }
+                from = reader.position();
+            }
+            delete object;
+            to = reader.position();
+        }
+        if (from != to)
+        {
+            strippedData.insert(strippedData.end(), from, to);
+        }
+        *result = ImportResult::success;
+    }
+    return strippedData;
+}
+#endif
diff --git a/src/generated/animation/animation_base.cpp b/src/generated/animation/animation_base.cpp
new file mode 100644
index 0000000..e832aeb
--- /dev/null
+++ b/src/generated/animation/animation_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/animation_base.hpp"
+#include "rive/animation/animation.hpp"
+
+using namespace rive;
+
+Core* AnimationBase::clone() const
+{
+    auto cloned = new Animation();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/animation_state_base.cpp b/src/generated/animation/animation_state_base.cpp
new file mode 100644
index 0000000..922ea8e
--- /dev/null
+++ b/src/generated/animation/animation_state_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/animation_state_base.hpp"
+#include "rive/animation/animation_state.hpp"
+
+using namespace rive;
+
+Core* AnimationStateBase::clone() const
+{
+    auto cloned = new AnimationState();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/any_state_base.cpp b/src/generated/animation/any_state_base.cpp
new file mode 100644
index 0000000..54b2a42
--- /dev/null
+++ b/src/generated/animation/any_state_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/any_state_base.hpp"
+#include "rive/animation/any_state.hpp"
+
+using namespace rive;
+
+Core* AnyStateBase::clone() const
+{
+    auto cloned = new AnyState();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/blend_animation_1d_base.cpp b/src/generated/animation/blend_animation_1d_base.cpp
new file mode 100644
index 0000000..a04b71f
--- /dev/null
+++ b/src/generated/animation/blend_animation_1d_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/blend_animation_1d_base.hpp"
+#include "rive/animation/blend_animation_1d.hpp"
+
+using namespace rive;
+
+Core* BlendAnimation1DBase::clone() const
+{
+    auto cloned = new BlendAnimation1D();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/blend_animation_direct_base.cpp b/src/generated/animation/blend_animation_direct_base.cpp
new file mode 100644
index 0000000..98bd50a
--- /dev/null
+++ b/src/generated/animation/blend_animation_direct_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/blend_animation_direct_base.hpp"
+#include "rive/animation/blend_animation_direct.hpp"
+
+using namespace rive;
+
+Core* BlendAnimationDirectBase::clone() const
+{
+    auto cloned = new BlendAnimationDirect();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/blend_state_1d_base.cpp b/src/generated/animation/blend_state_1d_base.cpp
new file mode 100644
index 0000000..3a287ec
--- /dev/null
+++ b/src/generated/animation/blend_state_1d_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/blend_state_1d_base.hpp"
+#include "rive/animation/blend_state_1d.hpp"
+
+using namespace rive;
+
+Core* BlendState1DBase::clone() const
+{
+    auto cloned = new BlendState1D();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/blend_state_direct_base.cpp b/src/generated/animation/blend_state_direct_base.cpp
new file mode 100644
index 0000000..a0349bc
--- /dev/null
+++ b/src/generated/animation/blend_state_direct_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/blend_state_direct_base.hpp"
+#include "rive/animation/blend_state_direct.hpp"
+
+using namespace rive;
+
+Core* BlendStateDirectBase::clone() const
+{
+    auto cloned = new BlendStateDirect();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/blend_state_transition_base.cpp b/src/generated/animation/blend_state_transition_base.cpp
new file mode 100644
index 0000000..a4b74d4
--- /dev/null
+++ b/src/generated/animation/blend_state_transition_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/blend_state_transition_base.hpp"
+#include "rive/animation/blend_state_transition.hpp"
+
+using namespace rive;
+
+Core* BlendStateTransitionBase::clone() const
+{
+    auto cloned = new BlendStateTransition();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/cubic_ease_interpolator_base.cpp b/src/generated/animation/cubic_ease_interpolator_base.cpp
new file mode 100644
index 0000000..8dbda91
--- /dev/null
+++ b/src/generated/animation/cubic_ease_interpolator_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/cubic_ease_interpolator_base.hpp"
+#include "rive/animation/cubic_ease_interpolator.hpp"
+
+using namespace rive;
+
+Core* CubicEaseInterpolatorBase::clone() const
+{
+    auto cloned = new CubicEaseInterpolator();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/cubic_interpolator_component_base.cpp b/src/generated/animation/cubic_interpolator_component_base.cpp
new file mode 100644
index 0000000..4cb5db0
--- /dev/null
+++ b/src/generated/animation/cubic_interpolator_component_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/cubic_interpolator_component_base.hpp"
+#include "rive/animation/cubic_interpolator_component.hpp"
+
+using namespace rive;
+
+Core* CubicInterpolatorComponentBase::clone() const
+{
+    auto cloned = new CubicInterpolatorComponent();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/cubic_value_interpolator_base.cpp b/src/generated/animation/cubic_value_interpolator_base.cpp
new file mode 100644
index 0000000..3f64039
--- /dev/null
+++ b/src/generated/animation/cubic_value_interpolator_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/cubic_value_interpolator_base.hpp"
+#include "rive/animation/cubic_value_interpolator.hpp"
+
+using namespace rive;
+
+Core* CubicValueInterpolatorBase::clone() const
+{
+    auto cloned = new CubicValueInterpolator();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/elastic_interpolator_base.cpp b/src/generated/animation/elastic_interpolator_base.cpp
new file mode 100644
index 0000000..757cb6b
--- /dev/null
+++ b/src/generated/animation/elastic_interpolator_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/elastic_interpolator_base.hpp"
+#include "rive/animation/elastic_interpolator.hpp"
+
+using namespace rive;
+
+Core* ElasticInterpolatorBase::clone() const
+{
+    auto cloned = new ElasticInterpolator();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/entry_state_base.cpp b/src/generated/animation/entry_state_base.cpp
new file mode 100644
index 0000000..68ec90e
--- /dev/null
+++ b/src/generated/animation/entry_state_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/entry_state_base.hpp"
+#include "rive/animation/entry_state.hpp"
+
+using namespace rive;
+
+Core* EntryStateBase::clone() const
+{
+    auto cloned = new EntryState();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/exit_state_base.cpp b/src/generated/animation/exit_state_base.cpp
new file mode 100644
index 0000000..12f5ad5
--- /dev/null
+++ b/src/generated/animation/exit_state_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/exit_state_base.hpp"
+#include "rive/animation/exit_state.hpp"
+
+using namespace rive;
+
+Core* ExitStateBase::clone() const
+{
+    auto cloned = new ExitState();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/keyed_object_base.cpp b/src/generated/animation/keyed_object_base.cpp
new file mode 100644
index 0000000..57cb7a2
--- /dev/null
+++ b/src/generated/animation/keyed_object_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/keyed_object_base.hpp"
+#include "rive/animation/keyed_object.hpp"
+
+using namespace rive;
+
+Core* KeyedObjectBase::clone() const
+{
+    auto cloned = new KeyedObject();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/keyed_property_base.cpp b/src/generated/animation/keyed_property_base.cpp
new file mode 100644
index 0000000..fe175e5
--- /dev/null
+++ b/src/generated/animation/keyed_property_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/keyed_property_base.hpp"
+#include "rive/animation/keyed_property.hpp"
+
+using namespace rive;
+
+Core* KeyedPropertyBase::clone() const
+{
+    auto cloned = new KeyedProperty();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/keyframe_bool_base.cpp b/src/generated/animation/keyframe_bool_base.cpp
new file mode 100644
index 0000000..72b141d
--- /dev/null
+++ b/src/generated/animation/keyframe_bool_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/keyframe_bool_base.hpp"
+#include "rive/animation/keyframe_bool.hpp"
+
+using namespace rive;
+
+Core* KeyFrameBoolBase::clone() const
+{
+    auto cloned = new KeyFrameBool();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/keyframe_callback_base.cpp b/src/generated/animation/keyframe_callback_base.cpp
new file mode 100644
index 0000000..d68eefb
--- /dev/null
+++ b/src/generated/animation/keyframe_callback_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/keyframe_callback_base.hpp"
+#include "rive/animation/keyframe_callback.hpp"
+
+using namespace rive;
+
+Core* KeyFrameCallbackBase::clone() const
+{
+    auto cloned = new KeyFrameCallback();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/keyframe_color_base.cpp b/src/generated/animation/keyframe_color_base.cpp
new file mode 100644
index 0000000..93d2a15
--- /dev/null
+++ b/src/generated/animation/keyframe_color_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/keyframe_color_base.hpp"
+#include "rive/animation/keyframe_color.hpp"
+
+using namespace rive;
+
+Core* KeyFrameColorBase::clone() const
+{
+    auto cloned = new KeyFrameColor();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/keyframe_double_base.cpp b/src/generated/animation/keyframe_double_base.cpp
new file mode 100644
index 0000000..b991a0e
--- /dev/null
+++ b/src/generated/animation/keyframe_double_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/keyframe_double_base.hpp"
+#include "rive/animation/keyframe_double.hpp"
+
+using namespace rive;
+
+Core* KeyFrameDoubleBase::clone() const
+{
+    auto cloned = new KeyFrameDouble();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/keyframe_id_base.cpp b/src/generated/animation/keyframe_id_base.cpp
new file mode 100644
index 0000000..a228b47
--- /dev/null
+++ b/src/generated/animation/keyframe_id_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/keyframe_id_base.hpp"
+#include "rive/animation/keyframe_id.hpp"
+
+using namespace rive;
+
+Core* KeyFrameIdBase::clone() const
+{
+    auto cloned = new KeyFrameId();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/keyframe_string_base.cpp b/src/generated/animation/keyframe_string_base.cpp
new file mode 100644
index 0000000..ff1fa86
--- /dev/null
+++ b/src/generated/animation/keyframe_string_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/keyframe_string_base.hpp"
+#include "rive/animation/keyframe_string.hpp"
+
+using namespace rive;
+
+Core* KeyFrameStringBase::clone() const
+{
+    auto cloned = new KeyFrameString();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/keyframe_uint_base.cpp b/src/generated/animation/keyframe_uint_base.cpp
new file mode 100644
index 0000000..35ddbed
--- /dev/null
+++ b/src/generated/animation/keyframe_uint_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/keyframe_uint_base.hpp"
+#include "rive/animation/keyframe_uint.hpp"
+
+using namespace rive;
+
+Core* KeyFrameUintBase::clone() const
+{
+    auto cloned = new KeyFrameUint();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/linear_animation_base.cpp b/src/generated/animation/linear_animation_base.cpp
new file mode 100644
index 0000000..b55beef
--- /dev/null
+++ b/src/generated/animation/linear_animation_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/linear_animation_base.hpp"
+#include "rive/animation/linear_animation.hpp"
+
+using namespace rive;
+
+Core* LinearAnimationBase::clone() const
+{
+    auto cloned = new LinearAnimation();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/listener_align_target_base.cpp b/src/generated/animation/listener_align_target_base.cpp
new file mode 100644
index 0000000..f0c1c95
--- /dev/null
+++ b/src/generated/animation/listener_align_target_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/listener_align_target_base.hpp"
+#include "rive/animation/listener_align_target.hpp"
+
+using namespace rive;
+
+Core* ListenerAlignTargetBase::clone() const
+{
+    auto cloned = new ListenerAlignTarget();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/listener_bool_change_base.cpp b/src/generated/animation/listener_bool_change_base.cpp
new file mode 100644
index 0000000..3cd4cc3
--- /dev/null
+++ b/src/generated/animation/listener_bool_change_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/listener_bool_change_base.hpp"
+#include "rive/animation/listener_bool_change.hpp"
+
+using namespace rive;
+
+Core* ListenerBoolChangeBase::clone() const
+{
+    auto cloned = new ListenerBoolChange();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/listener_fire_event_base.cpp b/src/generated/animation/listener_fire_event_base.cpp
new file mode 100644
index 0000000..e7ae04f
--- /dev/null
+++ b/src/generated/animation/listener_fire_event_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/listener_fire_event_base.hpp"
+#include "rive/animation/listener_fire_event.hpp"
+
+using namespace rive;
+
+Core* ListenerFireEventBase::clone() const
+{
+    auto cloned = new ListenerFireEvent();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/listener_number_change_base.cpp b/src/generated/animation/listener_number_change_base.cpp
new file mode 100644
index 0000000..55d6212
--- /dev/null
+++ b/src/generated/animation/listener_number_change_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/listener_number_change_base.hpp"
+#include "rive/animation/listener_number_change.hpp"
+
+using namespace rive;
+
+Core* ListenerNumberChangeBase::clone() const
+{
+    auto cloned = new ListenerNumberChange();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/listener_trigger_change_base.cpp b/src/generated/animation/listener_trigger_change_base.cpp
new file mode 100644
index 0000000..4ead315
--- /dev/null
+++ b/src/generated/animation/listener_trigger_change_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/listener_trigger_change_base.hpp"
+#include "rive/animation/listener_trigger_change.hpp"
+
+using namespace rive;
+
+Core* ListenerTriggerChangeBase::clone() const
+{
+    auto cloned = new ListenerTriggerChange();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/listener_viewmodel_change_base.cpp b/src/generated/animation/listener_viewmodel_change_base.cpp
new file mode 100644
index 0000000..96d9e9a
--- /dev/null
+++ b/src/generated/animation/listener_viewmodel_change_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/listener_viewmodel_change_base.hpp"
+#include "rive/animation/listener_viewmodel_change.hpp"
+
+using namespace rive;
+
+Core* ListenerViewModelChangeBase::clone() const
+{
+    auto cloned = new ListenerViewModelChange();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/nested_bool_base.cpp b/src/generated/animation/nested_bool_base.cpp
new file mode 100644
index 0000000..f21b56c
--- /dev/null
+++ b/src/generated/animation/nested_bool_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/nested_bool_base.hpp"
+#include "rive/animation/nested_bool.hpp"
+
+using namespace rive;
+
+Core* NestedBoolBase::clone() const
+{
+    auto cloned = new NestedBool();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/nested_number_base.cpp b/src/generated/animation/nested_number_base.cpp
new file mode 100644
index 0000000..6e5613c
--- /dev/null
+++ b/src/generated/animation/nested_number_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/nested_number_base.hpp"
+#include "rive/animation/nested_number.hpp"
+
+using namespace rive;
+
+Core* NestedNumberBase::clone() const
+{
+    auto cloned = new NestedNumber();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/nested_remap_animation_base.cpp b/src/generated/animation/nested_remap_animation_base.cpp
new file mode 100644
index 0000000..ff0a6c6
--- /dev/null
+++ b/src/generated/animation/nested_remap_animation_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/nested_remap_animation_base.hpp"
+#include "rive/animation/nested_remap_animation.hpp"
+
+using namespace rive;
+
+Core* NestedRemapAnimationBase::clone() const
+{
+    auto cloned = new NestedRemapAnimation();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/nested_simple_animation_base.cpp b/src/generated/animation/nested_simple_animation_base.cpp
new file mode 100644
index 0000000..283d353
--- /dev/null
+++ b/src/generated/animation/nested_simple_animation_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/nested_simple_animation_base.hpp"
+#include "rive/animation/nested_simple_animation.hpp"
+
+using namespace rive;
+
+Core* NestedSimpleAnimationBase::clone() const
+{
+    auto cloned = new NestedSimpleAnimation();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/nested_state_machine_base.cpp b/src/generated/animation/nested_state_machine_base.cpp
new file mode 100644
index 0000000..ef221f1
--- /dev/null
+++ b/src/generated/animation/nested_state_machine_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/nested_state_machine_base.hpp"
+#include "rive/animation/nested_state_machine.hpp"
+
+using namespace rive;
+
+Core* NestedStateMachineBase::clone() const
+{
+    auto cloned = new NestedStateMachine();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/nested_trigger_base.cpp b/src/generated/animation/nested_trigger_base.cpp
new file mode 100644
index 0000000..526737b
--- /dev/null
+++ b/src/generated/animation/nested_trigger_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/nested_trigger_base.hpp"
+#include "rive/animation/nested_trigger.hpp"
+
+using namespace rive;
+
+Core* NestedTriggerBase::clone() const
+{
+    auto cloned = new NestedTrigger();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/state_machine_base.cpp b/src/generated/animation/state_machine_base.cpp
new file mode 100644
index 0000000..7d34670
--- /dev/null
+++ b/src/generated/animation/state_machine_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/state_machine_base.hpp"
+#include "rive/animation/state_machine.hpp"
+
+using namespace rive;
+
+Core* StateMachineBase::clone() const
+{
+    auto cloned = new StateMachine();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/state_machine_bool_base.cpp b/src/generated/animation/state_machine_bool_base.cpp
new file mode 100644
index 0000000..3db764d
--- /dev/null
+++ b/src/generated/animation/state_machine_bool_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/state_machine_bool_base.hpp"
+#include "rive/animation/state_machine_bool.hpp"
+
+using namespace rive;
+
+Core* StateMachineBoolBase::clone() const
+{
+    auto cloned = new StateMachineBool();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/state_machine_fire_event.cpp b/src/generated/animation/state_machine_fire_event.cpp
new file mode 100644
index 0000000..9513eb7
--- /dev/null
+++ b/src/generated/animation/state_machine_fire_event.cpp
@@ -0,0 +1,30 @@
+#include "rive/generated/animation/state_machine_fire_event_base.hpp"
+#include "rive/animation/state_machine_fire_event.hpp"
+#include "rive/animation/state_machine_layer_component.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/event.hpp"
+#include "rive/importers/state_machine_layer_component_importer.hpp"
+
+using namespace rive;
+
+StatusCode StateMachineFireEvent::import(ImportStack& importStack)
+{
+    auto stateImporter =
+        importStack.latest<StateMachineLayerComponentImporter>(StateMachineLayerComponent::typeKey);
+    if (stateImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    stateImporter->addFireEvent(this);
+    return Super::import(importStack);
+}
+
+void StateMachineFireEvent::perform(StateMachineInstance* stateMachineInstance) const
+{
+    auto coreEvent = stateMachineInstance->artboard()->resolve(eventId());
+    if (coreEvent == nullptr || !coreEvent->is<Event>())
+    {
+        return;
+    }
+    stateMachineInstance->reportEvent(coreEvent->as<Event>());
+}
\ No newline at end of file
diff --git a/src/generated/animation/state_machine_fire_event_base.cpp b/src/generated/animation/state_machine_fire_event_base.cpp
new file mode 100644
index 0000000..b43b331
--- /dev/null
+++ b/src/generated/animation/state_machine_fire_event_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/state_machine_fire_event_base.hpp"
+#include "rive/animation/state_machine_fire_event.hpp"
+
+using namespace rive;
+
+Core* StateMachineFireEventBase::clone() const
+{
+    auto cloned = new StateMachineFireEvent();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/state_machine_layer_base.cpp b/src/generated/animation/state_machine_layer_base.cpp
new file mode 100644
index 0000000..acafe6e
--- /dev/null
+++ b/src/generated/animation/state_machine_layer_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/state_machine_layer_base.hpp"
+#include "rive/animation/state_machine_layer.hpp"
+
+using namespace rive;
+
+Core* StateMachineLayerBase::clone() const
+{
+    auto cloned = new StateMachineLayer();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/state_machine_listener_base.cpp b/src/generated/animation/state_machine_listener_base.cpp
new file mode 100644
index 0000000..37e95cf
--- /dev/null
+++ b/src/generated/animation/state_machine_listener_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/state_machine_listener_base.hpp"
+#include "rive/animation/state_machine_listener.hpp"
+
+using namespace rive;
+
+Core* StateMachineListenerBase::clone() const
+{
+    auto cloned = new StateMachineListener();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/state_machine_number_base.cpp b/src/generated/animation/state_machine_number_base.cpp
new file mode 100644
index 0000000..b821aa4
--- /dev/null
+++ b/src/generated/animation/state_machine_number_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/state_machine_number_base.hpp"
+#include "rive/animation/state_machine_number.hpp"
+
+using namespace rive;
+
+Core* StateMachineNumberBase::clone() const
+{
+    auto cloned = new StateMachineNumber();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/state_machine_trigger_base.cpp b/src/generated/animation/state_machine_trigger_base.cpp
new file mode 100644
index 0000000..d7890be
--- /dev/null
+++ b/src/generated/animation/state_machine_trigger_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/state_machine_trigger_base.hpp"
+#include "rive/animation/state_machine_trigger.hpp"
+
+using namespace rive;
+
+Core* StateMachineTriggerBase::clone() const
+{
+    auto cloned = new StateMachineTrigger();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/state_transition_base.cpp b/src/generated/animation/state_transition_base.cpp
new file mode 100644
index 0000000..f535fbd
--- /dev/null
+++ b/src/generated/animation/state_transition_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/state_transition_base.hpp"
+#include "rive/animation/state_transition.hpp"
+
+using namespace rive;
+
+Core* StateTransitionBase::clone() const
+{
+    auto cloned = new StateTransition();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/transition_artboard_condition_base.cpp b/src/generated/animation/transition_artboard_condition_base.cpp
new file mode 100644
index 0000000..4adb3c8
--- /dev/null
+++ b/src/generated/animation/transition_artboard_condition_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/transition_artboard_condition_base.hpp"
+#include "rive/animation/transition_artboard_condition.hpp"
+
+using namespace rive;
+
+Core* TransitionArtboardConditionBase::clone() const
+{
+    auto cloned = new TransitionArtboardCondition();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/transition_bool_condition_base.cpp b/src/generated/animation/transition_bool_condition_base.cpp
new file mode 100644
index 0000000..3b92f3d
--- /dev/null
+++ b/src/generated/animation/transition_bool_condition_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/transition_bool_condition_base.hpp"
+#include "rive/animation/transition_bool_condition.hpp"
+
+using namespace rive;
+
+Core* TransitionBoolConditionBase::clone() const
+{
+    auto cloned = new TransitionBoolCondition();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/transition_number_condition_base.cpp b/src/generated/animation/transition_number_condition_base.cpp
new file mode 100644
index 0000000..537addf
--- /dev/null
+++ b/src/generated/animation/transition_number_condition_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/transition_number_condition_base.hpp"
+#include "rive/animation/transition_number_condition.hpp"
+
+using namespace rive;
+
+Core* TransitionNumberConditionBase::clone() const
+{
+    auto cloned = new TransitionNumberCondition();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/transition_property_artboard_comparator_base.cpp b/src/generated/animation/transition_property_artboard_comparator_base.cpp
new file mode 100644
index 0000000..31a068a
--- /dev/null
+++ b/src/generated/animation/transition_property_artboard_comparator_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/transition_property_artboard_comparator_base.hpp"
+#include "rive/animation/transition_property_artboard_comparator.hpp"
+
+using namespace rive;
+
+Core* TransitionPropertyArtboardComparatorBase::clone() const
+{
+    auto cloned = new TransitionPropertyArtboardComparator();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/transition_property_viewmodel_comparator_base.cpp b/src/generated/animation/transition_property_viewmodel_comparator_base.cpp
new file mode 100644
index 0000000..8c0c2d6
--- /dev/null
+++ b/src/generated/animation/transition_property_viewmodel_comparator_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/transition_property_viewmodel_comparator_base.hpp"
+#include "rive/animation/transition_property_viewmodel_comparator.hpp"
+
+using namespace rive;
+
+Core* TransitionPropertyViewModelComparatorBase::clone() const
+{
+    auto cloned = new TransitionPropertyViewModelComparator();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/transition_trigger_condition_base.cpp b/src/generated/animation/transition_trigger_condition_base.cpp
new file mode 100644
index 0000000..31dadd3
--- /dev/null
+++ b/src/generated/animation/transition_trigger_condition_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/transition_trigger_condition_base.hpp"
+#include "rive/animation/transition_trigger_condition.hpp"
+
+using namespace rive;
+
+Core* TransitionTriggerConditionBase::clone() const
+{
+    auto cloned = new TransitionTriggerCondition();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/transition_value_boolean_comparator_base.cpp b/src/generated/animation/transition_value_boolean_comparator_base.cpp
new file mode 100644
index 0000000..3e9a2ef
--- /dev/null
+++ b/src/generated/animation/transition_value_boolean_comparator_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/transition_value_boolean_comparator_base.hpp"
+#include "rive/animation/transition_value_boolean_comparator.hpp"
+
+using namespace rive;
+
+Core* TransitionValueBooleanComparatorBase::clone() const
+{
+    auto cloned = new TransitionValueBooleanComparator();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/transition_value_color_comparator_base.cpp b/src/generated/animation/transition_value_color_comparator_base.cpp
new file mode 100644
index 0000000..a199f5f
--- /dev/null
+++ b/src/generated/animation/transition_value_color_comparator_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/transition_value_color_comparator_base.hpp"
+#include "rive/animation/transition_value_color_comparator.hpp"
+
+using namespace rive;
+
+Core* TransitionValueColorComparatorBase::clone() const
+{
+    auto cloned = new TransitionValueColorComparator();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/transition_value_enum_comparator_base.cpp b/src/generated/animation/transition_value_enum_comparator_base.cpp
new file mode 100644
index 0000000..dad8789
--- /dev/null
+++ b/src/generated/animation/transition_value_enum_comparator_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/transition_value_enum_comparator_base.hpp"
+#include "rive/animation/transition_value_enum_comparator.hpp"
+
+using namespace rive;
+
+Core* TransitionValueEnumComparatorBase::clone() const
+{
+    auto cloned = new TransitionValueEnumComparator();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/transition_value_number_comparator_base.cpp b/src/generated/animation/transition_value_number_comparator_base.cpp
new file mode 100644
index 0000000..8ebecac
--- /dev/null
+++ b/src/generated/animation/transition_value_number_comparator_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/transition_value_number_comparator_base.hpp"
+#include "rive/animation/transition_value_number_comparator.hpp"
+
+using namespace rive;
+
+Core* TransitionValueNumberComparatorBase::clone() const
+{
+    auto cloned = new TransitionValueNumberComparator();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/transition_value_string_comparator_base.cpp b/src/generated/animation/transition_value_string_comparator_base.cpp
new file mode 100644
index 0000000..07db01d
--- /dev/null
+++ b/src/generated/animation/transition_value_string_comparator_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/transition_value_string_comparator_base.hpp"
+#include "rive/animation/transition_value_string_comparator.hpp"
+
+using namespace rive;
+
+Core* TransitionValueStringComparatorBase::clone() const
+{
+    auto cloned = new TransitionValueStringComparator();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/animation/transition_viewmodel_condition_base.cpp b/src/generated/animation/transition_viewmodel_condition_base.cpp
new file mode 100644
index 0000000..a0d3b25
--- /dev/null
+++ b/src/generated/animation/transition_viewmodel_condition_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/transition_viewmodel_condition_base.hpp"
+#include "rive/animation/transition_viewmodel_condition.hpp"
+
+using namespace rive;
+
+Core* TransitionViewModelConditionBase::clone() const
+{
+    auto cloned = new TransitionViewModelCondition();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/artboard_base.cpp b/src/generated/artboard_base.cpp
new file mode 100644
index 0000000..612c270
--- /dev/null
+++ b/src/generated/artboard_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/artboard_base.hpp"
+#include "rive/artboard.hpp"
+
+using namespace rive;
+
+Core* ArtboardBase::clone() const
+{
+    auto cloned = new Artboard();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/assets/audio_asset_base.cpp b/src/generated/assets/audio_asset_base.cpp
new file mode 100644
index 0000000..0b7504d
--- /dev/null
+++ b/src/generated/assets/audio_asset_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/assets/audio_asset_base.hpp"
+#include "rive/assets/audio_asset.hpp"
+
+using namespace rive;
+
+Core* AudioAssetBase::clone() const
+{
+    auto cloned = new AudioAsset();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/assets/file_asset_contents_base.cpp b/src/generated/assets/file_asset_contents_base.cpp
new file mode 100644
index 0000000..8d0e376
--- /dev/null
+++ b/src/generated/assets/file_asset_contents_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/assets/file_asset_contents_base.hpp"
+#include "rive/assets/file_asset_contents.hpp"
+
+using namespace rive;
+
+Core* FileAssetContentsBase::clone() const
+{
+    auto cloned = new FileAssetContents();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/assets/folder_base.cpp b/src/generated/assets/folder_base.cpp
new file mode 100644
index 0000000..ba27d04
--- /dev/null
+++ b/src/generated/assets/folder_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/assets/folder_base.hpp"
+#include "rive/assets/folder.hpp"
+
+using namespace rive;
+
+Core* FolderBase::clone() const
+{
+    auto cloned = new Folder();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/assets/font_asset_base.cpp b/src/generated/assets/font_asset_base.cpp
new file mode 100644
index 0000000..95101bd
--- /dev/null
+++ b/src/generated/assets/font_asset_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/assets/font_asset_base.hpp"
+#include "rive/assets/font_asset.hpp"
+
+using namespace rive;
+
+Core* FontAssetBase::clone() const
+{
+    auto cloned = new FontAsset();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/assets/image_asset_base.cpp b/src/generated/assets/image_asset_base.cpp
new file mode 100644
index 0000000..6b32b1d
--- /dev/null
+++ b/src/generated/assets/image_asset_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/assets/image_asset_base.hpp"
+#include "rive/assets/image_asset.hpp"
+
+using namespace rive;
+
+Core* ImageAssetBase::clone() const
+{
+    auto cloned = new ImageAsset();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/audio_event_base.cpp b/src/generated/audio_event_base.cpp
new file mode 100644
index 0000000..83ef474
--- /dev/null
+++ b/src/generated/audio_event_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/audio_event_base.hpp"
+#include "rive/audio_event.hpp"
+
+using namespace rive;
+
+Core* AudioEventBase::clone() const
+{
+    auto cloned = new AudioEvent();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/backboard_base.cpp b/src/generated/backboard_base.cpp
new file mode 100644
index 0000000..c5dfd7a
--- /dev/null
+++ b/src/generated/backboard_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/backboard_base.hpp"
+#include "rive/backboard.hpp"
+
+using namespace rive;
+
+Core* BackboardBase::clone() const
+{
+    auto cloned = new Backboard();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/bones/bone_base.cpp b/src/generated/bones/bone_base.cpp
new file mode 100644
index 0000000..0ad594f
--- /dev/null
+++ b/src/generated/bones/bone_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/bones/bone_base.hpp"
+#include "rive/bones/bone.hpp"
+
+using namespace rive;
+
+Core* BoneBase::clone() const
+{
+    auto cloned = new Bone();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/bones/cubic_weight_base.cpp b/src/generated/bones/cubic_weight_base.cpp
new file mode 100644
index 0000000..4192b74
--- /dev/null
+++ b/src/generated/bones/cubic_weight_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/bones/cubic_weight_base.hpp"
+#include "rive/bones/cubic_weight.hpp"
+
+using namespace rive;
+
+Core* CubicWeightBase::clone() const
+{
+    auto cloned = new CubicWeight();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/bones/root_bone_base.cpp b/src/generated/bones/root_bone_base.cpp
new file mode 100644
index 0000000..faf1c91
--- /dev/null
+++ b/src/generated/bones/root_bone_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/bones/root_bone_base.hpp"
+#include "rive/bones/root_bone.hpp"
+
+using namespace rive;
+
+Core* RootBoneBase::clone() const
+{
+    auto cloned = new RootBone();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/bones/skin_base.cpp b/src/generated/bones/skin_base.cpp
new file mode 100644
index 0000000..401c65f
--- /dev/null
+++ b/src/generated/bones/skin_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/bones/skin_base.hpp"
+#include "rive/bones/skin.hpp"
+
+using namespace rive;
+
+Core* SkinBase::clone() const
+{
+    auto cloned = new Skin();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/bones/tendon_base.cpp b/src/generated/bones/tendon_base.cpp
new file mode 100644
index 0000000..866209b
--- /dev/null
+++ b/src/generated/bones/tendon_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/bones/tendon_base.hpp"
+#include "rive/bones/tendon.hpp"
+
+using namespace rive;
+
+Core* TendonBase::clone() const
+{
+    auto cloned = new Tendon();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/bones/weight_base.cpp b/src/generated/bones/weight_base.cpp
new file mode 100644
index 0000000..f62cb02
--- /dev/null
+++ b/src/generated/bones/weight_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/bones/weight_base.hpp"
+#include "rive/bones/weight.hpp"
+
+using namespace rive;
+
+Core* WeightBase::clone() const
+{
+    auto cloned = new Weight();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/constraints/distance_constraint_base.cpp b/src/generated/constraints/distance_constraint_base.cpp
new file mode 100644
index 0000000..457fe8c
--- /dev/null
+++ b/src/generated/constraints/distance_constraint_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/constraints/distance_constraint_base.hpp"
+#include "rive/constraints/distance_constraint.hpp"
+
+using namespace rive;
+
+Core* DistanceConstraintBase::clone() const
+{
+    auto cloned = new DistanceConstraint();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/constraints/follow_path_constraint_base.cpp b/src/generated/constraints/follow_path_constraint_base.cpp
new file mode 100644
index 0000000..c35b2cf
--- /dev/null
+++ b/src/generated/constraints/follow_path_constraint_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/constraints/follow_path_constraint_base.hpp"
+#include "rive/constraints/follow_path_constraint.hpp"
+
+using namespace rive;
+
+Core* FollowPathConstraintBase::clone() const
+{
+    auto cloned = new FollowPathConstraint();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/constraints/ik_constraint_base.cpp b/src/generated/constraints/ik_constraint_base.cpp
new file mode 100644
index 0000000..8a16dac
--- /dev/null
+++ b/src/generated/constraints/ik_constraint_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/constraints/ik_constraint_base.hpp"
+#include "rive/constraints/ik_constraint.hpp"
+
+using namespace rive;
+
+Core* IKConstraintBase::clone() const
+{
+    auto cloned = new IKConstraint();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/constraints/rotation_constraint_base.cpp b/src/generated/constraints/rotation_constraint_base.cpp
new file mode 100644
index 0000000..870570f
--- /dev/null
+++ b/src/generated/constraints/rotation_constraint_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/constraints/rotation_constraint_base.hpp"
+#include "rive/constraints/rotation_constraint.hpp"
+
+using namespace rive;
+
+Core* RotationConstraintBase::clone() const
+{
+    auto cloned = new RotationConstraint();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/constraints/scale_constraint_base.cpp b/src/generated/constraints/scale_constraint_base.cpp
new file mode 100644
index 0000000..612fcd4
--- /dev/null
+++ b/src/generated/constraints/scale_constraint_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/constraints/scale_constraint_base.hpp"
+#include "rive/constraints/scale_constraint.hpp"
+
+using namespace rive;
+
+Core* ScaleConstraintBase::clone() const
+{
+    auto cloned = new ScaleConstraint();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/constraints/transform_constraint_base.cpp b/src/generated/constraints/transform_constraint_base.cpp
new file mode 100644
index 0000000..0c594f0
--- /dev/null
+++ b/src/generated/constraints/transform_constraint_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/constraints/transform_constraint_base.hpp"
+#include "rive/constraints/transform_constraint.hpp"
+
+using namespace rive;
+
+Core* TransformConstraintBase::clone() const
+{
+    auto cloned = new TransformConstraint();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/constraints/translation_constraint_base.cpp b/src/generated/constraints/translation_constraint_base.cpp
new file mode 100644
index 0000000..3de3c2a
--- /dev/null
+++ b/src/generated/constraints/translation_constraint_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/constraints/translation_constraint_base.hpp"
+#include "rive/constraints/translation_constraint.hpp"
+
+using namespace rive;
+
+Core* TranslationConstraintBase::clone() const
+{
+    auto cloned = new TranslationConstraint();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/custom_property_boolean_base.cpp b/src/generated/custom_property_boolean_base.cpp
new file mode 100644
index 0000000..b999ee8
--- /dev/null
+++ b/src/generated/custom_property_boolean_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/custom_property_boolean_base.hpp"
+#include "rive/custom_property_boolean.hpp"
+
+using namespace rive;
+
+Core* CustomPropertyBooleanBase::clone() const
+{
+    auto cloned = new CustomPropertyBoolean();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/custom_property_number_base.cpp b/src/generated/custom_property_number_base.cpp
new file mode 100644
index 0000000..e1d0dd8
--- /dev/null
+++ b/src/generated/custom_property_number_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/custom_property_number_base.hpp"
+#include "rive/custom_property_number.hpp"
+
+using namespace rive;
+
+Core* CustomPropertyNumberBase::clone() const
+{
+    auto cloned = new CustomPropertyNumber();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/custom_property_string_base.cpp b/src/generated/custom_property_string_base.cpp
new file mode 100644
index 0000000..c7e27ec
--- /dev/null
+++ b/src/generated/custom_property_string_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/custom_property_string_base.hpp"
+#include "rive/custom_property_string.hpp"
+
+using namespace rive;
+
+Core* CustomPropertyStringBase::clone() const
+{
+    auto cloned = new CustomPropertyString();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/data_bind/bindable_property_boolean_base.cpp b/src/generated/data_bind/bindable_property_boolean_base.cpp
new file mode 100644
index 0000000..4e3dcc2
--- /dev/null
+++ b/src/generated/data_bind/bindable_property_boolean_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/data_bind/bindable_property_boolean_base.hpp"
+#include "rive/data_bind/bindable_property_boolean.hpp"
+
+using namespace rive;
+
+Core* BindablePropertyBooleanBase::clone() const
+{
+    auto cloned = new BindablePropertyBoolean();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/data_bind/bindable_property_color_base.cpp b/src/generated/data_bind/bindable_property_color_base.cpp
new file mode 100644
index 0000000..fa64a07
--- /dev/null
+++ b/src/generated/data_bind/bindable_property_color_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/data_bind/bindable_property_color_base.hpp"
+#include "rive/data_bind/bindable_property_color.hpp"
+
+using namespace rive;
+
+Core* BindablePropertyColorBase::clone() const
+{
+    auto cloned = new BindablePropertyColor();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/data_bind/bindable_property_enum_base.cpp b/src/generated/data_bind/bindable_property_enum_base.cpp
new file mode 100644
index 0000000..acd5be6
--- /dev/null
+++ b/src/generated/data_bind/bindable_property_enum_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/data_bind/bindable_property_enum_base.hpp"
+#include "rive/data_bind/bindable_property_enum.hpp"
+
+using namespace rive;
+
+Core* BindablePropertyEnumBase::clone() const
+{
+    auto cloned = new BindablePropertyEnum();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/data_bind/bindable_property_number_base.cpp b/src/generated/data_bind/bindable_property_number_base.cpp
new file mode 100644
index 0000000..95b0453
--- /dev/null
+++ b/src/generated/data_bind/bindable_property_number_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/data_bind/bindable_property_number_base.hpp"
+#include "rive/data_bind/bindable_property_number.hpp"
+
+using namespace rive;
+
+Core* BindablePropertyNumberBase::clone() const
+{
+    auto cloned = new BindablePropertyNumber();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/data_bind/bindable_property_string_base.cpp b/src/generated/data_bind/bindable_property_string_base.cpp
new file mode 100644
index 0000000..04133c7
--- /dev/null
+++ b/src/generated/data_bind/bindable_property_string_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/data_bind/bindable_property_string_base.hpp"
+#include "rive/data_bind/bindable_property_string.hpp"
+
+using namespace rive;
+
+Core* BindablePropertyStringBase::clone() const
+{
+    auto cloned = new BindablePropertyString();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/data_bind/converters/data_converter_group_base.cpp b/src/generated/data_bind/converters/data_converter_group_base.cpp
new file mode 100644
index 0000000..23ee770
--- /dev/null
+++ b/src/generated/data_bind/converters/data_converter_group_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/data_bind/converters/data_converter_group_base.hpp"
+#include "rive/data_bind/converters/data_converter_group.hpp"
+
+using namespace rive;
+
+Core* DataConverterGroupBase::clone() const
+{
+    auto cloned = new DataConverterGroup();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/data_bind/converters/data_converter_group_item_base.cpp b/src/generated/data_bind/converters/data_converter_group_item_base.cpp
new file mode 100644
index 0000000..8f1e17b
--- /dev/null
+++ b/src/generated/data_bind/converters/data_converter_group_item_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/data_bind/converters/data_converter_group_item_base.hpp"
+#include "rive/data_bind/converters/data_converter_group_item.hpp"
+
+using namespace rive;
+
+Core* DataConverterGroupItemBase::clone() const
+{
+    auto cloned = new DataConverterGroupItem();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/data_bind/converters/data_converter_operation_base.cpp b/src/generated/data_bind/converters/data_converter_operation_base.cpp
new file mode 100644
index 0000000..77ee84b
--- /dev/null
+++ b/src/generated/data_bind/converters/data_converter_operation_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/data_bind/converters/data_converter_operation_base.hpp"
+#include "rive/data_bind/converters/data_converter_operation.hpp"
+
+using namespace rive;
+
+Core* DataConverterOperationBase::clone() const
+{
+    auto cloned = new DataConverterOperation();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/data_bind/converters/data_converter_rounder_base.cpp b/src/generated/data_bind/converters/data_converter_rounder_base.cpp
new file mode 100644
index 0000000..e6a03a3
--- /dev/null
+++ b/src/generated/data_bind/converters/data_converter_rounder_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/data_bind/converters/data_converter_rounder_base.hpp"
+#include "rive/data_bind/converters/data_converter_rounder.hpp"
+
+using namespace rive;
+
+Core* DataConverterRounderBase::clone() const
+{
+    auto cloned = new DataConverterRounder();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/data_bind/converters/data_converter_to_string_base.cpp b/src/generated/data_bind/converters/data_converter_to_string_base.cpp
new file mode 100644
index 0000000..c600a8d
--- /dev/null
+++ b/src/generated/data_bind/converters/data_converter_to_string_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/data_bind/converters/data_converter_to_string_base.hpp"
+#include "rive/data_bind/converters/data_converter_to_string.hpp"
+
+using namespace rive;
+
+Core* DataConverterToStringBase::clone() const
+{
+    auto cloned = new DataConverterToString();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/data_bind/data_bind_base.cpp b/src/generated/data_bind/data_bind_base.cpp
new file mode 100644
index 0000000..cc993f8
--- /dev/null
+++ b/src/generated/data_bind/data_bind_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/data_bind/data_bind_base.hpp"
+#include "rive/data_bind/data_bind.hpp"
+
+using namespace rive;
+
+Core* DataBindBase::clone() const
+{
+    auto cloned = new DataBind();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/data_bind/data_bind_context_base.cpp b/src/generated/data_bind/data_bind_context_base.cpp
new file mode 100644
index 0000000..4691f4c
--- /dev/null
+++ b/src/generated/data_bind/data_bind_context_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/data_bind/data_bind_context_base.hpp"
+#include "rive/data_bind/data_bind_context.hpp"
+
+using namespace rive;
+
+Core* DataBindContextBase::clone() const
+{
+    auto cloned = new DataBindContext();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/draw_rules_base.cpp b/src/generated/draw_rules_base.cpp
new file mode 100644
index 0000000..67b9c81
--- /dev/null
+++ b/src/generated/draw_rules_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/draw_rules_base.hpp"
+#include "rive/draw_rules.hpp"
+
+using namespace rive;
+
+Core* DrawRulesBase::clone() const
+{
+    auto cloned = new DrawRules();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/draw_target_base.cpp b/src/generated/draw_target_base.cpp
new file mode 100644
index 0000000..0578be9
--- /dev/null
+++ b/src/generated/draw_target_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/draw_target_base.hpp"
+#include "rive/draw_target.hpp"
+
+using namespace rive;
+
+Core* DrawTargetBase::clone() const
+{
+    auto cloned = new DrawTarget();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/event_base.cpp b/src/generated/event_base.cpp
new file mode 100644
index 0000000..1829228
--- /dev/null
+++ b/src/generated/event_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/event_base.hpp"
+#include "rive/event.hpp"
+
+using namespace rive;
+
+Core* EventBase::clone() const
+{
+    auto cloned = new Event();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/joystick_base.cpp b/src/generated/joystick_base.cpp
new file mode 100644
index 0000000..19e3602
--- /dev/null
+++ b/src/generated/joystick_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/joystick_base.hpp"
+#include "rive/joystick.hpp"
+
+using namespace rive;
+
+Core* JoystickBase::clone() const
+{
+    auto cloned = new Joystick();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/layout/axis_x_base.cpp b/src/generated/layout/axis_x_base.cpp
new file mode 100644
index 0000000..172c284
--- /dev/null
+++ b/src/generated/layout/axis_x_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/layout/axis_x_base.hpp"
+#include "rive/layout/axis_x.hpp"
+
+using namespace rive;
+
+Core* AxisXBase::clone() const
+{
+    auto cloned = new AxisX();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/layout/axis_y_base.cpp b/src/generated/layout/axis_y_base.cpp
new file mode 100644
index 0000000..8815cef
--- /dev/null
+++ b/src/generated/layout/axis_y_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/layout/axis_y_base.hpp"
+#include "rive/layout/axis_y.hpp"
+
+using namespace rive;
+
+Core* AxisYBase::clone() const
+{
+    auto cloned = new AxisY();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/layout/layout_component_style_base.cpp b/src/generated/layout/layout_component_style_base.cpp
new file mode 100644
index 0000000..22c4da8
--- /dev/null
+++ b/src/generated/layout/layout_component_style_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/layout/layout_component_style_base.hpp"
+#include "rive/layout/layout_component_style.hpp"
+
+using namespace rive;
+
+Core* LayoutComponentStyleBase::clone() const
+{
+    auto cloned = new LayoutComponentStyle();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/layout/n_slicer_base.cpp b/src/generated/layout/n_slicer_base.cpp
new file mode 100644
index 0000000..49d6f37
--- /dev/null
+++ b/src/generated/layout/n_slicer_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/layout/n_slicer_base.hpp"
+#include "rive/layout/n_slicer.hpp"
+
+using namespace rive;
+
+Core* NSlicerBase::clone() const
+{
+    auto cloned = new NSlicer();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/layout/n_slicer_tile_mode_base.cpp b/src/generated/layout/n_slicer_tile_mode_base.cpp
new file mode 100644
index 0000000..068c59c
--- /dev/null
+++ b/src/generated/layout/n_slicer_tile_mode_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/layout/n_slicer_tile_mode_base.hpp"
+#include "rive/layout/n_slicer_tile_mode.hpp"
+
+using namespace rive;
+
+Core* NSlicerTileModeBase::clone() const
+{
+    auto cloned = new NSlicerTileMode();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/layout_component_base.cpp b/src/generated/layout_component_base.cpp
new file mode 100644
index 0000000..13e6c86
--- /dev/null
+++ b/src/generated/layout_component_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/layout_component_base.hpp"
+#include "rive/layout_component.hpp"
+
+using namespace rive;
+
+Core* LayoutComponentBase::clone() const
+{
+    auto cloned = new LayoutComponent();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/nested_artboard_base.cpp b/src/generated/nested_artboard_base.cpp
new file mode 100644
index 0000000..46a277e
--- /dev/null
+++ b/src/generated/nested_artboard_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/nested_artboard_base.hpp"
+#include "rive/nested_artboard.hpp"
+
+using namespace rive;
+
+Core* NestedArtboardBase::clone() const
+{
+    auto cloned = new NestedArtboard();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/nested_artboard_layout_base.cpp b/src/generated/nested_artboard_layout_base.cpp
new file mode 100644
index 0000000..069c374
--- /dev/null
+++ b/src/generated/nested_artboard_layout_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/nested_artboard_layout_base.hpp"
+#include "rive/nested_artboard_layout.hpp"
+
+using namespace rive;
+
+Core* NestedArtboardLayoutBase::clone() const
+{
+    auto cloned = new NestedArtboardLayout();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/nested_artboard_leaf_base.cpp b/src/generated/nested_artboard_leaf_base.cpp
new file mode 100644
index 0000000..d025a17
--- /dev/null
+++ b/src/generated/nested_artboard_leaf_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/nested_artboard_leaf_base.hpp"
+#include "rive/nested_artboard_leaf.hpp"
+
+using namespace rive;
+
+Core* NestedArtboardLeafBase::clone() const
+{
+    auto cloned = new NestedArtboardLeaf();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/node_base.cpp b/src/generated/node_base.cpp
new file mode 100644
index 0000000..7f144d5
--- /dev/null
+++ b/src/generated/node_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/node_base.hpp"
+#include "rive/node.hpp"
+
+using namespace rive;
+
+Core* NodeBase::clone() const
+{
+    auto cloned = new Node();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/open_url_event_base.cpp b/src/generated/open_url_event_base.cpp
new file mode 100644
index 0000000..6c7735d
--- /dev/null
+++ b/src/generated/open_url_event_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/open_url_event_base.hpp"
+#include "rive/open_url_event.hpp"
+
+using namespace rive;
+
+Core* OpenUrlEventBase::clone() const
+{
+    auto cloned = new OpenUrlEvent();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/clipping_shape_base.cpp b/src/generated/shapes/clipping_shape_base.cpp
new file mode 100644
index 0000000..2c28b3a
--- /dev/null
+++ b/src/generated/shapes/clipping_shape_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/clipping_shape_base.hpp"
+#include "rive/shapes/clipping_shape.hpp"
+
+using namespace rive;
+
+Core* ClippingShapeBase::clone() const
+{
+    auto cloned = new ClippingShape();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/contour_mesh_vertex_base.cpp b/src/generated/shapes/contour_mesh_vertex_base.cpp
new file mode 100644
index 0000000..8ced9fd
--- /dev/null
+++ b/src/generated/shapes/contour_mesh_vertex_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/contour_mesh_vertex_base.hpp"
+#include "rive/shapes/contour_mesh_vertex.hpp"
+
+using namespace rive;
+
+Core* ContourMeshVertexBase::clone() const
+{
+    auto cloned = new ContourMeshVertex();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/cubic_asymmetric_vertex_base.cpp b/src/generated/shapes/cubic_asymmetric_vertex_base.cpp
new file mode 100644
index 0000000..62b665a
--- /dev/null
+++ b/src/generated/shapes/cubic_asymmetric_vertex_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/cubic_asymmetric_vertex_base.hpp"
+#include "rive/shapes/cubic_asymmetric_vertex.hpp"
+
+using namespace rive;
+
+Core* CubicAsymmetricVertexBase::clone() const
+{
+    auto cloned = new CubicAsymmetricVertex();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/cubic_detached_vertex_base.cpp b/src/generated/shapes/cubic_detached_vertex_base.cpp
new file mode 100644
index 0000000..22f88ba
--- /dev/null
+++ b/src/generated/shapes/cubic_detached_vertex_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/cubic_detached_vertex_base.hpp"
+#include "rive/shapes/cubic_detached_vertex.hpp"
+
+using namespace rive;
+
+Core* CubicDetachedVertexBase::clone() const
+{
+    auto cloned = new CubicDetachedVertex();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/cubic_mirrored_vertex_base.cpp b/src/generated/shapes/cubic_mirrored_vertex_base.cpp
new file mode 100644
index 0000000..fb1731b
--- /dev/null
+++ b/src/generated/shapes/cubic_mirrored_vertex_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/cubic_mirrored_vertex_base.hpp"
+#include "rive/shapes/cubic_mirrored_vertex.hpp"
+
+using namespace rive;
+
+Core* CubicMirroredVertexBase::clone() const
+{
+    auto cloned = new CubicMirroredVertex();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/ellipse_base.cpp b/src/generated/shapes/ellipse_base.cpp
new file mode 100644
index 0000000..df160bb
--- /dev/null
+++ b/src/generated/shapes/ellipse_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/ellipse_base.hpp"
+#include "rive/shapes/ellipse.hpp"
+
+using namespace rive;
+
+Core* EllipseBase::clone() const
+{
+    auto cloned = new Ellipse();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/image_base.cpp b/src/generated/shapes/image_base.cpp
new file mode 100644
index 0000000..60bb78e
--- /dev/null
+++ b/src/generated/shapes/image_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/image_base.hpp"
+#include "rive/shapes/image.hpp"
+
+using namespace rive;
+
+Core* ImageBase::clone() const
+{
+    auto cloned = new Image();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/mesh_base.cpp b/src/generated/shapes/mesh_base.cpp
new file mode 100644
index 0000000..e30a683
--- /dev/null
+++ b/src/generated/shapes/mesh_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/mesh_base.hpp"
+#include "rive/shapes/mesh.hpp"
+
+using namespace rive;
+
+Core* MeshBase::clone() const
+{
+    auto cloned = new Mesh();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/mesh_vertex_base.cpp b/src/generated/shapes/mesh_vertex_base.cpp
new file mode 100644
index 0000000..ff36262
--- /dev/null
+++ b/src/generated/shapes/mesh_vertex_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/mesh_vertex_base.hpp"
+#include "rive/shapes/mesh_vertex.hpp"
+
+using namespace rive;
+
+Core* MeshVertexBase::clone() const
+{
+    auto cloned = new MeshVertex();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/paint/fill_base.cpp b/src/generated/shapes/paint/fill_base.cpp
new file mode 100644
index 0000000..1b2dbbc
--- /dev/null
+++ b/src/generated/shapes/paint/fill_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/paint/fill_base.hpp"
+#include "rive/shapes/paint/fill.hpp"
+
+using namespace rive;
+
+Core* FillBase::clone() const
+{
+    auto cloned = new Fill();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/paint/gradient_stop_base.cpp b/src/generated/shapes/paint/gradient_stop_base.cpp
new file mode 100644
index 0000000..0ff71eb
--- /dev/null
+++ b/src/generated/shapes/paint/gradient_stop_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/paint/gradient_stop_base.hpp"
+#include "rive/shapes/paint/gradient_stop.hpp"
+
+using namespace rive;
+
+Core* GradientStopBase::clone() const
+{
+    auto cloned = new GradientStop();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/paint/linear_gradient_base.cpp b/src/generated/shapes/paint/linear_gradient_base.cpp
new file mode 100644
index 0000000..ad261e7
--- /dev/null
+++ b/src/generated/shapes/paint/linear_gradient_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/paint/linear_gradient_base.hpp"
+#include "rive/shapes/paint/linear_gradient.hpp"
+
+using namespace rive;
+
+Core* LinearGradientBase::clone() const
+{
+    auto cloned = new LinearGradient();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/paint/radial_gradient_base.cpp b/src/generated/shapes/paint/radial_gradient_base.cpp
new file mode 100644
index 0000000..79b00d6
--- /dev/null
+++ b/src/generated/shapes/paint/radial_gradient_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/paint/radial_gradient_base.hpp"
+#include "rive/shapes/paint/radial_gradient.hpp"
+
+using namespace rive;
+
+Core* RadialGradientBase::clone() const
+{
+    auto cloned = new RadialGradient();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/paint/solid_color_base.cpp b/src/generated/shapes/paint/solid_color_base.cpp
new file mode 100644
index 0000000..df57f7f
--- /dev/null
+++ b/src/generated/shapes/paint/solid_color_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/paint/solid_color_base.hpp"
+#include "rive/shapes/paint/solid_color.hpp"
+
+using namespace rive;
+
+Core* SolidColorBase::clone() const
+{
+    auto cloned = new SolidColor();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/paint/stroke_base.cpp b/src/generated/shapes/paint/stroke_base.cpp
new file mode 100644
index 0000000..0c3acba
--- /dev/null
+++ b/src/generated/shapes/paint/stroke_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/paint/stroke_base.hpp"
+#include "rive/shapes/paint/stroke.hpp"
+
+using namespace rive;
+
+Core* StrokeBase::clone() const
+{
+    auto cloned = new Stroke();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/paint/trim_path_base.cpp b/src/generated/shapes/paint/trim_path_base.cpp
new file mode 100644
index 0000000..fb319ca
--- /dev/null
+++ b/src/generated/shapes/paint/trim_path_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/paint/trim_path_base.hpp"
+#include "rive/shapes/paint/trim_path.hpp"
+
+using namespace rive;
+
+Core* TrimPathBase::clone() const
+{
+    auto cloned = new TrimPath();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/points_path_base.cpp b/src/generated/shapes/points_path_base.cpp
new file mode 100644
index 0000000..27169a8
--- /dev/null
+++ b/src/generated/shapes/points_path_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/points_path_base.hpp"
+#include "rive/shapes/points_path.hpp"
+
+using namespace rive;
+
+Core* PointsPathBase::clone() const
+{
+    auto cloned = new PointsPath();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/polygon_base.cpp b/src/generated/shapes/polygon_base.cpp
new file mode 100644
index 0000000..96214c5
--- /dev/null
+++ b/src/generated/shapes/polygon_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/polygon_base.hpp"
+#include "rive/shapes/polygon.hpp"
+
+using namespace rive;
+
+Core* PolygonBase::clone() const
+{
+    auto cloned = new Polygon();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/rectangle_base.cpp b/src/generated/shapes/rectangle_base.cpp
new file mode 100644
index 0000000..47a83fe
--- /dev/null
+++ b/src/generated/shapes/rectangle_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/rectangle_base.hpp"
+#include "rive/shapes/rectangle.hpp"
+
+using namespace rive;
+
+Core* RectangleBase::clone() const
+{
+    auto cloned = new Rectangle();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/shape_base.cpp b/src/generated/shapes/shape_base.cpp
new file mode 100644
index 0000000..595afa6
--- /dev/null
+++ b/src/generated/shapes/shape_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/shape_base.hpp"
+#include "rive/shapes/shape.hpp"
+
+using namespace rive;
+
+Core* ShapeBase::clone() const
+{
+    auto cloned = new Shape();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/star_base.cpp b/src/generated/shapes/star_base.cpp
new file mode 100644
index 0000000..3b753c2
--- /dev/null
+++ b/src/generated/shapes/star_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/star_base.hpp"
+#include "rive/shapes/star.hpp"
+
+using namespace rive;
+
+Core* StarBase::clone() const
+{
+    auto cloned = new Star();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/straight_vertex_base.cpp b/src/generated/shapes/straight_vertex_base.cpp
new file mode 100644
index 0000000..a7c99c1
--- /dev/null
+++ b/src/generated/shapes/straight_vertex_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/straight_vertex_base.hpp"
+#include "rive/shapes/straight_vertex.hpp"
+
+using namespace rive;
+
+Core* StraightVertexBase::clone() const
+{
+    auto cloned = new StraightVertex();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/shapes/triangle_base.cpp b/src/generated/shapes/triangle_base.cpp
new file mode 100644
index 0000000..c4f7e95
--- /dev/null
+++ b/src/generated/shapes/triangle_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/shapes/triangle_base.hpp"
+#include "rive/shapes/triangle.hpp"
+
+using namespace rive;
+
+Core* TriangleBase::clone() const
+{
+    auto cloned = new Triangle();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/solo_base.cpp b/src/generated/solo_base.cpp
new file mode 100644
index 0000000..132eb74
--- /dev/null
+++ b/src/generated/solo_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/solo_base.hpp"
+#include "rive/solo.hpp"
+
+using namespace rive;
+
+Core* SoloBase::clone() const
+{
+    auto cloned = new Solo();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/text/text_base.cpp b/src/generated/text/text_base.cpp
new file mode 100644
index 0000000..7c34405
--- /dev/null
+++ b/src/generated/text/text_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/text/text_base.hpp"
+#include "rive/text/text.hpp"
+
+using namespace rive;
+
+Core* TextBase::clone() const
+{
+    auto cloned = new Text();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/text/text_modifier_group_base.cpp b/src/generated/text/text_modifier_group_base.cpp
new file mode 100644
index 0000000..cc74e27
--- /dev/null
+++ b/src/generated/text/text_modifier_group_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/text/text_modifier_group_base.hpp"
+#include "rive/text/text_modifier_group.hpp"
+
+using namespace rive;
+
+Core* TextModifierGroupBase::clone() const
+{
+    auto cloned = new TextModifierGroup();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/text/text_modifier_range_base.cpp b/src/generated/text/text_modifier_range_base.cpp
new file mode 100644
index 0000000..c144981
--- /dev/null
+++ b/src/generated/text/text_modifier_range_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/text/text_modifier_range_base.hpp"
+#include "rive/text/text_modifier_range.hpp"
+
+using namespace rive;
+
+Core* TextModifierRangeBase::clone() const
+{
+    auto cloned = new TextModifierRange();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/text/text_style_axis_base.cpp b/src/generated/text/text_style_axis_base.cpp
new file mode 100644
index 0000000..9f32f7f
--- /dev/null
+++ b/src/generated/text/text_style_axis_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/text/text_style_axis_base.hpp"
+#include "rive/text/text_style_axis.hpp"
+
+using namespace rive;
+
+Core* TextStyleAxisBase::clone() const
+{
+    auto cloned = new TextStyleAxis();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/text/text_style_base.cpp b/src/generated/text/text_style_base.cpp
new file mode 100644
index 0000000..4fa7bcc
--- /dev/null
+++ b/src/generated/text/text_style_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/text/text_style_base.hpp"
+#include "rive/text/text_style.hpp"
+
+using namespace rive;
+
+Core* TextStyleBase::clone() const
+{
+    auto cloned = new TextStyle();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/text/text_style_feature_base.cpp b/src/generated/text/text_style_feature_base.cpp
new file mode 100644
index 0000000..7f5f9b7
--- /dev/null
+++ b/src/generated/text/text_style_feature_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/text/text_style_feature_base.hpp"
+#include "rive/text/text_style_feature.hpp"
+
+using namespace rive;
+
+Core* TextStyleFeatureBase::clone() const
+{
+    auto cloned = new TextStyleFeature();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/text/text_value_run_base.cpp b/src/generated/text/text_value_run_base.cpp
new file mode 100644
index 0000000..732cc3b
--- /dev/null
+++ b/src/generated/text/text_value_run_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/text/text_value_run_base.hpp"
+#include "rive/text/text_value_run.hpp"
+
+using namespace rive;
+
+Core* TextValueRunBase::clone() const
+{
+    auto cloned = new TextValueRun();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/text/text_variation_modifier_base.cpp b/src/generated/text/text_variation_modifier_base.cpp
new file mode 100644
index 0000000..02a3007
--- /dev/null
+++ b/src/generated/text/text_variation_modifier_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/text/text_variation_modifier_base.hpp"
+#include "rive/text/text_variation_modifier.hpp"
+
+using namespace rive;
+
+Core* TextVariationModifierBase::clone() const
+{
+    auto cloned = new TextVariationModifier();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/data_enum_base.cpp b/src/generated/viewmodel/data_enum_base.cpp
new file mode 100644
index 0000000..9ac0ef0
--- /dev/null
+++ b/src/generated/viewmodel/data_enum_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/data_enum_base.hpp"
+#include "rive/viewmodel/data_enum.hpp"
+
+using namespace rive;
+
+Core* DataEnumBase::clone() const
+{
+    auto cloned = new DataEnum();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/data_enum_value_base.cpp b/src/generated/viewmodel/data_enum_value_base.cpp
new file mode 100644
index 0000000..f695edc
--- /dev/null
+++ b/src/generated/viewmodel/data_enum_value_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/data_enum_value_base.hpp"
+#include "rive/viewmodel/data_enum_value.hpp"
+
+using namespace rive;
+
+Core* DataEnumValueBase::clone() const
+{
+    auto cloned = new DataEnumValue();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/viewmodel_base.cpp b/src/generated/viewmodel/viewmodel_base.cpp
new file mode 100644
index 0000000..87e7f7a
--- /dev/null
+++ b/src/generated/viewmodel/viewmodel_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/viewmodel_base.hpp"
+#include "rive/viewmodel/viewmodel.hpp"
+
+using namespace rive;
+
+Core* ViewModelBase::clone() const
+{
+    auto cloned = new ViewModel();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/viewmodel_component_base.cpp b/src/generated/viewmodel/viewmodel_component_base.cpp
new file mode 100644
index 0000000..706138b
--- /dev/null
+++ b/src/generated/viewmodel/viewmodel_component_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/viewmodel_component_base.hpp"
+#include "rive/viewmodel/viewmodel_component.hpp"
+
+using namespace rive;
+
+Core* ViewModelComponentBase::clone() const
+{
+    auto cloned = new ViewModelComponent();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/viewmodel_instance_base.cpp b/src/generated/viewmodel/viewmodel_instance_base.cpp
new file mode 100644
index 0000000..892eba0
--- /dev/null
+++ b/src/generated/viewmodel/viewmodel_instance_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/viewmodel_instance_base.hpp"
+#include "rive/viewmodel/viewmodel_instance.hpp"
+
+using namespace rive;
+
+Core* ViewModelInstanceBase::clone() const
+{
+    auto cloned = new ViewModelInstance();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/viewmodel_instance_boolean_base.cpp b/src/generated/viewmodel/viewmodel_instance_boolean_base.cpp
new file mode 100644
index 0000000..641bfec
--- /dev/null
+++ b/src/generated/viewmodel/viewmodel_instance_boolean_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/viewmodel_instance_boolean_base.hpp"
+#include "rive/viewmodel/viewmodel_instance_boolean.hpp"
+
+using namespace rive;
+
+Core* ViewModelInstanceBooleanBase::clone() const
+{
+    auto cloned = new ViewModelInstanceBoolean();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/viewmodel_instance_color_base.cpp b/src/generated/viewmodel/viewmodel_instance_color_base.cpp
new file mode 100644
index 0000000..599a25d
--- /dev/null
+++ b/src/generated/viewmodel/viewmodel_instance_color_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/viewmodel_instance_color_base.hpp"
+#include "rive/viewmodel/viewmodel_instance_color.hpp"
+
+using namespace rive;
+
+Core* ViewModelInstanceColorBase::clone() const
+{
+    auto cloned = new ViewModelInstanceColor();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/viewmodel_instance_enum_base.cpp b/src/generated/viewmodel/viewmodel_instance_enum_base.cpp
new file mode 100644
index 0000000..3937d48
--- /dev/null
+++ b/src/generated/viewmodel/viewmodel_instance_enum_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/viewmodel_instance_enum_base.hpp"
+#include "rive/viewmodel/viewmodel_instance_enum.hpp"
+
+using namespace rive;
+
+Core* ViewModelInstanceEnumBase::clone() const
+{
+    auto cloned = new ViewModelInstanceEnum();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/viewmodel_instance_list_base.cpp b/src/generated/viewmodel/viewmodel_instance_list_base.cpp
new file mode 100644
index 0000000..b73c5a6
--- /dev/null
+++ b/src/generated/viewmodel/viewmodel_instance_list_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/viewmodel_instance_list_base.hpp"
+#include "rive/viewmodel/viewmodel_instance_list.hpp"
+
+using namespace rive;
+
+Core* ViewModelInstanceListBase::clone() const
+{
+    auto cloned = new ViewModelInstanceList();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/viewmodel_instance_list_item_base.cpp b/src/generated/viewmodel/viewmodel_instance_list_item_base.cpp
new file mode 100644
index 0000000..b327aad
--- /dev/null
+++ b/src/generated/viewmodel/viewmodel_instance_list_item_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/viewmodel_instance_list_item_base.hpp"
+#include "rive/viewmodel/viewmodel_instance_list_item.hpp"
+
+using namespace rive;
+
+Core* ViewModelInstanceListItemBase::clone() const
+{
+    auto cloned = new ViewModelInstanceListItem();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/viewmodel_instance_number_base.cpp b/src/generated/viewmodel/viewmodel_instance_number_base.cpp
new file mode 100644
index 0000000..6ac41a5
--- /dev/null
+++ b/src/generated/viewmodel/viewmodel_instance_number_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/viewmodel_instance_number_base.hpp"
+#include "rive/viewmodel/viewmodel_instance_number.hpp"
+
+using namespace rive;
+
+Core* ViewModelInstanceNumberBase::clone() const
+{
+    auto cloned = new ViewModelInstanceNumber();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/viewmodel_instance_string_base.cpp b/src/generated/viewmodel/viewmodel_instance_string_base.cpp
new file mode 100644
index 0000000..ce48839
--- /dev/null
+++ b/src/generated/viewmodel/viewmodel_instance_string_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/viewmodel_instance_string_base.hpp"
+#include "rive/viewmodel/viewmodel_instance_string.hpp"
+
+using namespace rive;
+
+Core* ViewModelInstanceStringBase::clone() const
+{
+    auto cloned = new ViewModelInstanceString();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/viewmodel_instance_viewmodel_base.cpp b/src/generated/viewmodel/viewmodel_instance_viewmodel_base.cpp
new file mode 100644
index 0000000..b0854a0
--- /dev/null
+++ b/src/generated/viewmodel/viewmodel_instance_viewmodel_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/viewmodel_instance_viewmodel_base.hpp"
+#include "rive/viewmodel/viewmodel_instance_viewmodel.hpp"
+
+using namespace rive;
+
+Core* ViewModelInstanceViewModelBase::clone() const
+{
+    auto cloned = new ViewModelInstanceViewModel();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/viewmodel_property_base.cpp b/src/generated/viewmodel/viewmodel_property_base.cpp
new file mode 100644
index 0000000..333ffa8
--- /dev/null
+++ b/src/generated/viewmodel/viewmodel_property_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/viewmodel_property_base.hpp"
+#include "rive/viewmodel/viewmodel_property.hpp"
+
+using namespace rive;
+
+Core* ViewModelPropertyBase::clone() const
+{
+    auto cloned = new ViewModelProperty();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/viewmodel_property_boolean_base.cpp b/src/generated/viewmodel/viewmodel_property_boolean_base.cpp
new file mode 100644
index 0000000..0371e8c
--- /dev/null
+++ b/src/generated/viewmodel/viewmodel_property_boolean_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/viewmodel_property_boolean_base.hpp"
+#include "rive/viewmodel/viewmodel_property_boolean.hpp"
+
+using namespace rive;
+
+Core* ViewModelPropertyBooleanBase::clone() const
+{
+    auto cloned = new ViewModelPropertyBoolean();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/viewmodel_property_color_base.cpp b/src/generated/viewmodel/viewmodel_property_color_base.cpp
new file mode 100644
index 0000000..6f3bf82
--- /dev/null
+++ b/src/generated/viewmodel/viewmodel_property_color_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/viewmodel_property_color_base.hpp"
+#include "rive/viewmodel/viewmodel_property_color.hpp"
+
+using namespace rive;
+
+Core* ViewModelPropertyColorBase::clone() const
+{
+    auto cloned = new ViewModelPropertyColor();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/viewmodel_property_enum_base.cpp b/src/generated/viewmodel/viewmodel_property_enum_base.cpp
new file mode 100644
index 0000000..5f05b71
--- /dev/null
+++ b/src/generated/viewmodel/viewmodel_property_enum_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/viewmodel_property_enum_base.hpp"
+#include "rive/viewmodel/viewmodel_property_enum.hpp"
+
+using namespace rive;
+
+Core* ViewModelPropertyEnumBase::clone() const
+{
+    auto cloned = new ViewModelPropertyEnum();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/viewmodel_property_list_base.cpp b/src/generated/viewmodel/viewmodel_property_list_base.cpp
new file mode 100644
index 0000000..d6a7e79
--- /dev/null
+++ b/src/generated/viewmodel/viewmodel_property_list_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/viewmodel_property_list_base.hpp"
+#include "rive/viewmodel/viewmodel_property_list.hpp"
+
+using namespace rive;
+
+Core* ViewModelPropertyListBase::clone() const
+{
+    auto cloned = new ViewModelPropertyList();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/viewmodel_property_number_base.cpp b/src/generated/viewmodel/viewmodel_property_number_base.cpp
new file mode 100644
index 0000000..bf38820
--- /dev/null
+++ b/src/generated/viewmodel/viewmodel_property_number_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/viewmodel_property_number_base.hpp"
+#include "rive/viewmodel/viewmodel_property_number.hpp"
+
+using namespace rive;
+
+Core* ViewModelPropertyNumberBase::clone() const
+{
+    auto cloned = new ViewModelPropertyNumber();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/viewmodel_property_string_base.cpp b/src/generated/viewmodel/viewmodel_property_string_base.cpp
new file mode 100644
index 0000000..5deac13
--- /dev/null
+++ b/src/generated/viewmodel/viewmodel_property_string_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/viewmodel_property_string_base.hpp"
+#include "rive/viewmodel/viewmodel_property_string.hpp"
+
+using namespace rive;
+
+Core* ViewModelPropertyStringBase::clone() const
+{
+    auto cloned = new ViewModelPropertyString();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/generated/viewmodel/viewmodel_property_viewmodel_base.cpp b/src/generated/viewmodel/viewmodel_property_viewmodel_base.cpp
new file mode 100644
index 0000000..5b57217
--- /dev/null
+++ b/src/generated/viewmodel/viewmodel_property_viewmodel_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/viewmodel/viewmodel_property_viewmodel_base.hpp"
+#include "rive/viewmodel/viewmodel_property_viewmodel.hpp"
+
+using namespace rive;
+
+Core* ViewModelPropertyViewModelBase::clone() const
+{
+    auto cloned = new ViewModelPropertyViewModel();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/hittest_command_path.cpp b/src/hittest_command_path.cpp
new file mode 100644
index 0000000..27c42bf
--- /dev/null
+++ b/src/hittest_command_path.cpp
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "rive/hittest_command_path.hpp"
+
+using namespace rive;
+
+HitTestCommandPath::HitTestCommandPath(const IAABB& area) : m_Area(area) { m_Tester.reset(m_Area); }
+
+bool HitTestCommandPath::wasHit() { return m_Tester.test(m_FillRule); }
+
+void HitTestCommandPath::rewind() { m_Tester.reset(m_Area); }
+
+void HitTestCommandPath::fillRule(FillRule value)
+{
+    // remember this here, and pass it to test()
+    m_FillRule = value;
+}
+
+void HitTestCommandPath::addPath(CommandPath* path, const Mat2D& transform)
+{
+    assert(false);
+    // not supported
+}
+
+RenderPath* HitTestCommandPath::renderPath()
+{
+    assert(false);
+    // not supported
+    return nullptr;
+}
+
+void HitTestCommandPath::moveTo(float x, float y) { m_Tester.move(m_Xform * Vec2D(x, y)); }
+
+void HitTestCommandPath::lineTo(float x, float y) { m_Tester.line(m_Xform * Vec2D(x, y)); }
+
+void HitTestCommandPath::cubicTo(float ox, float oy, float ix, float iy, float x, float y)
+{
+    m_Tester.cubic(m_Xform * Vec2D(ox, oy), m_Xform * Vec2D(ix, iy), m_Xform * Vec2D(x, y));
+}
+
+void HitTestCommandPath::close() { m_Tester.close(); }
diff --git a/src/importers/artboard_importer.cpp b/src/importers/artboard_importer.cpp
new file mode 100644
index 0000000..f2957b0
--- /dev/null
+++ b/src/importers/artboard_importer.cpp
@@ -0,0 +1,34 @@
+#include "rive/artboard.hpp"
+#include "rive/importers/artboard_importer.hpp"
+#include "rive/animation/linear_animation.hpp"
+#include "rive/animation/state_machine.hpp"
+#include "rive/data_bind/data_bind.hpp"
+#include "rive/text/text_value_run.hpp"
+#include "rive/event.hpp"
+#include "rive/artboard.hpp"
+
+using namespace rive;
+
+ArtboardImporter::ArtboardImporter(Artboard* artboard) : m_Artboard(artboard) {}
+
+void ArtboardImporter::addComponent(Core* object) { m_Artboard->addObject(object); }
+
+void ArtboardImporter::addAnimation(LinearAnimation* animation)
+{
+    m_Artboard->addAnimation(animation);
+}
+
+void ArtboardImporter::addStateMachine(StateMachine* stateMachine)
+{
+    m_Artboard->addStateMachine(stateMachine);
+}
+
+void ArtboardImporter::addDataBind(DataBind* dataBind) { m_Artboard->addDataBind(dataBind); }
+
+StatusCode ArtboardImporter::resolve() { return m_Artboard->initialize(); }
+
+bool ArtboardImporter::readNullObject()
+{
+    addComponent(nullptr);
+    return true;
+}
\ No newline at end of file
diff --git a/src/importers/backboard_importer.cpp b/src/importers/backboard_importer.cpp
new file mode 100644
index 0000000..97a6c55
--- /dev/null
+++ b/src/importers/backboard_importer.cpp
@@ -0,0 +1,125 @@
+
+#include "rive/importers/backboard_importer.hpp"
+#include "rive/artboard.hpp"
+#include "rive/nested_artboard.hpp"
+#include "rive/backboard.hpp"
+#include "rive/assets/file_asset_referencer.hpp"
+#include "rive/assets/file_asset.hpp"
+#include "rive/viewmodel/viewmodel.hpp"
+#include "rive/viewmodel/viewmodel_instance.hpp"
+#include "rive/data_bind/converters/data_converter.hpp"
+#include "rive/data_bind/converters/data_converter_group_item.hpp"
+#include "rive/data_bind/data_bind.hpp"
+#include <unordered_set>
+
+using namespace rive;
+
+BackboardImporter::BackboardImporter(Backboard* backboard) :
+    m_Backboard(backboard), m_NextArtboardId(0)
+{}
+void BackboardImporter::addNestedArtboard(NestedArtboard* artboard)
+{
+    m_NestedArtboards.push_back(artboard);
+}
+
+void BackboardImporter::addFileAsset(FileAsset* asset)
+{
+    m_FileAssets.push_back(asset);
+    {
+        // EDITOR BUG 4204
+        // --------------
+        // Ensure assetIds are unique. Due to an editor bug:
+        // https://github.com/rive-app/rive/issues/4204
+        std::unordered_set<uint32_t> ids;
+        uint32_t nextId = 1;
+        for (auto fileAsset : m_FileAssets)
+        {
+            if (ids.count(fileAsset->assetId()))
+            {
+                fileAsset->assetId(nextId);
+            }
+            else
+            {
+                ids.insert(fileAsset->assetId());
+                if (fileAsset->assetId() >= nextId)
+                {
+                    nextId = fileAsset->assetId() + 1;
+                }
+            }
+        }
+
+        // --------------
+    }
+}
+
+void BackboardImporter::addFileAssetReferencer(FileAssetReferencer* referencer)
+{
+    m_FileAssetReferencers.push_back(referencer);
+}
+
+void BackboardImporter::addArtboard(Artboard* artboard)
+{
+    m_ArtboardLookup[m_NextArtboardId++] = artboard;
+}
+
+void BackboardImporter::addMissingArtboard() { m_NextArtboardId++; }
+
+StatusCode BackboardImporter::resolve()
+{
+    for (auto nestedArtboard : m_NestedArtboards)
+    {
+        auto itr = m_ArtboardLookup.find(nestedArtboard->artboardId());
+        if (itr != m_ArtboardLookup.end())
+        {
+            auto artboard = itr->second;
+            if (artboard != nullptr)
+            {
+                nestedArtboard->nest(artboard);
+            }
+        }
+    }
+    for (auto referencer : m_FileAssetReferencers)
+    {
+        auto index = (size_t)referencer->assetId();
+        if (index >= m_FileAssets.size())
+        {
+            continue;
+        }
+        auto asset = m_FileAssets[index];
+        referencer->setAsset(asset);
+    }
+    for (auto referencer : m_DataConverterReferencers)
+    {
+        auto index = (size_t)referencer->converterId();
+        if (index >= m_DataConverters.size() || index < 0)
+        {
+            continue;
+        }
+        referencer->converter(m_DataConverters[index]);
+    }
+    for (auto referencer : m_DataConverterGroupItemReferencers)
+    {
+        auto index = (size_t)referencer->converterId();
+        if (index >= m_DataConverters.size() || index < 0)
+        {
+            continue;
+        }
+        referencer->converter(m_DataConverters[index]);
+    }
+    return StatusCode::Ok;
+}
+
+void BackboardImporter::addDataConverter(DataConverter* dataConverter)
+{
+    m_DataConverters.push_back(dataConverter);
+}
+
+void BackboardImporter::addDataConverterReferencer(DataBind* dataBind)
+{
+    m_DataConverterReferencers.push_back(dataBind);
+}
+
+void BackboardImporter::addDataConverterGroupItemReferencer(DataConverterGroupItem* dataBind)
+{
+    m_DataConverterGroupItemReferencers.push_back(dataBind);
+}
\ No newline at end of file
diff --git a/src/importers/bindable_property_importer.cpp b/src/importers/bindable_property_importer.cpp
new file mode 100644
index 0000000..1d992af
--- /dev/null
+++ b/src/importers/bindable_property_importer.cpp
@@ -0,0 +1,9 @@
+#include "rive/artboard.hpp"
+#include "rive/importers/bindable_property_importer.hpp"
+#include "rive/data_bind/bindable_property.hpp"
+
+using namespace rive;
+
+BindablePropertyImporter::BindablePropertyImporter(BindableProperty* bindableProperty) :
+    m_bindableProperty(bindableProperty)
+{}
\ No newline at end of file
diff --git a/src/importers/data_converter_group_importer.cpp b/src/importers/data_converter_group_importer.cpp
new file mode 100644
index 0000000..457a0e2
--- /dev/null
+++ b/src/importers/data_converter_group_importer.cpp
@@ -0,0 +1,9 @@
+#include "rive/artboard.hpp"
+#include "rive/importers/data_converter_group_importer.hpp"
+#include "rive/data_bind/converters/data_converter.hpp"
+
+using namespace rive;
+
+DataConverterGroupImporter::DataConverterGroupImporter(DataConverterGroup* group) :
+    m_dataConverterGroup(group)
+{}
\ No newline at end of file
diff --git a/src/importers/enum_importer.cpp b/src/importers/enum_importer.cpp
new file mode 100644
index 0000000..65d616a
--- /dev/null
+++ b/src/importers/enum_importer.cpp
@@ -0,0 +1,11 @@
+#include "rive/importers/enum_importer.hpp"
+#include "rive/viewmodel/data_enum.hpp"
+#include "rive/viewmodel/data_enum_value.hpp"
+
+using namespace rive;
+
+EnumImporter::EnumImporter(DataEnum* dataEnum) : m_DataEnum(dataEnum) {}
+
+void EnumImporter::addValue(DataEnumValue* value) { m_DataEnum->addValue(value); }
+
+StatusCode EnumImporter::resolve() { return StatusCode::Ok; }
\ No newline at end of file
diff --git a/src/importers/file_asset_importer.cpp b/src/importers/file_asset_importer.cpp
new file mode 100644
index 0000000..1165e0d
--- /dev/null
+++ b/src/importers/file_asset_importer.cpp
@@ -0,0 +1,48 @@
+#include "rive/importers/file_asset_importer.hpp"
+#include "rive/assets/file_asset_contents.hpp"
+#include "rive/assets/file_asset.hpp"
+#include "rive/file_asset_loader.hpp"
+#include "rive/span.hpp"
+#include <cstdint>
+
+using namespace rive;
+
+FileAssetImporter::FileAssetImporter(FileAsset* fileAsset,
+                                     FileAssetLoader* assetLoader,
+                                     Factory* factory) :
+    m_FileAsset(fileAsset), m_FileAssetLoader(assetLoader), m_Factory(factory)
+{}
+
+// if file asset contents are found when importing a rive file, store those for when we resolve
+// the importer later
+void FileAssetImporter::onFileAssetContents(std::unique_ptr<FileAssetContents> contents)
+{
+    // we should only ever be called once
+    assert(!m_Content);
+    m_Content = std::move(contents);
+}
+
+StatusCode FileAssetImporter::resolve()
+{
+    Span<const uint8_t> bytes;
+    if (m_Content != nullptr)
+    {
+        bytes = m_Content->bytes();
+    }
+
+    // If we have a file asset loader, lets give it the opportunity to claim responsibility for
+    // loading the asset
+    if (m_FileAssetLoader != nullptr &&
+        m_FileAssetLoader->loadContents(*m_FileAsset, bytes, m_Factory))
+    {
+        return StatusCode::Ok;
+    }
+    // If we do not, but we have found in band contents, load those
+    else if (bytes.size() > 0)
+    {
+        m_FileAsset->decode(m_Content->bytes(), m_Factory);
+    }
+
+    // Note that it's ok for an asset to not resolve (or to resolve async).
+    return StatusCode::Ok;
+}
diff --git a/src/importers/keyed_object_importer.cpp b/src/importers/keyed_object_importer.cpp
new file mode 100644
index 0000000..9e93cc9
--- /dev/null
+++ b/src/importers/keyed_object_importer.cpp
@@ -0,0 +1,13 @@
+#include "rive/importers/keyed_object_importer.hpp"
+#include "rive/animation/keyed_object.hpp"
+#include "rive/animation/keyed_property.hpp"
+#include "rive/artboard.hpp"
+
+using namespace rive;
+
+KeyedObjectImporter::KeyedObjectImporter(KeyedObject* keyedObject) : m_KeyedObject(keyedObject) {}
+
+void KeyedObjectImporter::addKeyedProperty(std::unique_ptr<KeyedProperty> property)
+{
+    m_KeyedObject->addKeyedProperty(std::move(property));
+}
\ No newline at end of file
diff --git a/src/importers/keyed_property_importer.cpp b/src/importers/keyed_property_importer.cpp
new file mode 100644
index 0000000..8efb275
--- /dev/null
+++ b/src/importers/keyed_property_importer.cpp
@@ -0,0 +1,24 @@
+#include "rive/importers/keyed_property_importer.hpp"
+#include "rive/animation/keyed_property.hpp"
+#include "rive/animation/keyframe.hpp"
+#include "rive/animation/linear_animation.hpp"
+
+using namespace rive;
+
+KeyedPropertyImporter::KeyedPropertyImporter(LinearAnimation* animation,
+                                             KeyedProperty* keyedProperty) :
+    m_Animation(animation), m_KeyedProperty(keyedProperty)
+{}
+
+void KeyedPropertyImporter::addKeyFrame(std::unique_ptr<KeyFrame> keyFrame)
+{
+    keyFrame->computeSeconds(m_Animation->fps());
+    m_KeyedProperty->addKeyFrame(std::move(keyFrame));
+}
+
+bool KeyedPropertyImporter::readNullObject()
+{
+    // We don't need to add the null keyframe as nothing references them, but we
+    // do need to not allow the null to propagate up.
+    return true;
+}
\ No newline at end of file
diff --git a/src/importers/layer_state_importer.cpp b/src/importers/layer_state_importer.cpp
new file mode 100644
index 0000000..3dc48ee
--- /dev/null
+++ b/src/importers/layer_state_importer.cpp
@@ -0,0 +1,48 @@
+#include "rive/importers/layer_state_importer.hpp"
+#include "rive/animation/state_transition.hpp"
+#include "rive/animation/layer_state.hpp"
+#include "rive/animation/blend_state.hpp"
+#include "rive/animation/blend_state_transition.hpp"
+
+using namespace rive;
+
+LayerStateImporter::LayerStateImporter(LayerState* state) : m_State(state) {}
+void LayerStateImporter::addTransition(StateTransition* transition)
+{
+    m_State->addTransition(transition);
+}
+
+bool LayerStateImporter::addBlendAnimation(BlendAnimation* animation)
+{
+    if (!m_State->is<BlendState>())
+    {
+        return false;
+    }
+    auto blendState = m_State->as<BlendState>();
+
+    blendState->addAnimation(animation);
+    return true;
+}
+
+StatusCode LayerStateImporter::resolve()
+{
+    if (m_State->is<BlendState>())
+    {
+        auto blendState = m_State->as<BlendState>();
+        for (auto transition : blendState->m_Transitions)
+        {
+            if (!transition->is<BlendStateTransition>())
+            {
+                continue;
+            }
+
+            auto blendStateTransition = transition->as<BlendStateTransition>();
+            size_t exitId = blendStateTransition->exitBlendAnimationId();
+            if (exitId < blendState->m_Animations.size())
+            {
+                blendStateTransition->m_ExitBlendAnimation = blendState->m_Animations[exitId];
+            }
+        }
+    }
+    return StatusCode::Ok;
+}
diff --git a/src/importers/linear_animation_importer.cpp b/src/importers/linear_animation_importer.cpp
new file mode 100644
index 0000000..b6ab77f
--- /dev/null
+++ b/src/importers/linear_animation_importer.cpp
@@ -0,0 +1,15 @@
+#include "rive/importers/linear_animation_importer.hpp"
+#include "rive/animation/keyed_object.hpp"
+#include "rive/animation/linear_animation.hpp"
+#include "rive/artboard.hpp"
+
+using namespace rive;
+
+LinearAnimationImporter::LinearAnimationImporter(LinearAnimation* animation) :
+    m_Animation(animation)
+{}
+
+void LinearAnimationImporter::addKeyedObject(std::unique_ptr<KeyedObject> object)
+{
+    m_Animation->addKeyedObject(std::move(object));
+}
\ No newline at end of file
diff --git a/src/importers/state_machine_importer.cpp b/src/importers/state_machine_importer.cpp
new file mode 100644
index 0000000..70a3985
--- /dev/null
+++ b/src/importers/state_machine_importer.cpp
@@ -0,0 +1,39 @@
+#include "rive/importers/state_machine_importer.hpp"
+#include "rive/animation/state_machine.hpp"
+#include "rive/animation/state_machine_listener.hpp"
+#include "rive/animation/state_machine_input.hpp"
+#include "rive/animation/state_machine_layer.hpp"
+#include "rive/data_bind/data_bind.hpp"
+
+using namespace rive;
+
+StateMachineImporter::StateMachineImporter(StateMachine* machine) : m_StateMachine(machine) {}
+
+void StateMachineImporter::addLayer(std::unique_ptr<StateMachineLayer> layer)
+{
+    m_StateMachine->addLayer(std::move(layer));
+}
+
+void StateMachineImporter::addInput(std::unique_ptr<StateMachineInput> input)
+{
+    m_StateMachine->addInput(std::move(input));
+}
+
+void StateMachineImporter::addListener(std::unique_ptr<StateMachineListener> listener)
+{
+    m_StateMachine->addListener(std::move(listener));
+}
+
+void StateMachineImporter::addDataBind(std::unique_ptr<DataBind> dataBind)
+{
+    m_StateMachine->addDataBind(std::move(dataBind));
+}
+
+bool StateMachineImporter::readNullObject()
+{
+    // Hard assumption that we won't add new layer types...
+    m_StateMachine->addInput(nullptr);
+    return true;
+}
+
+StatusCode StateMachineImporter::resolve() { return StatusCode::Ok; }
\ No newline at end of file
diff --git a/src/importers/state_machine_layer_component_importer.cpp b/src/importers/state_machine_layer_component_importer.cpp
new file mode 100644
index 0000000..d8e4c6d
--- /dev/null
+++ b/src/importers/state_machine_layer_component_importer.cpp
@@ -0,0 +1,23 @@
+#include "rive/importers/state_machine_layer_component_importer.hpp"
+#include "rive/animation/state_machine_layer_component.hpp"
+#include "rive/animation/state_machine_fire_event.hpp"
+
+using namespace rive;
+
+StateMachineLayerComponent::~StateMachineLayerComponent()
+{
+    for (auto event : m_events)
+    {
+        delete event;
+    }
+}
+
+StateMachineLayerComponentImporter::StateMachineLayerComponentImporter(
+    StateMachineLayerComponent* component) :
+    m_stateMachineLayerComponent(component)
+{}
+
+void StateMachineLayerComponentImporter::addFireEvent(StateMachineFireEvent* fireEvent)
+{
+    m_stateMachineLayerComponent->m_events.push_back(fireEvent);
+}
\ No newline at end of file
diff --git a/src/importers/state_machine_layer_importer.cpp b/src/importers/state_machine_layer_importer.cpp
new file mode 100644
index 0000000..f56899a
--- /dev/null
+++ b/src/importers/state_machine_layer_importer.cpp
@@ -0,0 +1,56 @@
+#include "rive/importers/state_machine_layer_importer.hpp"
+#include "rive/importers/artboard_importer.hpp"
+#include "rive/animation/state_machine_layer.hpp"
+#include "rive/animation/animation_state.hpp"
+#include "rive/animation/state_transition.hpp"
+#include "rive/artboard.hpp"
+
+using namespace rive;
+StateMachineLayerImporter::StateMachineLayerImporter(StateMachineLayer* layer,
+                                                     const Artboard* artboard) :
+    m_Layer(layer), m_Artboard(artboard)
+{}
+void StateMachineLayerImporter::addState(LayerState* state) { m_Layer->addState(state); }
+
+StatusCode StateMachineLayerImporter::resolve()
+{
+
+    for (auto state : m_Layer->m_States)
+    {
+        if (state->is<AnimationState>())
+        {
+            auto animationState = state->as<AnimationState>();
+
+            if (animationState->animationId() < m_Artboard->animationCount())
+            {
+                animationState->m_Animation = m_Artboard->animation(animationState->animationId());
+                if (animationState->m_Animation == nullptr)
+                {
+                    return StatusCode::MissingObject;
+                }
+            }
+        }
+        for (auto transition : state->m_Transitions)
+        {
+            if ((size_t)transition->stateToId() < m_Layer->m_States.size())
+            {
+                transition->m_StateTo = m_Layer->m_States[transition->stateToId()];
+            }
+            else
+            {
+                return StatusCode::InvalidObject;
+            }
+        }
+    }
+    return StatusCode::Ok;
+}
+
+bool StateMachineLayerImporter::readNullObject()
+{
+    // Add an 'empty' generic state that can be transitioned to/from but doesn't
+    // effectively do anything. This allows us to deal with unexpected new state
+    // types the runtime won't be able to understand. It'll still be able to
+    // make use of the state but it won't do anything visually.
+    addState(new LayerState());
+    return true;
+}
diff --git a/src/importers/state_machine_listener_importer.cpp b/src/importers/state_machine_listener_importer.cpp
new file mode 100644
index 0000000..d2ece94
--- /dev/null
+++ b/src/importers/state_machine_listener_importer.cpp
@@ -0,0 +1,16 @@
+#include "rive/animation/listener_action.hpp"
+#include "rive/importers/state_machine_listener_importer.hpp"
+#include "rive/animation/state_machine_listener.hpp"
+
+using namespace rive;
+
+StateMachineListenerImporter::StateMachineListenerImporter(StateMachineListener* listener) :
+    m_StateMachineListener(listener)
+{}
+
+void StateMachineListenerImporter::addAction(std::unique_ptr<ListenerAction> action)
+{
+    m_StateMachineListener->addAction(std::move(action));
+}
+
+StatusCode StateMachineListenerImporter::resolve() { return StatusCode::Ok; }
\ No newline at end of file
diff --git a/src/importers/state_transition_importer.cpp b/src/importers/state_transition_importer.cpp
new file mode 100644
index 0000000..beb6b45
--- /dev/null
+++ b/src/importers/state_transition_importer.cpp
@@ -0,0 +1,15 @@
+#include "rive/importers/state_transition_importer.hpp"
+#include "rive/animation/transition_condition.hpp"
+#include "rive/animation/state_transition.hpp"
+
+using namespace rive;
+
+StateTransitionImporter::StateTransitionImporter(StateTransition* transition) :
+    m_Transition(transition)
+{}
+void StateTransitionImporter::addCondition(TransitionCondition* condition)
+{
+    m_Transition->addCondition(condition);
+}
+
+StatusCode StateTransitionImporter::resolve() { return StatusCode::Ok; }
\ No newline at end of file
diff --git a/src/importers/transition_viewmodel_condition_importer.cpp b/src/importers/transition_viewmodel_condition_importer.cpp
new file mode 100644
index 0000000..97d4cb0
--- /dev/null
+++ b/src/importers/transition_viewmodel_condition_importer.cpp
@@ -0,0 +1,16 @@
+#include "rive/importers/transition_viewmodel_condition_importer.hpp"
+#include "rive/animation/transition_viewmodel_condition.hpp"
+#include "rive/animation/transition_comparator.hpp"
+#include "rive/data_bind/data_bind.hpp"
+
+using namespace rive;
+
+TransitionViewModelConditionImporter::TransitionViewModelConditionImporter(
+    TransitionViewModelCondition* transitionViewModelCondition) :
+    m_TransitionViewModelCondition(transitionViewModelCondition)
+{}
+
+void TransitionViewModelConditionImporter::setComparator(TransitionComparator* comparator)
+{
+    m_TransitionViewModelCondition->comparator(comparator);
+}
\ No newline at end of file
diff --git a/src/importers/viewmodel_importer.cpp b/src/importers/viewmodel_importer.cpp
new file mode 100644
index 0000000..d868d05
--- /dev/null
+++ b/src/importers/viewmodel_importer.cpp
@@ -0,0 +1,15 @@
+#include "rive/importers/viewmodel_importer.hpp"
+#include "rive/viewmodel/viewmodel.hpp"
+#include "rive/viewmodel/viewmodel_property.hpp"
+#include "rive/viewmodel/viewmodel_instance.hpp"
+
+using namespace rive;
+
+ViewModelImporter::ViewModelImporter(ViewModel* viewModel) : m_ViewModel(viewModel) {}
+void ViewModelImporter::addProperty(ViewModelProperty* property)
+{
+    m_ViewModel->addProperty(property);
+}
+void ViewModelImporter::addInstance(ViewModelInstance* value) { m_ViewModel->addInstance(value); }
+
+StatusCode ViewModelImporter::resolve() { return StatusCode::Ok; }
\ No newline at end of file
diff --git a/src/importers/viewmodel_instance_importer.cpp b/src/importers/viewmodel_instance_importer.cpp
new file mode 100644
index 0000000..c4642fc
--- /dev/null
+++ b/src/importers/viewmodel_instance_importer.cpp
@@ -0,0 +1,15 @@
+#include "rive/importers/viewmodel_instance_importer.hpp"
+#include "rive/viewmodel/viewmodel_instance.hpp"
+#include "rive/viewmodel/viewmodel_instance_value.hpp"
+
+using namespace rive;
+
+ViewModelInstanceImporter::ViewModelInstanceImporter(ViewModelInstance* viewModelInstance) :
+    m_ViewModelInstance(viewModelInstance)
+{}
+void ViewModelInstanceImporter::addValue(ViewModelInstanceValue* value)
+{
+    m_ViewModelInstance->addValue(value);
+}
+
+StatusCode ViewModelInstanceImporter::resolve() { return StatusCode::Ok; }
\ No newline at end of file
diff --git a/src/importers/viewmodel_instance_list_importer.cpp b/src/importers/viewmodel_instance_list_importer.cpp
new file mode 100644
index 0000000..2edf062
--- /dev/null
+++ b/src/importers/viewmodel_instance_list_importer.cpp
@@ -0,0 +1,16 @@
+#include "rive/importers/viewmodel_instance_list_importer.hpp"
+#include "rive/viewmodel/viewmodel_instance_list.hpp"
+#include "rive/viewmodel/viewmodel_instance_list_item.hpp"
+
+using namespace rive;
+
+ViewModelInstanceListImporter::ViewModelInstanceListImporter(
+    ViewModelInstanceList* viewModelInstanceList) :
+    m_ViewModelInstanceList(viewModelInstanceList)
+{}
+void ViewModelInstanceListImporter::addItem(ViewModelInstanceListItem* listItem)
+{
+    m_ViewModelInstanceList->addItem(listItem);
+}
+
+StatusCode ViewModelInstanceListImporter::resolve() { return StatusCode::Ok; }
\ No newline at end of file
diff --git a/src/joystick.cpp b/src/joystick.cpp
new file mode 100644
index 0000000..334face
--- /dev/null
+++ b/src/joystick.cpp
@@ -0,0 +1,94 @@
+#include "rive/joystick.hpp"
+#include "rive/artboard.hpp"
+#include "rive/transform_component.hpp"
+
+using namespace rive;
+
+StatusCode Joystick::onAddedDirty(CoreContext* context)
+{
+    StatusCode status = Super::onAddedDirty(context);
+    if (status != StatusCode::Ok)
+    {
+        return status;
+    }
+    if (handleSourceId() != Core::emptyId)
+    {
+        auto coreObject = context->resolve(handleSourceId());
+        if (coreObject == nullptr || !coreObject->is<TransformComponent>())
+        {
+            return StatusCode::MissingObject;
+        }
+        m_handleSource = static_cast<TransformComponent*>(coreObject);
+    }
+
+    return StatusCode::Ok;
+}
+
+StatusCode Joystick::onAddedClean(CoreContext* context)
+{
+    m_xAnimation = artboard()->animation(xId());
+    m_yAnimation = artboard()->animation(yId());
+
+    return StatusCode::Ok;
+}
+
+void Joystick::buildDependencies()
+{
+    // We only need to update if we're in world space (and that is only required
+    // at runtime if we have a custom handle source).
+    if (m_handleSource != nullptr && parent() != nullptr)
+    {
+        parent()->addDependent(this);
+        m_handleSource->addDependent(this);
+    }
+}
+
+void Joystick::update(ComponentDirt value)
+{
+    if (m_handleSource == nullptr)
+    {
+        return;
+    }
+    if (hasDirt(value, ComponentDirt::WorldTransform | ComponentDirt::Transform))
+    {
+        Mat2D world = Mat2D::fromTranslate(posX(), posY());
+        if (parent() != nullptr && parent()->is<WorldTransformComponent>())
+        {
+            world = parent()->as<WorldTransformComponent>()->worldTransform() * world;
+        }
+
+        if (m_worldTransform != world)
+        {
+            m_worldTransform = world;
+            m_inverseWorldTransform = world.invertOrIdentity();
+        }
+
+        auto pos = m_inverseWorldTransform * m_handleSource->worldTranslation();
+
+        auto localBounds = AABB(-width() * originX(),
+                                -height() * originY(),
+                                -width() * originX() + width(),
+                                -height() * originY() + height());
+
+        auto local = localBounds.factorFrom(pos);
+        x(local.x);
+        y(local.y);
+    }
+}
+
+void Joystick::apply(Artboard* artboard) const
+{
+    if (m_xAnimation != nullptr)
+    {
+        m_xAnimation->apply(artboard,
+                            ((isJoystickFlagged(JoystickFlags::invertX) ? -x() : x()) + 1.0f) /
+                                2.0f * m_xAnimation->durationSeconds());
+    }
+    if (m_yAnimation != nullptr)
+    {
+
+        m_yAnimation->apply(artboard,
+                            ((isJoystickFlagged(JoystickFlags::invertY) ? -y() : y()) + 1.0f) /
+                                2.0f * m_yAnimation->durationSeconds());
+    }
+}
diff --git a/src/layout.cpp b/src/layout.cpp
new file mode 100644
index 0000000..55d9e8f
--- /dev/null
+++ b/src/layout.cpp
@@ -0,0 +1,21 @@
+#include "rive/layout.hpp"
+
+using namespace rive;
+
+const Alignment Alignment::topLeft = Alignment(-1.0f, -1.0f);
+
+const Alignment Alignment::topCenter = Alignment(0.0f, -1.0f);
+
+const Alignment Alignment::topRight = Alignment(1.0f, -1.0f);
+
+const Alignment Alignment::centerLeft = Alignment(-1.0f, 0.0f);
+
+const Alignment Alignment::center = Alignment(0.0f, 0.0f);
+
+const Alignment Alignment::centerRight = Alignment(1.0f, 0.0f);
+
+const Alignment Alignment::bottomLeft = Alignment(-1.0f, 1.0f);
+
+const Alignment Alignment::bottomCenter = Alignment(0.0f, 1.0f);
+
+const Alignment Alignment::bottomRight = Alignment(1.0f, 1.0f);
diff --git a/src/layout/layout_component_style.cpp b/src/layout/layout_component_style.cpp
new file mode 100644
index 0000000..504a5bb
--- /dev/null
+++ b/src/layout/layout_component_style.cpp
@@ -0,0 +1,215 @@
+#include "rive/animation/keyframe_interpolator.hpp"
+#include "rive/core_context.hpp"
+#include "rive/layout_component.hpp"
+#include "rive/layout/layout_component_style.hpp"
+#include <vector>
+
+using namespace rive;
+
+#ifdef WITH_RIVE_LAYOUT
+
+KeyFrameInterpolator* LayoutComponentStyle::interpolator() { return m_interpolator; }
+
+LayoutStyleInterpolation LayoutComponentStyle::interpolation()
+{
+    return LayoutStyleInterpolation(interpolationType());
+}
+
+LayoutAnimationStyle LayoutComponentStyle::animationStyle()
+{
+    return LayoutAnimationStyle(animationStyleType());
+}
+
+LayoutAlignmentType LayoutComponentStyle::alignmentType()
+{
+    return LayoutAlignmentType(layoutAlignmentType());
+}
+
+LayoutScaleType LayoutComponentStyle::widthScaleType()
+{
+    return LayoutScaleType(layoutWidthScaleType());
+}
+
+LayoutScaleType LayoutComponentStyle::heightScaleType()
+{
+    return LayoutScaleType(layoutHeightScaleType());
+}
+
+YGDisplay LayoutComponentStyle::display() { return YGDisplay(displayValue()); }
+
+YGPositionType LayoutComponentStyle::positionType() { return YGPositionType(positionTypeValue()); }
+
+YGFlexDirection LayoutComponentStyle::flexDirection()
+{
+    return YGFlexDirection(flexDirectionValue());
+}
+
+YGDirection LayoutComponentStyle::direction() { return YGDirection(directionValue()); }
+
+YGWrap LayoutComponentStyle::flexWrap() { return YGWrap(flexWrapValue()); }
+
+YGAlign LayoutComponentStyle::alignItems() { return YGAlign(alignItemsValue()); }
+
+YGAlign LayoutComponentStyle::alignSelf() { return YGAlign(alignSelfValue()); }
+
+YGAlign LayoutComponentStyle::alignContent() { return YGAlign(alignContentValue()); }
+
+YGJustify LayoutComponentStyle::justifyContent() { return YGJustify(justifyContentValue()); }
+
+YGOverflow LayoutComponentStyle::overflow() { return YGOverflow(overflowValue()); }
+
+bool LayoutComponentStyle::intrinsicallySized() { return intrinsicallySizedValue() == 1; }
+
+YGUnit LayoutComponentStyle::widthUnits() { return YGUnit(widthUnitsValue()); }
+
+YGUnit LayoutComponentStyle::heightUnits() { return YGUnit(heightUnitsValue()); }
+
+YGUnit LayoutComponentStyle::borderLeftUnits() { return YGUnit(borderLeftUnitsValue()); }
+
+YGUnit LayoutComponentStyle::borderRightUnits() { return YGUnit(borderRightUnitsValue()); }
+
+YGUnit LayoutComponentStyle::borderTopUnits() { return YGUnit(borderTopUnitsValue()); }
+
+YGUnit LayoutComponentStyle::borderBottomUnits() { return YGUnit(borderBottomUnitsValue()); }
+
+YGUnit LayoutComponentStyle::marginLeftUnits() { return YGUnit(marginLeftUnitsValue()); }
+
+YGUnit LayoutComponentStyle::marginRightUnits() { return YGUnit(marginRightUnitsValue()); }
+
+YGUnit LayoutComponentStyle::marginTopUnits() { return YGUnit(marginTopUnitsValue()); }
+
+YGUnit LayoutComponentStyle::marginBottomUnits() { return YGUnit(marginBottomUnitsValue()); }
+
+YGUnit LayoutComponentStyle::paddingLeftUnits() { return YGUnit(paddingLeftUnitsValue()); }
+
+YGUnit LayoutComponentStyle::paddingRightUnits() { return YGUnit(paddingRightUnitsValue()); }
+
+YGUnit LayoutComponentStyle::paddingTopUnits() { return YGUnit(paddingTopUnitsValue()); }
+
+YGUnit LayoutComponentStyle::paddingBottomUnits() { return YGUnit(paddingBottomUnitsValue()); }
+
+YGUnit LayoutComponentStyle::positionLeftUnits() { return YGUnit(positionLeftUnitsValue()); }
+
+YGUnit LayoutComponentStyle::positionRightUnits() { return YGUnit(positionRightUnitsValue()); }
+
+YGUnit LayoutComponentStyle::positionTopUnits() { return YGUnit(positionTopUnitsValue()); }
+
+YGUnit LayoutComponentStyle::positionBottomUnits() { return YGUnit(positionBottomUnitsValue()); }
+
+YGUnit LayoutComponentStyle::gapHorizontalUnits() { return YGUnit(gapHorizontalUnitsValue()); }
+
+YGUnit LayoutComponentStyle::gapVerticalUnits() { return YGUnit(gapVerticalUnitsValue()); }
+
+YGUnit LayoutComponentStyle::maxWidthUnits() { return YGUnit(maxWidthUnitsValue()); }
+
+YGUnit LayoutComponentStyle::maxHeightUnits() { return YGUnit(maxHeightUnitsValue()); }
+
+YGUnit LayoutComponentStyle::minWidthUnits() { return YGUnit(minWidthUnitsValue()); }
+YGUnit LayoutComponentStyle::minHeightUnits() { return YGUnit(minHeightUnitsValue()); }
+void LayoutComponentStyle::markLayoutNodeDirty()
+{
+    if (parent()->is<LayoutComponent>())
+    {
+        parent()->as<LayoutComponent>()->markLayoutNodeDirty();
+    }
+}
+
+void LayoutComponentStyle::markLayoutStyleDirty()
+{
+    if (parent()->is<LayoutComponent>())
+    {
+        parent()->as<LayoutComponent>()->markLayoutStyleDirty();
+    }
+}
+
+StatusCode LayoutComponentStyle::onAddedDirty(CoreContext* context)
+{
+    auto code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+
+    auto coreObject = context->resolve(interpolatorId());
+    if (coreObject != nullptr && coreObject->is<KeyFrameInterpolator>())
+    {
+        m_interpolator = static_cast<KeyFrameInterpolator*>(coreObject);
+    }
+    return StatusCode::Ok;
+}
+#else
+void LayoutComponentStyle::markLayoutNodeDirty() {}
+void LayoutComponentStyle::markLayoutStyleDirty() {}
+#endif
+
+void LayoutComponentStyle::layoutAlignmentTypeChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::layoutWidthScaleTypeChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::layoutHeightScaleTypeChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::displayValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::positionTypeValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::overflowValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::intrinsicallySizedValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::flexDirectionValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::directionValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::alignContentValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::alignItemsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::alignSelfValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::justifyContentValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::flexWrapValueChanged() { markLayoutNodeDirty(); }
+
+void LayoutComponentStyle::flexChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::flexGrowChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::flexShrinkChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::flexBasisChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::aspectRatioChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::gapHorizontalChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::gapVerticalChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::maxWidthChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::maxHeightChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::minWidthChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::minHeightChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::borderLeftChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::borderRightChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::borderTopChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::borderBottomChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::marginLeftChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::marginRightChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::marginTopChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::marginBottomChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::paddingLeftChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::paddingRightChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::paddingTopChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::paddingBottomChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::positionLeftChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::positionRightChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::positionTopChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::positionBottomChanged() { markLayoutNodeDirty(); }
+
+void LayoutComponentStyle::widthUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::heightUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::gapHorizontalUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::gapVerticalUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::maxWidthUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::maxHeightUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::minWidthUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::minHeightUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::borderLeftUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::borderRightUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::borderTopUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::borderBottomUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::marginLeftUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::marginRightUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::marginTopUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::marginBottomUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::paddingLeftUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::paddingRightUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::paddingTopUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::paddingBottomUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::positionLeftUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::positionRightUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::positionTopUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::positionBottomUnitsValueChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::cornerRadiusTLChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::cornerRadiusTRChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::cornerRadiusBLChanged() { markLayoutNodeDirty(); }
+void LayoutComponentStyle::cornerRadiusBRChanged() { markLayoutNodeDirty(); }
\ No newline at end of file
diff --git a/src/layout_component.cpp b/src/layout_component.cpp
new file mode 100644
index 0000000..c72a614
--- /dev/null
+++ b/src/layout_component.cpp
@@ -0,0 +1,877 @@
+#include "rive/animation/keyframe_interpolator.hpp"
+#include "rive/artboard.hpp"
+#include "rive/drawable.hpp"
+#include "rive/factory.hpp"
+#include "rive/layout_component.hpp"
+#include "rive/node.hpp"
+#include "rive/math/aabb.hpp"
+#include "rive/shapes/paint/fill.hpp"
+#include "rive/shapes/paint/shape_paint.hpp"
+#include "rive/shapes/paint/stroke.hpp"
+#include "rive/shapes/rectangle.hpp"
+#include "rive/nested_artboard_layout.hpp"
+#ifdef WITH_RIVE_LAYOUT
+#include "rive/transform_component.hpp"
+#include "yoga/YGEnums.h"
+#include "yoga/YGFloatOptional.h"
+#endif
+#include <vector>
+
+using namespace rive;
+
+void LayoutComponent::buildDependencies()
+{
+    Super::buildDependencies();
+    if (parent() != nullptr)
+    {
+        parent()->addDependent(this);
+    }
+    // Set the blend mode on all the shape paints. If we ever animate this
+    // property, we'll need to update it in the update cycle/mark dirty when the
+    // blend mode changes.
+    for (auto paint : m_ShapePaints)
+    {
+        paint->blendMode(blendMode());
+    }
+}
+
+void LayoutComponent::drawProxy(Renderer* renderer)
+{
+    if (clip())
+    {
+        renderer->save();
+        renderer->clipPath(m_clipPath.get());
+    }
+    renderer->save();
+    renderer->transform(worldTransform());
+    for (auto shapePaint : m_ShapePaints)
+    {
+        if (!shapePaint->isVisible())
+        {
+            continue;
+        }
+        if (shapePaint->is<Fill>())
+        {
+            shapePaint->draw(renderer, m_backgroundPath.get(), &m_backgroundRect->rawPath());
+        }
+    }
+    renderer->restore();
+}
+
+void LayoutComponent::draw(Renderer* renderer)
+{
+    // Restore clip before drawing stroke so we don't clip the stroke
+    if (clip())
+    {
+        renderer->restore();
+    }
+    renderer->save();
+    renderer->transform(worldTransform());
+    for (auto shapePaint : m_ShapePaints)
+    {
+        if (!shapePaint->isVisible())
+        {
+            continue;
+        }
+        if (shapePaint->is<Stroke>())
+        {
+            shapePaint->draw(renderer, m_backgroundPath.get(), &m_backgroundRect->rawPath());
+        }
+    }
+    renderer->restore();
+}
+
+Core* LayoutComponent::hitTest(HitInfo*, const Mat2D&) { return nullptr; }
+
+void LayoutComponent::updateRenderPath()
+{
+    m_backgroundRect->width(m_layoutSizeWidth);
+    m_backgroundRect->height(m_layoutSizeHeight);
+    m_backgroundRect->linkCornerRadius(style()->linkCornerRadius());
+    m_backgroundRect->cornerRadiusTL(style()->cornerRadiusTL());
+    m_backgroundRect->cornerRadiusTR(style()->cornerRadiusTR());
+    m_backgroundRect->cornerRadiusBL(style()->cornerRadiusBL());
+    m_backgroundRect->cornerRadiusBR(style()->cornerRadiusBR());
+    m_backgroundRect->update(ComponentDirt::Path);
+
+    m_backgroundPath->rewind();
+    m_backgroundRect->rawPath().addTo(m_backgroundPath.get());
+
+    RawPath clipPath;
+    clipPath.addPath(m_backgroundRect->rawPath(), &m_WorldTransform);
+    m_clipPath = artboard()->factory()->makeRenderPath(clipPath, FillRule::nonZero);
+}
+
+void LayoutComponent::update(ComponentDirt value)
+{
+    Super::update(value);
+    if (hasDirt(value, ComponentDirt::RenderOpacity))
+    {
+        propagateOpacity(childOpacity());
+    }
+    if (parent() != nullptr && hasDirt(value, ComponentDirt::WorldTransform))
+    {
+        Mat2D parentWorld = parent()->is<WorldTransformComponent>()
+                                ? (parent()->as<WorldTransformComponent>())->worldTransform()
+                                : Mat2D();
+        auto location = Vec2D(m_layoutLocationX, m_layoutLocationY);
+        if (parent()->is<Artboard>())
+        {
+            auto art = parent()->as<Artboard>();
+            location -=
+                Vec2D(art->layoutWidth() * art->originX(), art->layoutHeight() * art->originY());
+        }
+        auto transform = Mat2D::fromTranslation(location);
+        m_WorldTransform = Mat2D::multiply(parentWorld, transform);
+        updateConstraints();
+    }
+    if (hasDirt(value, ComponentDirt::Path))
+    {
+        updateRenderPath();
+    }
+}
+
+void LayoutComponent::widthOverride(float width, int unitValue, bool isRow)
+{
+    m_widthOverride = width;
+    m_widthUnitValueOverride = unitValue;
+    m_parentIsRow = isRow;
+    markLayoutNodeDirty();
+}
+
+void LayoutComponent::heightOverride(float height, int unitValue, bool isRow)
+{
+    m_heightOverride = height;
+    m_heightUnitValueOverride = unitValue;
+    m_parentIsRow = isRow;
+    markLayoutNodeDirty();
+}
+
+#ifdef WITH_RIVE_LAYOUT
+StatusCode LayoutComponent::onAddedDirty(CoreContext* context)
+{
+    auto code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+
+    auto coreStyle = context->resolve(styleId());
+    if (coreStyle == nullptr || !coreStyle->is<LayoutComponentStyle>())
+    {
+        return StatusCode::MissingObject;
+    }
+    m_style = static_cast<LayoutComponentStyle*>(coreStyle);
+    addChild(m_style);
+
+    return StatusCode::Ok;
+}
+
+StatusCode LayoutComponent::onAddedClean(CoreContext* context)
+{
+    auto code = Super::onAddedClean(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+    artboard()->markLayoutDirty(this);
+    markLayoutStyleDirty();
+    m_backgroundPath = artboard()->factory()->makeEmptyRenderPath();
+    m_clipPath = artboard()->factory()->makeEmptyRenderPath();
+    m_backgroundRect->originX(0);
+    m_backgroundRect->originY(0);
+    syncLayoutChildren();
+    return StatusCode::Ok;
+}
+
+static YGSize measureFunc(YGNode* node,
+                          float width,
+                          YGMeasureMode widthMode,
+                          float height,
+                          YGMeasureMode heightMode)
+{
+    Vec2D size = ((LayoutComponent*)node->getContext())
+                     ->measureLayout(width,
+                                     (LayoutMeasureMode)widthMode,
+                                     height,
+                                     (LayoutMeasureMode)heightMode);
+
+    return YGSize{size.x, size.y};
+}
+
+Vec2D LayoutComponent::measureLayout(float width,
+                                     LayoutMeasureMode widthMode,
+                                     float height,
+                                     LayoutMeasureMode heightMode)
+{
+    Vec2D size = Vec2D();
+    for (auto child : children())
+    {
+        if (child->is<LayoutComponent>())
+        {
+            continue;
+        }
+        //  && child->is<TransformComponent>()->canMeasure() for nested artboard layout
+        if (child->is<TransformComponent>())
+        {
+            auto transformComponent = child->as<TransformComponent>();
+            Vec2D measured =
+                transformComponent->measureLayout(width, widthMode, height, heightMode);
+            size = Vec2D(std::max(size.x, measured.x), std::max(size.y, measured.y));
+        }
+    }
+    return size;
+}
+
+bool LayoutComponent::mainAxisIsRow()
+{
+    return style()->flexDirection() == YGFlexDirectionRow ||
+           style()->flexDirection() == YGFlexDirectionRowReverse;
+}
+
+bool LayoutComponent::mainAxisIsColumn()
+{
+    return style()->flexDirection() == YGFlexDirectionColumn ||
+           style()->flexDirection() == YGFlexDirectionColumnReverse;
+}
+
+void LayoutComponent::syncStyle()
+{
+    if (m_style == nullptr)
+    {
+        return;
+    }
+    YGNode& ygNode = layoutNode();
+    YGStyle& ygStyle = layoutStyle();
+    if (m_style->intrinsicallySized())
+    {
+        ygNode.setContext(this);
+        ygNode.setMeasureFunc(measureFunc);
+    }
+    else
+    {
+        ygNode.setMeasureFunc(nullptr);
+    }
+
+    auto realWidth = width();
+    auto realWidthUnits = m_style->widthUnits();
+    auto realWidthScaleType = m_style->widthScaleType();
+    auto realHeight = height();
+    auto realHeightUnits = m_style->heightUnits();
+    auto realHeightScaleType = m_style->heightScaleType();
+    auto parentIsRow = layoutParent() != nullptr ? layoutParent()->mainAxisIsRow() : true;
+
+    // If we have override width/height values, use those.
+    // Currently we only use these for Artboards that are part of a NestedArtboardLayout
+    // but perhaps there will be other use cases for overriding in the future?
+    if (canHaveOverrides())
+    {
+        if (!std::isnan(m_widthOverride))
+        {
+            realWidth = m_widthOverride;
+        }
+        if (!std::isnan(m_heightOverride))
+        {
+            realHeight = m_heightOverride;
+        }
+        parentIsRow = m_parentIsRow;
+
+        if (m_widthUnitValueOverride != -1)
+        {
+            realWidthUnits = YGUnit(m_widthUnitValueOverride);
+            switch (realWidthUnits)
+            {
+                case YGUnitPoint:
+                case YGUnitPercent:
+                    realWidthScaleType = LayoutScaleType::fixed;
+                    break;
+                case YGUnitAuto:
+                    realWidthScaleType = LayoutScaleType::fill;
+                    break;
+                default:
+                    break;
+            }
+        }
+        if (m_heightUnitValueOverride != -1)
+        {
+            realHeightUnits = YGUnit(m_heightUnitValueOverride);
+            switch (realHeightUnits)
+            {
+                case YGUnitPoint:
+                case YGUnitPercent:
+                    realHeightScaleType = LayoutScaleType::fixed;
+                    break;
+                case YGUnitAuto:
+                    realHeightScaleType = LayoutScaleType::fill;
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+    ygStyle.dimensions()[YGDimensionWidth] = YGValue{realWidth, realWidthUnits};
+    ygStyle.dimensions()[YGDimensionHeight] = YGValue{realHeight, realHeightUnits};
+
+    switch (realWidthScaleType)
+    {
+        case LayoutScaleType::fixed:
+            if (parentIsRow)
+            {
+                ygStyle.flexGrow() = YGFloatOptional(0);
+            }
+            break;
+        case LayoutScaleType::fill:
+            if (parentIsRow)
+            {
+                ygStyle.flexGrow() = YGFloatOptional(1);
+            }
+            else
+            {
+                ygStyle.alignSelf() = YGAlignStretch;
+            }
+            break;
+        case LayoutScaleType::hug:
+            if (parentIsRow)
+            {
+                ygStyle.flexGrow() = YGFloatOptional(0);
+            }
+            else
+            {
+                ygStyle.alignSelf() = YGAlignAuto;
+            }
+            break;
+        default:
+            break;
+    }
+
+    switch (realHeightScaleType)
+    {
+        case LayoutScaleType::fixed:
+            if (!parentIsRow)
+            {
+                ygStyle.flexGrow() = YGFloatOptional(0);
+            }
+            break;
+        case LayoutScaleType::fill:
+            if (!parentIsRow)
+            {
+                ygStyle.flexGrow() = YGFloatOptional(1);
+            }
+            else
+            {
+                ygStyle.alignSelf() = YGAlignStretch;
+            }
+            break;
+        case LayoutScaleType::hug:
+            if (!parentIsRow)
+            {
+                ygStyle.flexGrow() = YGFloatOptional(0);
+            }
+            else
+            {
+                ygStyle.alignSelf() = YGAlignAuto;
+            }
+            break;
+        default:
+            break;
+    }
+
+    bool isRowForAlignment = mainAxisIsRow();
+    switch (m_style->alignmentType())
+    {
+        case LayoutAlignmentType::topLeft:
+        case LayoutAlignmentType::topCenter:
+        case LayoutAlignmentType::topRight:
+        case LayoutAlignmentType::spaceBetweenStart:
+            if (isRowForAlignment)
+            {
+                ygStyle.alignItems() = YGAlignFlexStart;
+                ygStyle.alignContent() = YGAlignFlexStart;
+            }
+            else
+            {
+                ygStyle.justifyContent() = YGJustifyFlexStart;
+            }
+            break;
+        case LayoutAlignmentType::centerLeft:
+        case LayoutAlignmentType::center:
+        case LayoutAlignmentType::centerRight:
+        case LayoutAlignmentType::spaceBetweenCenter:
+            if (isRowForAlignment)
+            {
+                ygStyle.alignItems() = YGAlignCenter;
+                ygStyle.alignContent() = YGAlignCenter;
+            }
+            else
+            {
+                ygStyle.justifyContent() = YGJustifyCenter;
+            }
+            break;
+        case LayoutAlignmentType::bottomLeft:
+        case LayoutAlignmentType::bottomCenter:
+        case LayoutAlignmentType::bottomRight:
+        case LayoutAlignmentType::spaceBetweenEnd:
+            if (isRowForAlignment)
+            {
+                ygStyle.alignItems() = YGAlignFlexEnd;
+                ygStyle.alignContent() = YGAlignFlexEnd;
+            }
+            else
+            {
+                ygStyle.justifyContent() = YGJustifyFlexEnd;
+            }
+            break;
+    }
+    switch (m_style->alignmentType())
+    {
+        case LayoutAlignmentType::topLeft:
+        case LayoutAlignmentType::centerLeft:
+        case LayoutAlignmentType::bottomLeft:
+            if (isRowForAlignment)
+            {
+                ygStyle.justifyContent() = YGJustifyFlexStart;
+            }
+            else
+            {
+                ygStyle.alignItems() = YGAlignFlexStart;
+                ygStyle.alignContent() = YGAlignFlexStart;
+            }
+            break;
+        case LayoutAlignmentType::topCenter:
+        case LayoutAlignmentType::center:
+        case LayoutAlignmentType::bottomCenter:
+            if (isRowForAlignment)
+            {
+                ygStyle.justifyContent() = YGJustifyCenter;
+            }
+            else
+            {
+                ygStyle.alignItems() = YGAlignCenter;
+                ygStyle.alignContent() = YGAlignCenter;
+            }
+            break;
+        case LayoutAlignmentType::topRight:
+        case LayoutAlignmentType::centerRight:
+        case LayoutAlignmentType::bottomRight:
+            if (isRowForAlignment)
+            {
+                ygStyle.justifyContent() = YGJustifyFlexEnd;
+            }
+            else
+            {
+                ygStyle.alignItems() = YGAlignFlexEnd;
+                ygStyle.alignContent() = YGAlignFlexEnd;
+            }
+            break;
+        case LayoutAlignmentType::spaceBetweenStart:
+        case LayoutAlignmentType::spaceBetweenCenter:
+        case LayoutAlignmentType::spaceBetweenEnd:
+            ygStyle.justifyContent() = YGJustifySpaceBetween;
+            break;
+    }
+
+    ygStyle.minDimensions()[YGDimensionWidth] =
+        YGValue{m_style->minWidth(), m_style->minWidthUnits()};
+    ygStyle.minDimensions()[YGDimensionHeight] =
+        YGValue{m_style->minHeight(), m_style->minHeightUnits()};
+    ygStyle.maxDimensions()[YGDimensionWidth] =
+        YGValue{m_style->maxWidth(), m_style->maxWidthUnits()};
+    ygStyle.maxDimensions()[YGDimensionHeight] =
+        YGValue{m_style->maxHeight(), m_style->maxHeightUnits()};
+    ygStyle.gap()[YGGutterColumn] =
+        YGValue{m_style->gapHorizontal(), m_style->gapHorizontalUnits()};
+    ygStyle.gap()[YGGutterRow] = YGValue{m_style->gapVertical(), m_style->gapVerticalUnits()};
+    ygStyle.border()[YGEdgeLeft] = YGValue{m_style->borderLeft(), m_style->borderLeftUnits()};
+    ygStyle.border()[YGEdgeRight] = YGValue{m_style->borderRight(), m_style->borderRightUnits()};
+    ygStyle.border()[YGEdgeTop] = YGValue{m_style->borderTop(), m_style->borderTopUnits()};
+    ygStyle.border()[YGEdgeBottom] = YGValue{m_style->borderBottom(), m_style->borderBottomUnits()};
+    ygStyle.margin()[YGEdgeLeft] = YGValue{m_style->marginLeft(), m_style->marginLeftUnits()};
+    ygStyle.margin()[YGEdgeRight] = YGValue{m_style->marginRight(), m_style->marginRightUnits()};
+    ygStyle.margin()[YGEdgeTop] = YGValue{m_style->marginTop(), m_style->marginTopUnits()};
+    ygStyle.margin()[YGEdgeBottom] = YGValue{m_style->marginBottom(), m_style->marginBottomUnits()};
+    ygStyle.padding()[YGEdgeLeft] = YGValue{m_style->paddingLeft(), m_style->paddingLeftUnits()};
+    ygStyle.padding()[YGEdgeRight] = YGValue{m_style->paddingRight(), m_style->paddingRightUnits()};
+    ygStyle.padding()[YGEdgeTop] = YGValue{m_style->paddingTop(), m_style->paddingTopUnits()};
+    ygStyle.padding()[YGEdgeBottom] =
+        YGValue{m_style->paddingBottom(), m_style->paddingBottomUnits()};
+    ygStyle.position()[YGEdgeLeft] = YGValue{m_style->positionLeft(), m_style->positionLeftUnits()};
+    ygStyle.position()[YGEdgeRight] =
+        YGValue{m_style->positionRight(), m_style->positionRightUnits()};
+    ygStyle.position()[YGEdgeTop] = YGValue{m_style->positionTop(), m_style->positionTopUnits()};
+    ygStyle.position()[YGEdgeBottom] =
+        YGValue{m_style->positionBottom(), m_style->positionBottomUnits()};
+
+    ygStyle.display() = m_style->display();
+    ygStyle.positionType() = m_style->positionType();
+    ygStyle.flex() = YGFloatOptional(m_style->flex());
+    ygStyle.flexDirection() = m_style->flexDirection();
+    ygStyle.flexWrap() = m_style->flexWrap();
+
+    ygNode.setStyle(ygStyle);
+}
+
+void LayoutComponent::syncLayoutChildren()
+{
+    auto ourNode = &layoutNode();
+    YGNodeRemoveAllChildren(ourNode);
+    int index = 0;
+    for (auto child : children())
+    {
+        YGNode* node = nullptr;
+        switch (child->coreType())
+        {
+            case LayoutComponentBase::typeKey:
+                node = &child->as<LayoutComponent>()->layoutNode();
+                break;
+            case NestedArtboardLayoutBase::typeKey:
+                node = static_cast<YGNode*>(child->as<NestedArtboardLayout>()->layoutNode());
+                break;
+        }
+        if (node != nullptr)
+        {
+            // YGNodeInsertChild(ourNode, node, index++);
+            ourNode->insertChild(node, index++);
+            node->setOwner(ourNode);
+            ourNode->markDirtyAndPropagate();
+        }
+    }
+    markLayoutNodeDirty();
+}
+
+void LayoutComponent::propagateSize() { propagateSizeToChildren(this); }
+
+void LayoutComponent::propagateSizeToChildren(ContainerComponent* component)
+{
+    for (auto child : component->children())
+    {
+        if (child->is<LayoutComponent>() || child->coreType() == NodeBase::typeKey)
+        {
+            continue;
+        }
+        if (child->is<TransformComponent>())
+        {
+            auto sizableChild = child->as<TransformComponent>();
+            sizableChild->controlSize(Vec2D(m_layoutSizeWidth, m_layoutSizeHeight));
+        }
+        if (child->is<ContainerComponent>())
+        {
+            propagateSizeToChildren(child->as<ContainerComponent>());
+        }
+    }
+}
+
+void LayoutComponent::calculateLayout()
+{
+    YGNodeCalculateLayout(&layoutNode(), width(), height(), YGDirection::YGDirectionInherit);
+}
+
+void LayoutComponent::onDirty(ComponentDirt value)
+{
+    Super::onDirty(value);
+    if ((value & ComponentDirt::WorldTransform) == ComponentDirt::WorldTransform && clip())
+    {
+        addDirt(ComponentDirt::Path);
+    }
+}
+
+void LayoutComponent::updateLayoutBounds()
+{
+    auto node = &layoutNode();
+    auto left = YGNodeLayoutGetLeft(node);
+    auto top = YGNodeLayoutGetTop(node);
+    auto width = YGNodeLayoutGetWidth(node);
+    auto height = YGNodeLayoutGetHeight(node);
+
+#ifdef DEBUG
+    // Temporarily here to keep track of an issue.
+    if (left != left || top != top || width != width || height != height)
+    {
+        fprintf(stderr,
+                "Layout returned nan: %f %f %f %f | %p %s\n",
+                left,
+                top,
+                width,
+                height,
+                YGNodeGetParent(node),
+                name().c_str());
+        return;
+    }
+#endif
+    if (animates())
+    {
+        auto toBounds = m_animationData.toBounds;
+        if (left != toBounds.left() || top != toBounds.top() || width != toBounds.width() ||
+            height != toBounds.height())
+        {
+            m_animationData.fromBounds = AABB(m_layoutLocationX,
+                                              m_layoutLocationY,
+                                              m_layoutLocationX + this->width(),
+                                              m_layoutLocationY + this->height());
+            m_animationData.toBounds = AABB(left, top, left + width, top + height);
+            if (m_animationData.elapsedSeconds > 0.1)
+            {
+                m_animationData.elapsedSeconds = 0;
+            }
+            propagateSize();
+            markWorldTransformDirty();
+        }
+    }
+    else
+
+        if (left != m_layoutLocationX || top != m_layoutLocationY || width != m_layoutSizeWidth ||
+            height != m_layoutSizeHeight)
+    {
+        if (m_layoutSizeWidth != width || m_layoutSizeHeight != height)
+        {
+            // Width changed, we need to rebuild the path.
+            addDirt(ComponentDirt::Path);
+        }
+        m_layoutLocationX = left;
+        m_layoutLocationY = top;
+        m_layoutSizeWidth = width;
+        m_layoutSizeHeight = height;
+
+        propagateSize();
+        markWorldTransformDirty();
+    }
+}
+
+bool LayoutComponent::advance(double elapsedSeconds) { return applyInterpolation(elapsedSeconds); }
+
+bool LayoutComponent::animates()
+{
+    if (m_style == nullptr)
+    {
+        return false;
+    }
+    return m_style->positionType() == YGPositionType::YGPositionTypeRelative &&
+           m_style->animationStyle() != LayoutAnimationStyle::none &&
+           interpolation() != LayoutStyleInterpolation::hold && interpolationTime() > 0;
+}
+
+LayoutAnimationStyle LayoutComponent::animationStyle()
+{
+    if (m_style == nullptr)
+    {
+        return LayoutAnimationStyle::none;
+    }
+    return m_style->animationStyle();
+}
+
+KeyFrameInterpolator* LayoutComponent::interpolator()
+{
+    if (m_style == nullptr)
+    {
+        return nullptr;
+    }
+    switch (m_style->animationStyle())
+    {
+        case LayoutAnimationStyle::inherit:
+            return m_inheritedInterpolator != nullptr ? m_inheritedInterpolator
+                                                      : m_style->interpolator();
+        case LayoutAnimationStyle::custom:
+            return m_style->interpolator();
+        default:
+            return nullptr;
+    }
+}
+
+LayoutStyleInterpolation LayoutComponent::interpolation()
+{
+    auto defaultInterpolation = LayoutStyleInterpolation::hold;
+    if (m_style == nullptr)
+    {
+        return defaultInterpolation;
+    }
+    switch (m_style->animationStyle())
+    {
+        case LayoutAnimationStyle::inherit:
+            return m_inheritedInterpolation;
+        case LayoutAnimationStyle::custom:
+            return m_style->interpolation();
+        default:
+            return defaultInterpolation;
+    }
+}
+
+float LayoutComponent::interpolationTime()
+{
+    if (m_style == nullptr)
+    {
+        return 0;
+    }
+    switch (m_style->animationStyle())
+    {
+        case LayoutAnimationStyle::inherit:
+            return m_inheritedInterpolationTime;
+        case LayoutAnimationStyle::custom:
+            return m_style->interpolationTime();
+        default:
+            return 0;
+    }
+}
+
+void LayoutComponent::cascadeAnimationStyle(LayoutStyleInterpolation inheritedInterpolation,
+                                            KeyFrameInterpolator* inheritedInterpolator,
+                                            float inheritedInterpolationTime)
+{
+    if (m_style != nullptr && m_style->animationStyle() == LayoutAnimationStyle::inherit)
+    {
+        setInheritedInterpolation(inheritedInterpolation,
+                                  inheritedInterpolator,
+                                  inheritedInterpolationTime);
+    }
+    else
+    {
+        clearInheritedInterpolation();
+    }
+    for (auto child : children())
+    {
+        if (child->is<LayoutComponent>())
+        {
+            child->as<LayoutComponent>()->cascadeAnimationStyle(interpolation(),
+                                                                interpolator(),
+                                                                interpolationTime());
+        }
+    }
+}
+
+void LayoutComponent::setInheritedInterpolation(LayoutStyleInterpolation inheritedInterpolation,
+                                                KeyFrameInterpolator* inheritedInterpolator,
+                                                float inheritedInterpolationTime)
+{
+    m_inheritedInterpolation = inheritedInterpolation;
+    m_inheritedInterpolator = inheritedInterpolator;
+    m_inheritedInterpolationTime = inheritedInterpolationTime;
+}
+
+void LayoutComponent::clearInheritedInterpolation()
+{
+    m_inheritedInterpolation = LayoutStyleInterpolation::hold;
+    m_inheritedInterpolator = nullptr;
+    m_inheritedInterpolationTime = 0;
+}
+
+bool LayoutComponent::applyInterpolation(double elapsedSeconds)
+{
+    if (!animates() || m_style == nullptr || m_animationData.toBounds == layoutBounds())
+    {
+        return false;
+    }
+
+    if (m_animationData.elapsedSeconds >= interpolationTime())
+    {
+        m_layoutLocationX = m_animationData.toBounds.left();
+        m_layoutLocationY = m_animationData.toBounds.top();
+
+        float width = m_animationData.toBounds.width();
+        float height = m_animationData.toBounds.height();
+        if (width != m_layoutSizeWidth || height != m_layoutSizeHeight)
+        {
+            addDirt(ComponentDirt::Path);
+        }
+        m_layoutSizeWidth = width;
+        m_layoutSizeHeight = height;
+
+        m_animationData.elapsedSeconds = 0;
+        propagateSize();
+        markWorldTransformDirty();
+
+        return false;
+    }
+    float f = 1;
+    if (interpolationTime() > 0)
+    {
+        f = m_animationData.elapsedSeconds / interpolationTime();
+    }
+    bool needsAdvance = false;
+    auto fromBounds = m_animationData.fromBounds;
+    auto toBounds = m_animationData.toBounds;
+    auto left = m_layoutLocationX;
+    auto top = m_layoutLocationY;
+    auto width = m_layoutSizeWidth;
+    auto height = m_layoutSizeHeight;
+    if (toBounds.left() != left || toBounds.top() != top)
+    {
+        if (interpolation() == LayoutStyleInterpolation::linear)
+        {
+            left = fromBounds.left() + f * (toBounds.left() - fromBounds.left());
+            top = fromBounds.top() + f * (toBounds.top() - fromBounds.top());
+        }
+        else
+        {
+            if (interpolator() != nullptr)
+            {
+                left = interpolator()->transformValue(fromBounds.left(), toBounds.left(), f);
+                top = interpolator()->transformValue(fromBounds.top(), toBounds.top(), f);
+            }
+        }
+        needsAdvance = true;
+        m_layoutLocationX = left;
+        m_layoutLocationY = top;
+    }
+    if (toBounds.width() != width || toBounds.height() != height)
+    {
+        if (interpolation() == LayoutStyleInterpolation::linear)
+        {
+            width = fromBounds.width() + f * (toBounds.width() - fromBounds.width());
+            height = fromBounds.height() + f * (toBounds.height() - fromBounds.height());
+        }
+        else
+        {
+            if (interpolator() != nullptr)
+            {
+                width = interpolator()->transformValue(fromBounds.width(), toBounds.width(), f);
+                height = interpolator()->transformValue(fromBounds.height(), toBounds.height(), f);
+            }
+        }
+        needsAdvance = true;
+        m_layoutSizeWidth = width;
+        m_layoutSizeHeight = height;
+        addDirt(ComponentDirt::Path);
+    }
+    m_animationData.elapsedSeconds = m_animationData.elapsedSeconds + (float)elapsedSeconds;
+    if (needsAdvance)
+    {
+        propagateSize();
+        markWorldTransformDirty();
+    }
+    return needsAdvance;
+}
+
+void LayoutComponent::markLayoutNodeDirty()
+{
+    layoutNode().markDirtyAndPropagate();
+    artboard()->markLayoutDirty(this);
+}
+
+void LayoutComponent::markLayoutStyleDirty()
+{
+    clearInheritedInterpolation();
+    addDirt(ComponentDirt::LayoutStyle);
+    if (artboard() != this)
+    {
+        artboard()->markLayoutStyleDirty();
+    }
+}
+#else
+Vec2D LayoutComponent::measureLayout(float width,
+                                     LayoutMeasureMode widthMode,
+                                     float height,
+                                     LayoutMeasureMode heightMode)
+{
+    return Vec2D();
+}
+
+void LayoutComponent::markLayoutNodeDirty() {}
+void LayoutComponent::markLayoutStyleDirty() {}
+void LayoutComponent::onDirty(ComponentDirt value) {}
+bool LayoutComponent::mainAxisIsRow() { return true; }
+
+bool LayoutComponent::mainAxisIsColumn() { return false; }
+#endif
+
+void LayoutComponent::clipChanged() { markLayoutNodeDirty(); }
+void LayoutComponent::widthChanged() { markLayoutNodeDirty(); }
+void LayoutComponent::heightChanged() { markLayoutNodeDirty(); }
+void LayoutComponent::styleIdChanged() { markLayoutNodeDirty(); }
diff --git a/src/math/aabb.cpp b/src/math/aabb.cpp
new file mode 100644
index 0000000..c6fa11a
--- /dev/null
+++ b/src/math/aabb.cpp
@@ -0,0 +1,87 @@
+#include "rive/math/math_types.hpp"
+#include "rive/math/aabb.hpp"
+#include "rive/math/simd.hpp"
+#include <algorithm>
+#include <cmath>
+
+using namespace rive;
+
+AABB::AABB(Span<Vec2D> pts)
+{
+    if (pts.size() == 0)
+    {
+        minX = minY = maxX = maxY = 0;
+        return;
+    }
+
+    float L = pts[0].x, R = L, T = pts[0].y, B = T;
+
+    for (size_t i = 1; i < pts.size(); ++i)
+    {
+        L = std::min(L, pts[i].x);
+        R = std::max(R, pts[i].x);
+        T = std::min(T, pts[i].y);
+        B = std::max(B, pts[i].y);
+    }
+    minX = L;
+    maxX = R;
+    minY = T;
+    maxY = B;
+}
+
+static inline float graphics_roundf(float x) { return std::floor(x + 0.5f); }
+
+static inline int graphics_round(float x) { return (int)graphics_roundf(x); }
+
+IAABB AABB::round() const
+{
+    return {
+        graphics_round(left()),
+        graphics_round(top()),
+        graphics_round(right()),
+        graphics_round(bottom()),
+    };
+}
+
+IAABB AABB::roundOut() const
+{
+    float4 bounds = simd::load4f(this);
+    bounds.xy = simd::floor(bounds.xy);
+    bounds.zw = simd::ceil(bounds.zw);
+    return math::bit_cast<IAABB>(simd::cast<int32_t>(bounds));
+}
+
+void AABB::expandTo(AABB& out, const Vec2D& point) { expandTo(out, point.x, point.y); }
+
+void AABB::expandTo(AABB& out, float x, float y)
+{
+    if (x < out.minX)
+    {
+        out.minX = x;
+    }
+    if (x > out.maxX)
+    {
+        out.maxX = x;
+    }
+    if (y < out.minY)
+    {
+        out.minY = y;
+    }
+    if (y > out.maxY)
+    {
+        out.maxY = y;
+    }
+}
+
+void AABB::join(AABB& out, const AABB& a, const AABB& b)
+{
+    out.minX = std::min(a.minX, b.minX);
+    out.minY = std::min(a.minY, b.minY);
+    out.maxX = std::max(a.maxX, b.maxX);
+    out.maxY = std::max(a.maxY, b.maxY);
+}
+
+bool AABB::contains(Vec2D point) const
+{
+    return point.x >= left() && point.x <= right() && point.y >= top() && point.y <= bottom();
+}
diff --git a/src/math/bit_field_loc.cpp b/src/math/bit_field_loc.cpp
new file mode 100644
index 0000000..377b10a
--- /dev/null
+++ b/src/math/bit_field_loc.cpp
@@ -0,0 +1,20 @@
+#include "rive/math/bit_field_loc.hpp"
+#include <cassert>
+
+using namespace rive;
+
+BitFieldLoc::BitFieldLoc(uint32_t start, uint32_t end) : m_start(start)
+{
+    assert(end >= start);
+    assert(end < 32);
+
+    m_count = end - start + 1;
+    m_mask = ((1 << (end - start + 1)) - 1) << start;
+}
+
+uint32_t BitFieldLoc::read(uint32_t bits) { return (bits & m_mask) >> m_start; }
+
+uint32_t BitFieldLoc::write(uint32_t bits, uint32_t value)
+{
+    return (bits & ~m_mask) | ((value << m_start) & m_mask);
+}
\ No newline at end of file
diff --git a/src/math/contour_measure.cpp b/src/math/contour_measure.cpp
new file mode 100644
index 0000000..89c1e42
--- /dev/null
+++ b/src/math/contour_measure.cpp
@@ -0,0 +1,548 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "rive/core/type_conversions.hpp"
+#include "rive/math/raw_path_utils.hpp"
+#include "rive/math/contour_measure.hpp"
+#include "rive/math/math_types.hpp"
+#include "rive/math/wangs_formula.hpp"
+#include <cmath>
+
+using namespace rive;
+
+enum SegmentType
+{ // must fit in 2 bits
+    kLine,
+    kQuad,
+    kCubic,
+    // unused for now
+};
+
+/*
+ *  Inspired by Skia's SkContourMeasure
+ */
+
+ContourMeasure::ContourMeasure(std::vector<Segment>&& segs,
+                               std::vector<Vec2D>&& pts,
+                               float length,
+                               bool isClosed) :
+    m_segments(std::move(segs)), m_points(std::move(pts)), m_length(length), m_isClosed(isClosed)
+{}
+
+// Return index of the segment that contains distance,
+// or the last segment if distance == m_distance
+size_t ContourMeasure::findSegment(float distance) const
+{
+    assert(m_segments.front().m_distance >= 0);
+    assert(m_segments.back().m_distance == m_length);
+
+    assert(distance >= 0 && distance <= m_length);
+
+    const Segment seg = {distance, 0, 0, 0};
+    auto iter = std::lower_bound(m_segments.begin(), m_segments.end(), seg);
+    assert(iter != m_segments.end());
+    assert(iter->m_distance >= distance);
+    assert(iter->m_ptIndex < m_points.size());
+    return iter - m_segments.begin();
+}
+
+static ContourMeasure::PosTan eval_quad(const Vec2D pts[], float t)
+{
+    assert(t >= 0 && t <= 1);
+
+    const EvalQuad eval(pts);
+
+    // Compute derivative as at + b
+    auto a = two(eval.a);
+    auto b = eval.b;
+
+    return {
+        eval(t),
+        (a * t + b).normalized(),
+    };
+}
+
+static ContourMeasure::PosTan eval_cubic(const Vec2D pts[], float t)
+{
+    assert(t >= 0 && t <= 1);
+    // When t==0 and t==1, the most accurate way to find tangents is by differencing.
+    if (t == 0 || t == 1)
+    {
+        if (t == 0)
+        {
+            return {pts[0],
+                    (pts[0] != pts[1]   ? pts[1]
+                     : pts[1] != pts[2] ? pts[2]
+                                        : pts[3]) -
+                        pts[0]
+
+            };
+        }
+        else
+        {
+            return {pts[3],
+                    pts[3] - (pts[3] != pts[2]   ? pts[2]
+                              : pts[2] != pts[1] ? pts[1]
+                                                 : pts[0])};
+        }
+    }
+
+    const EvalCubic eval(pts);
+
+    // Compute derivative as at^2 + bt + c;
+    auto a = eval.a * 3;
+    auto b = two(eval.b);
+    auto c = eval.c;
+
+    return {
+        eval(t),
+        ((a * t + b) * t + c).normalized(),
+    };
+}
+
+void ContourMeasure::Segment::extract(RawPath* dst, const Vec2D pts[]) const
+{
+    pts += m_ptIndex;
+    switch (m_type)
+    {
+        case SegmentType::kLine:
+            dst->line(pts[1]);
+            break;
+        case SegmentType::kQuad:
+            dst->quad(pts[1], pts[2]);
+            break;
+        case SegmentType::kCubic:
+            dst->cubic(pts[1], pts[2], pts[3]);
+            break;
+    }
+}
+
+void ContourMeasure::Segment::extract(RawPath* dst,
+                                      float fromT,
+                                      float toT,
+                                      const Vec2D pts[],
+                                      bool moveTo) const
+{
+    assert(fromT <= toT);
+    pts += m_ptIndex;
+
+    Vec2D tmp[4];
+    switch (m_type)
+    {
+        case SegmentType::kLine:
+            line_extract(pts, fromT, toT, tmp);
+            if (moveTo)
+            {
+                dst->move(tmp[0]);
+            }
+            dst->line(tmp[1]);
+            break;
+        case SegmentType::kQuad:
+            quad_extract(pts, fromT, toT, tmp);
+            if (moveTo)
+            {
+                dst->move(tmp[0]);
+            }
+            dst->quad(tmp[1], tmp[2]);
+            break;
+        case SegmentType::kCubic:
+            cubic_extract(pts, fromT, toT, tmp);
+            if (moveTo)
+            {
+                dst->move(tmp[0]);
+            }
+            dst->cubic(tmp[1], tmp[2], tmp[3]);
+            break;
+    }
+}
+
+ContourMeasure::PosTan ContourMeasure::getPosTan(float distance) const
+{
+    // specal-case end of the contour
+    if (distance > m_length)
+    {
+        distance = m_length;
+    }
+
+    if (distance < 0)
+    {
+        distance = 0;
+    }
+
+    size_t i = this->findSegment(distance);
+    assert(i < m_segments.size());
+    const auto seg = m_segments[i];
+    const float currD = seg.m_distance;
+    const float prevD = i > 0 ? m_segments[i - 1].m_distance : 0;
+
+    assert(prevD < currD);
+    assert(distance <= currD);
+    assert(distance >= prevD);
+
+    const float relD = (distance - prevD) / (currD - prevD);
+    assert(relD >= 0 && relD <= 1);
+
+    if (seg.m_type == SegmentType::kLine)
+    {
+        assert(seg.m_ptIndex + 1 < m_points.size());
+        auto p0 = m_points[seg.m_ptIndex + 0];
+        auto p1 = m_points[seg.m_ptIndex + 1];
+        return {
+            Vec2D::lerp(p0, p1, relD),
+            (p1 - p0).normalized(),
+        };
+    }
+
+    float prevT = 0;
+    if (i > 0)
+    {
+        auto prev = m_segments[i - 1];
+        if (prev.m_ptIndex == seg.m_ptIndex)
+        {
+            prevT = prev.getT();
+        }
+    }
+
+    const float t = lerp(prevT, seg.getT(), relD);
+    assert(t >= 0 && t <= 1);
+
+    if (seg.m_type == SegmentType::kQuad)
+    {
+        assert(seg.m_ptIndex + 2 < m_points.size());
+        return eval_quad(&m_points[seg.m_ptIndex], t);
+    }
+    else
+    {
+        assert(seg.m_type == SegmentType::kCubic);
+        assert(seg.m_ptIndex + 3 < m_points.size());
+        return eval_cubic(&m_points[seg.m_ptIndex], t);
+    }
+}
+
+static const ContourMeasure::Segment* next_segment_beginning(const ContourMeasure::Segment* seg)
+{
+    auto startingPtIndex = seg->m_ptIndex;
+    do
+    {
+        seg += 1;
+    } while (seg->m_ptIndex == startingPtIndex);
+    return seg;
+}
+
+// Compute the (interpolated) t for a distance within the index'th segment
+static float compute_t(Span<const ContourMeasure::Segment> segs, size_t index, float distance)
+{
+    const auto seg = segs[index];
+    assert(distance <= seg.m_distance);
+
+    float prevDist = 0, prevT = 0;
+    if (index > 0)
+    {
+        const auto prev = segs[index - 1];
+        prevDist = prev.m_distance;
+        if (prev.m_ptIndex == seg.m_ptIndex)
+        {
+            prevT = prev.getT();
+        }
+    }
+
+    assert(prevDist <= seg.m_distance);
+    const auto ratio = (distance - prevDist) / (seg.m_distance - prevDist);
+    float t = lerp(prevT, seg.getT(), ratio);
+    t = math::clamp(t, prevT, seg.getT());
+    assert(prevT <= t && t <= seg.getT());
+    return t;
+}
+
+void ContourMeasure::getSegment(float startDist,
+                                float endDist,
+                                RawPath* dst,
+                                bool startWithMove) const
+{
+    // sanitize the inputs
+    startDist = std::max(0.f, startDist);
+    endDist = std::min(m_length, endDist);
+    if (startDist >= endDist)
+    {
+        return;
+    }
+
+    const auto startIndex = this->findSegment(startDist);
+    const auto endIndex = this->findSegment(endDist);
+
+    const auto start = m_segments[startIndex];
+    const auto end = m_segments[endIndex];
+
+    const auto startT = compute_t(m_segments, startIndex, startDist);
+    const auto endT = compute_t(m_segments, endIndex, endDist);
+
+    if (start.m_ptIndex == end.m_ptIndex)
+    {
+        start.extract(dst, startT, endT, m_points.data(), startWithMove);
+    }
+    else
+    {
+        start.extract(dst, startT, 1, m_points.data(), startWithMove);
+
+        // now scoop up all the segments after start, and before end
+        const auto* seg = next_segment_beginning(&m_segments[startIndex]);
+        while (seg->m_ptIndex != end.m_ptIndex)
+        {
+            seg->extract(dst, m_points.data());
+            seg = next_segment_beginning(seg);
+        }
+        assert(seg->m_ptIndex == end.m_ptIndex);
+
+        end.extract(dst, 0, endT, m_points.data(), false);
+    }
+}
+
+void ContourMeasure::dump() const
+{
+    printf("length %g pts %zu segs %zu\n", m_length, m_points.size(), m_segments.size());
+    for (const auto& s : m_segments)
+    {
+        printf(" %g %d %g %d\n", s.m_distance, s.m_ptIndex, s.getT(), s.m_type);
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////////
+
+constexpr auto kMaxDot30 = ContourMeasure::kMaxDot30;
+
+static inline unsigned toDot30(float x)
+{
+    assert(x >= 0 && x < 1);
+    return (unsigned)(x * (1 << 30));
+}
+
+// These add[SegmentType]Segs routines append intermediate segments for the curve.
+// They assume the caller has set the initial segment (with t == 0), so they only
+// add intermediates.
+
+float ContourMeasureIter::addQuadSegs(ContourMeasure::Segment* segs,
+                                      const Vec2D pts[],
+                                      uint32_t segmentCount,
+                                      uint32_t ptIndex,
+                                      float distance) const
+{
+    const float dt = 1.f / segmentCount;
+    const EvalQuad eval(pts);
+
+    float t = dt;
+    Vec2D prev = pts[0];
+    for (uint32_t i = 1; i < segmentCount; ++i)
+    {
+        auto next = eval(t);
+        distance += (next - prev).length();
+        *segs++ = {distance, ptIndex, toDot30(t), SegmentType::kQuad};
+        prev = next;
+        t += dt;
+    }
+    distance += (pts[2] - prev).length();
+    *segs++ = {distance, ptIndex, kMaxDot30, SegmentType::kQuad};
+    return distance;
+}
+
+float ContourMeasureIter::addCubicSegs(ContourMeasure::Segment* segs,
+                                       const Vec2D pts[],
+                                       uint32_t segmentCount,
+                                       uint32_t ptIndex,
+                                       float distance) const
+{
+    const float dt = 1.f / segmentCount;
+    const EvalCubic eval(pts);
+
+    float t = dt;
+    Vec2D prev = pts[0];
+    for (uint32_t i = 1; i < segmentCount; ++i)
+    {
+        auto next = eval(t);
+        distance += (next - prev).length();
+        *segs++ = {distance, ptIndex, toDot30(t), SegmentType::kCubic};
+        prev = next;
+        t += dt;
+    }
+    distance += (pts[3] - prev).length();
+    *segs++ = {distance, ptIndex, kMaxDot30, SegmentType::kCubic};
+    return distance;
+}
+
+void ContourMeasureIter::rewind(const RawPath* path, float tolerance)
+{
+    m_iter = path->begin();
+    m_end = path->end();
+    m_srcPoints = path->points().data();
+
+    constexpr float kMinTolerance = 1.0f / 16;
+    m_invTolerance = 1.0f / std::max(tolerance, kMinTolerance);
+
+    m_segmentCounts.resize(path->verbs().count());
+}
+
+// Can return null if either it encountered an empty contour (length == 0)
+// or the iterator is exhausted.
+//
+rcp<ContourMeasure> ContourMeasureIter::tryNext()
+{
+    assert(m_iter == m_end || m_iter.verb() == PathVerb::move);
+
+    // Advance to the first line or curve.
+    Vec2D p0;
+    for (;; ++m_iter)
+    {
+        if (m_iter == m_end)
+        {
+            return nullptr;
+        }
+        switch (m_iter.verb())
+        {
+            case PathVerb::move:
+                p0 = m_iter.movePt();
+                RIVE_FALLTHROUGH;
+            case PathVerb::close:
+                continue;
+            default:
+                break;
+        }
+        break;
+    }
+
+    // Pass 1: count segments.
+    uint32_t* nextSegCount = m_segmentCounts.data();
+    size_t segmentCountInCurves = 0;
+    size_t lineCount = 0;
+    bool isClosed = false;
+    RawPath::Iter endOfContour = m_end;
+    for (auto it = m_iter; it != m_end; ++it)
+    {
+        // Arbirtary limit to keep our segmenting tractable.
+        constexpr static uint32_t kMaxSegments = 100;
+        switch (it.verb())
+        {
+            case PathVerb::move:
+                endOfContour = it; // This move belongs to the next contour.
+                break;
+            case PathVerb::line:
+                ++lineCount;
+                continue;
+            case PathVerb::quad:
+            {
+                assert(nextSegCount < m_segmentCounts.data() + m_segmentCounts.size());
+                uint32_t segmentCount = static_cast<uint32_t>(
+                    ceilf(wangs_formula::quadratic(it.quadPts(), m_invTolerance)));
+                segmentCount = std::max(1u, std::min(segmentCount, kMaxSegments));
+                segmentCountInCurves += segmentCount;
+                *nextSegCount++ = segmentCount;
+                continue;
+            }
+            case PathVerb::cubic:
+            {
+                assert(nextSegCount < m_segmentCounts.data() + m_segmentCounts.size());
+                uint32_t segmentCount = static_cast<uint32_t>(
+                    ceilf(ceilf(wangs_formula::cubic(it.cubicPts(), m_invTolerance))));
+                segmentCount = std::max(1u, std::min(segmentCount, kMaxSegments));
+                segmentCountInCurves += segmentCount;
+                *nextSegCount++ = segmentCount;
+                continue;
+            }
+            case PathVerb::close:
+                if (it.ptBeforeClose() != p0)
+                {
+                    ++lineCount;
+                }
+                isClosed = true;
+                continue;
+        }
+        break;
+    }
+
+    // Pass 2: emit segments.
+    std::vector<ContourMeasure::Segment> segs;
+    segs.resize(segmentCountInCurves + lineCount);
+    ContourMeasure::Segment* nextSeg = segs.data();
+    nextSegCount = m_segmentCounts.data();
+    float distance = 0;
+    uint32_t ptIndex = 0;
+    bool duplicateP0 = false;
+    for (auto it = m_iter; it != endOfContour; ++it)
+    {
+        switch (it.verb())
+        {
+            case PathVerb::move:
+                RIVE_UNREACHABLE();
+            case PathVerb::line:
+                distance += (it.linePts()[1] - it.linePts()[0]).length();
+                *nextSeg++ = {distance, ptIndex, kMaxDot30, SegmentType::kLine};
+                ++ptIndex;
+                break;
+            case PathVerb::quad:
+            {
+                const uint32_t n = *nextSegCount++;
+                distance = addQuadSegs(nextSeg, it.quadPts(), n, ptIndex, distance);
+                nextSeg += n;
+                ptIndex += 2;
+                break;
+            }
+            case PathVerb::cubic:
+            {
+                const uint32_t n = *nextSegCount++;
+                distance = addCubicSegs(nextSeg, it.cubicPts(), n, ptIndex, distance);
+                nextSeg += n;
+                ptIndex += 3;
+                break;
+            }
+            case PathVerb::close:
+                if (it.ptBeforeClose() != p0)
+                {
+                    distance += (p0 - it.ptBeforeClose()).length();
+                    *nextSeg++ = {distance, ptIndex, kMaxDot30, SegmentType::kLine};
+                    ++ptIndex;
+                    duplicateP0 = true;
+                }
+                assert(isClosed);
+        }
+    }
+    assert(nextSeg == segs.data() + segs.size());
+
+    // Copy out points.
+    std::vector<Vec2D> pts;
+    pts.reserve(1 + ptIndex);
+    pts.insert(pts.end(), m_iter.rawPtsPtr() - 1, endOfContour.rawPtsPtr());
+    if (duplicateP0)
+    {
+        pts.push_back(p0);
+    }
+    assert(pts.size() == 1 + ptIndex);
+
+    m_iter = endOfContour;
+
+    if (distance > 0 && pts.size() >= 2)
+    {
+        assert(!std::isnan(distance));
+        return rcp<ContourMeasure>(
+            new ContourMeasure(std::move(segs), std::move(pts), distance, isClosed));
+    }
+
+    assert(distance == 0 || std::isnan(distance));
+    return nullptr;
+}
+
+rcp<ContourMeasure> ContourMeasureIter::next()
+{
+    rcp<ContourMeasure> cm;
+    for (;;)
+    {
+        if ((cm = this->tryNext()))
+        {
+            break;
+        }
+        if (m_iter == m_end)
+        {
+            break;
+        }
+    }
+    assert(!cm || !std::isnan(cm->length()));
+    return cm;
+}
diff --git a/src/math/hit_test.cpp b/src/math/hit_test.cpp
new file mode 100644
index 0000000..56fe574
--- /dev/null
+++ b/src/math/hit_test.cpp
@@ -0,0 +1,463 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "rive/math/hit_test.hpp"
+
+#include "rive/math/mat2d.hpp"
+#include <algorithm>
+#include <assert.h>
+#include <cmath>
+
+// Should we make this an option at runtime?
+#define CULL_BOUNDS true
+
+using namespace rive;
+
+static inline float graphics_roundf(float x) { return std::floor(x + 0.5f); }
+
+static inline int graphics_round(float x) { return (int)graphics_roundf(x); }
+
+struct Point
+{
+    float x, y;
+
+    Point() {}
+    Point(float xx, float yy) : x(xx), y(yy) {}
+    Point(const Vec2D& src) : x(src.x), y(src.y) {}
+
+    Point operator+(Point v) const { return {x + v.x, y + v.y}; }
+    Point operator-(Point v) const { return {x - v.x, y - v.y}; }
+
+    Point& operator+=(Point v)
+    {
+        *this = *this + v;
+        return *this;
+    }
+    Point& operator-=(Point v)
+    {
+        *this = *this - v;
+        return *this;
+    }
+
+    friend Point operator*(Point v, float s) { return {v.x * s, v.y * s}; }
+    friend Point operator*(float s, Point v) { return {v.x * s, v.y * s}; }
+};
+
+template <typename T> T lerp(T a, T b, float t) { return a + (b - a) * t; }
+
+template <typename T> T ave(T a, T b) { return lerp(a, b, 0.5f); }
+
+static void append_line(const float height,
+                        Point p0,
+                        Point p1,
+                        float m,
+                        int winding,
+                        int delta[],
+                        int iwidth)
+{
+    assert(winding == 1 || winding == -1);
+
+    int top = graphics_round(p0.y);
+    int bottom = graphics_round(p1.y);
+    if (top == bottom)
+    {
+        return;
+    }
+
+    assert(top < bottom);
+    assert(top >= 0);
+    assert((float)bottom <= height);
+
+    // we add 0.5 at the end to pre-round the values
+    float x = p0.x + m * (top - p0.y + 0.5f) + 0.5f;
+
+    int* row = delta + top * iwidth;
+    for (int y = top; y < bottom; ++y)
+    {
+        int ix = (int)std::max(x, 0.0f);
+        if (ix < iwidth)
+        {
+            row[ix] += winding;
+        }
+        x += m;
+        row += iwidth;
+    }
+}
+
+static void clip_line(const float height, Point p0, Point p1, int delta[], const int iwidth)
+{
+    if (p0.y == p1.y)
+    {
+        return;
+    }
+
+    int winding = 1;
+    if (p0.y > p1.y)
+    {
+        winding = -1;
+        std::swap(p0, p1);
+    }
+    // now we're monotonic in Y: p0 <= p1
+    if (p1.y <= 0 || p0.y >= height)
+    {
+        return;
+    }
+
+    const float m = (float)(p1.x - p0.x) / (p1.y - p0.y);
+    if (p0.y < 0)
+    {
+        p0.x += m * (0 - p0.y);
+        p0.y = 0;
+    }
+    if (p1.y > height)
+    {
+        p1.x += m * (height - p1.y);
+        p1.y = height;
+    }
+
+    assert(p0.y <= p1.y);
+    assert(p0.y >= 0);
+    assert(p1.y <= height);
+
+    append_line(height, p0, p1, m, winding, delta, iwidth);
+}
+
+#define MAX_CURVE_SEGMENTS (1 << 8)
+
+static int compute_cubic_segments(Point a, Point b, Point c, Point d)
+{
+    Point abc = a - b - b + c;
+    Point bcd = b - c - c + d;
+    float dx = std::max(std::abs(abc.x), std::abs(bcd.x));
+    float dy = std::max(std::abs(abc.y), std::abs(bcd.y));
+    float dist = sqrtf(dx * dx + dy * dy);
+    // count = sqrt(6*dist / 8*tol)
+    // tol = 0.25
+    // count = sqrt(3*dist)
+    float count = sqrtf(3 * dist);
+    return std::max(1, std::min((int)ceilf(count), MAX_CURVE_SEGMENTS));
+}
+
+// cubic a(1-t)^3 + 3bt(1-t)^2 + 3c(1-t)t^2 + dt^3
+// becomes
+// At^3 + Bt^2 + Ct + D
+//
+struct CubicCoeff
+{
+    Point A, B, C, D;
+
+    // a(1-t)^3 + 3bt(1-t)^2 + 3ct^2(1-t) + dt^3
+    // a - 3at + 3at^2 -  at^3      a(1 - 3t + 3t^2 - t^3)
+    //     3bt - 6bt^2 + 3bt^3     3b(     t - 2t^2 + t^3)
+    //           3ct^2 - 3ct^3     3c(          t^2 - t^3)
+    //                    dt^3      d(                t^3)
+    // ...
+    // D  + Ct  + Bt^2  + At^3
+    //
+    CubicCoeff(Point a, Point b, Point c, Point d)
+    {
+        A = (d - a) + 3.0f * (b - c);
+        B = 3.0f * ((c - b) + (a - b));
+        C = 3.0f * (b - a);
+        D = a;
+    }
+
+    Point eval(float t) const { return ((A * t + B) * t + C) * t + D; }
+};
+
+////////////////////////////////////////////
+
+void HitTester::reset() { m_DW.clear(); }
+
+void HitTester::reset(const IAABB& clip)
+{
+    m_offset = Vec2D{(float)clip.left, (float)clip.top};
+    m_height = (float)clip.height();
+
+    m_IWidth = clip.width();
+    m_IHeight = clip.height();
+    m_DW.resize(m_IWidth * m_IHeight);
+    for (size_t i = 0; i < m_DW.size(); ++i)
+    {
+        m_DW[i] = 0;
+    }
+
+    m_ExpectsMove = true;
+}
+
+void HitTester::move(Vec2D v)
+{
+    if (!m_ExpectsMove)
+    {
+        this->close();
+    }
+    m_First = m_Prev = v - m_offset;
+    m_ExpectsMove = false;
+}
+
+void HitTester::line(Vec2D v)
+{
+    assert(!m_ExpectsMove);
+
+    v = v - m_offset;
+    clip_line(m_height, m_Prev, v, m_DW.data(), m_IWidth);
+    m_Prev = v;
+}
+
+void HitTester::quad(Vec2D b, Vec2D c)
+{
+    assert(!m_ExpectsMove);
+
+    m_Prev = c;
+}
+
+static bool quickRejectCubic(float height, Point a, Point b, Point c, Point d)
+{
+    const float h = height;
+    return (a.y <= 0 && b.y <= 0 && c.y <= 0 && d.y <= 0) ||
+           (a.y >= h && b.y >= h && c.y >= h && d.y >= h);
+}
+
+struct CubicChop
+{
+    Vec2D storage[7];
+
+    CubicChop(Vec2D a, Vec2D b, Vec2D c, Vec2D d)
+    {
+        auto ab = ave(a, b);
+        auto bc = ave(b, c);
+        auto cd = ave(c, d);
+        auto abc = ave(ab, bc);
+        auto bcd = ave(bc, cd);
+
+        storage[0] = a;
+        storage[1] = ab;
+        storage[2] = abc;
+        storage[3] = ave(abc, bcd);
+        storage[4] = bcd;
+        storage[5] = cd;
+        storage[6] = d;
+    }
+
+    Vec2D operator[](unsigned index) const
+    {
+        assert(index < 7);
+        return storage[index];
+    }
+};
+
+// Trial and error to pick a good value for this.
+//
+// Subdivision and recursion have their own cost, so at some point
+// just evaluating the cubic (count) times is cheaper than continuing
+// to chop.
+//
+// The key win is quickRejectCubic. This is how we save time over just
+// evaluating the cubic up front.
+//
+#define MAX_LOCAL_SEGMENTS 16
+
+void HitTester::recurse_cubic(Vec2D b, Vec2D c, Vec2D d, int count)
+{
+    if (quickRejectCubic(m_height, m_Prev, b, c, d))
+    {
+        m_Prev = d;
+        return;
+    }
+
+    if (count > MAX_LOCAL_SEGMENTS)
+    {
+        CubicChop chop(m_Prev, b, c, d);
+        const int newCount = (count + 1) >> 1;
+        assert(newCount < count);
+        this->recurse_cubic(chop[1], chop[2], chop[3], newCount);
+        this->recurse_cubic(chop[4], chop[5], chop[6], newCount);
+    }
+    else
+    {
+        const float dt = 1.0f / (float)count;
+        float t = dt;
+
+        CubicCoeff cube(m_Prev, b, c, d);
+        // we don't need the first point eval(0) or the last eval(1)
+        Point prev = m_Prev;
+        for (int i = 1; i < count - 1; ++i)
+        {
+            auto next = cube.eval(t);
+            clip_line(m_height, prev, next, m_DW.data(), m_IWidth);
+            prev = next;
+            t += dt;
+        }
+        clip_line(m_height, prev, d, m_DW.data(), m_IWidth);
+        m_Prev = d;
+    }
+}
+void HitTester::cubic(Vec2D b, Vec2D c, Vec2D d)
+{
+    assert(!m_ExpectsMove);
+
+    b = b - m_offset;
+    c = c - m_offset;
+    d = d - m_offset;
+
+    if (quickRejectCubic(m_height, m_Prev, b, c, d))
+    {
+        m_Prev = d;
+        return;
+    }
+
+    const int count = compute_cubic_segments(m_Prev, b, c, d);
+
+    this->recurse_cubic(b, c, d, count);
+}
+
+void HitTester::close()
+{
+    assert(!m_ExpectsMove);
+
+    clip_line(m_height, m_Prev, m_First, m_DW.data(), m_IWidth);
+    m_ExpectsMove = true;
+}
+
+void HitTester::addRect(const AABB& rect, const Mat2D& xform, PathDirection dir)
+{
+    const Vec2D pts[] = {
+        xform * Vec2D{rect.left(), rect.top()},
+        xform * Vec2D{rect.right(), rect.top()},
+        xform * Vec2D{rect.right(), rect.bottom()},
+        xform * Vec2D{rect.left(), rect.bottom()},
+    };
+
+    move(pts[0]);
+    if (dir == PathDirection::clockwise)
+    {
+        line(pts[1]);
+        line(pts[2]);
+        line(pts[3]);
+    }
+    else
+    {
+        line(pts[3]);
+        line(pts[2]);
+        line(pts[1]);
+    }
+    close();
+}
+
+bool HitTester::test(FillRule rule)
+{
+    if (!m_ExpectsMove)
+    {
+        this->close();
+    }
+
+    const int mask = (rule == rive::FillRule::nonZero) ? -1 : 1;
+
+    int nonzero = 0;
+    for (auto m : m_DW)
+    {
+        nonzero |= (m & mask);
+    }
+    return nonzero != 0;
+}
+
+/////////////////////////
+
+static bool cross_lt(Vec2D a, Vec2D b) { return a.x * b.y < a.y * b.x; }
+
+bool HitTester::testMesh(Vec2D pt, Span<Vec2D> verts, Span<uint16_t> indices)
+{
+    if (verts.size() < 3)
+    {
+        return false;
+    }
+
+    // Test against the bounds of the entire mesh
+    // Make this optional?
+    if (CULL_BOUNDS)
+    {
+        const auto bounds = AABB(verts);
+
+        if (bounds.bottom() < pt.y || pt.y < bounds.top() || bounds.right() < pt.x ||
+            pt.x < bounds.left())
+        {
+            return false;
+        }
+    }
+
+    for (size_t i = 0; i < indices.size(); i += 3)
+    {
+        const auto a = verts[indices[i + 0]];
+        const auto b = verts[indices[i + 1]];
+        const auto c = verts[indices[i + 2]];
+
+        auto pa = a - pt;
+        auto pb = b - pt;
+        auto pc = c - pt;
+
+        auto ab = cross_lt(pa, pb);
+        auto bc = cross_lt(pb, pc);
+        auto ca = cross_lt(pc, pa);
+
+        if (ab == bc && ab == ca)
+        {
+            return true;
+        }
+    }
+    return false;
+}
+
+bool HitTester::testMesh(const IAABB& area, Span<Vec2D> verts, Span<uint16_t> indices)
+{
+    // this version can give slightly different results, so perhaps we should do this
+    // automatically, ... its just much faster if we do.
+    if (area.width() * area.height() == 1)
+    {
+        return testMesh(Vec2D((float)area.left, (float)area.top), verts, indices);
+    }
+
+    if (verts.size() < 3)
+    {
+        return false;
+    }
+
+    // Test against the bounds of the entire mesh
+    // Make this optional?
+    if (CULL_BOUNDS)
+    {
+        const auto bounds = AABB(verts);
+
+        if (bounds.bottom() <= area.top || area.bottom <= bounds.top() ||
+            bounds.right() <= area.left || area.right <= bounds.left())
+        {
+            return false;
+        }
+    }
+
+    std::vector<int> windings(area.width() * area.height());
+    const auto offset = Vec2D((float)area.left, (float)area.top);
+    int* deltas = windings.data();
+
+    for (size_t i = 0; i < indices.size(); i += 3)
+    {
+        const auto a = verts[indices[i + 0]] - offset;
+        const auto b = verts[indices[i + 1]] - offset;
+        const auto c = verts[indices[i + 2]] - offset;
+
+        clip_line((float)area.height(), a, b, deltas, area.width());
+        clip_line((float)area.height(), b, c, deltas, area.width());
+        clip_line((float)area.height(), c, a, deltas, area.width());
+
+        int nonzero = 0;
+        for (auto w : windings)
+        {
+            nonzero |= w;
+        }
+        if (nonzero)
+        {
+            return true;
+        }
+    }
+    return false;
+}
diff --git a/src/math/mat2d.cpp b/src/math/mat2d.cpp
new file mode 100644
index 0000000..0980c8f
--- /dev/null
+++ b/src/math/mat2d.cpp
@@ -0,0 +1,231 @@
+#include "rive/math/math_types.hpp"
+#include "rive/math/mat2d.hpp"
+#include "rive/math/simd.hpp"
+#include "rive/math/transform_components.hpp"
+#include "rive/math/vec2d.hpp"
+#include <cmath>
+
+using namespace rive;
+
+Mat2D Mat2D::fromRotation(float rad)
+{
+    float s = 0, c = 1;
+    if (rad != 0)
+    {
+        s = sin(rad);
+        c = cos(rad);
+    }
+    return {c, s, -s, c, 0, 0};
+}
+
+Mat2D Mat2D::scale(Vec2D vec) const
+{
+    return {
+        m_buffer[0] * vec.x,
+        m_buffer[1] * vec.x,
+        m_buffer[2] * vec.y,
+        m_buffer[3] * vec.y,
+        m_buffer[4],
+        m_buffer[5],
+    };
+}
+
+Mat2D Mat2D::multiply(const Mat2D& a, const Mat2D& b)
+{
+    float a0 = a[0], a1 = a[1], a2 = a[2], a3 = a[3], a4 = a[4], a5 = a[5], b0 = b[0], b1 = b[1],
+          b2 = b[2], b3 = b[3], b4 = b[4], b5 = b[5];
+    return {
+        a0 * b0 + a2 * b1,
+        a1 * b0 + a3 * b1,
+        a0 * b2 + a2 * b3,
+        a1 * b2 + a3 * b3,
+        a0 * b4 + a2 * b5 + a4,
+        a1 * b4 + a3 * b5 + a5,
+    };
+}
+
+void Mat2D::mapPoints(Vec2D dst[], const Vec2D pts[], size_t n) const
+{
+    size_t i = 0;
+    float4 scale = float2{m_buffer[0], m_buffer[3]}.xyxy;
+    float4 skew = simd::load2f(&m_buffer[1]).yxyx;
+    float4 trans = simd::load2f(&m_buffer[4]).xyxy;
+    if (simd::all(skew.xy == 0.f))
+    {
+        // Scale + translate matrix.
+        if (n & 1)
+        {
+            float2 p = simd::load2f(pts);
+            p = scale.xy * p + trans.xy;
+            simd::store(dst, p);
+            ++i;
+        }
+        for (; i < n; i += 2)
+        {
+            float4 p = simd::load4f(pts + i);
+            p = scale * p + trans;
+            simd::store(dst + i, p);
+        }
+    }
+    else
+    {
+        // Affine matrix.
+        if (n & 1)
+        {
+            float2 p = simd::load2f(pts);
+            float2 p_ = skew.xy * p.yx + trans.xy;
+            p_ = scale.xy * p + p_;
+            simd::store(dst, p_);
+            ++i;
+        }
+        for (; i < n; i += 2)
+        {
+            float4 p = simd::load4f(pts + i);
+            float4 p_ = skew * p.yxwz + trans;
+            p_ = scale * p + p_;
+            simd::store(dst + i, p_);
+        }
+    }
+}
+
+AABB Mat2D::mapBoundingBox(const Vec2D pts[], size_t n) const
+{
+    size_t i = 0;
+    float4 scale = float2{m_buffer[0], m_buffer[3]}.xyxy;
+    float4 skew = simd::load2f(&m_buffer[1]).yxyx;
+    float4 mins = std::numeric_limits<float>::infinity();
+    float4 maxes = -std::numeric_limits<float>::infinity();
+    if (simd::all(skew.xy == 0.f))
+    {
+        // Scale + translate matrix.
+        if (n & 1)
+        {
+            float2 p = simd::load2f(pts);
+            p = scale.xy * p;
+            mins.xy = maxes.xy = p;
+            ++i;
+        }
+        for (; i < n; i += 2)
+        {
+            float4 p = simd::load4f(pts + i);
+            p = scale * p;
+            mins = simd::min(p, mins);
+            maxes = simd::max(p, maxes);
+        }
+    }
+    else
+    {
+        // Affine matrix.
+        if (n & 1)
+        {
+            float2 p = simd::load2f(pts);
+            float2 p_ = skew.xy * p.yx;
+            p_ = scale.xy * p + p_;
+            mins.xy = maxes.xy = p_;
+            ++i;
+        }
+        for (; i < n; i += 2)
+        {
+            float4 p = simd::load4f(pts + i);
+            float4 p_ = skew * p.yxwz;
+            p_ = scale * p + p_;
+            mins = simd::min(p_, mins);
+            maxes = simd::max(p_, maxes);
+        }
+    }
+
+    float4 bbox = simd::join(simd::min(mins.xy, mins.zw), simd::max(maxes.xy, maxes.zw));
+    // Use logic that takes the "nonfinite" branch when bbox has NaN values.
+    // Use "b - a >= 0" instead of "a >= b" because it fails when b == a == inf.
+    if (!simd::all(bbox.zw - bbox.xy >= 0))
+    {
+        // The given points were NaN or empty, or infinite.
+        bbox = float4(0);
+    }
+    else
+    {
+        float4 trans = simd::load2f(&m_buffer[4]).xyxy;
+        bbox += trans;
+    }
+
+    auto aabb = math::bit_cast<AABB>(bbox);
+    assert(aabb.width() >= 0);
+    assert(aabb.height() >= 0);
+    return aabb;
+}
+
+AABB Mat2D::mapBoundingBox(const AABB& aabb) const
+{
+    Vec2D pts[4] = {{aabb.left(), aabb.top()},
+                    {aabb.right(), aabb.top()},
+                    {aabb.right(), aabb.bottom()},
+                    {aabb.left(), aabb.bottom()}};
+    return mapBoundingBox(pts, 4);
+}
+
+bool Mat2D::invert(Mat2D* result) const
+{
+    float aa = m_buffer[0], ab = m_buffer[1], ac = m_buffer[2], ad = m_buffer[3], atx = m_buffer[4],
+          aty = m_buffer[5];
+
+    float det = aa * ad - ab * ac;
+    if (det == 0.0f)
+    {
+        return false;
+    }
+    det = 1.0f / det;
+
+    *result = {
+        ad * det,
+        -ab * det,
+        -ac * det,
+        aa * det,
+        (ac * aty - ad * atx) * det,
+        (ab * atx - aa * aty) * det,
+    };
+    return true;
+}
+
+TransformComponents Mat2D::decompose() const
+{
+    float m0 = m_buffer[0], m1 = m_buffer[1], m2 = m_buffer[2], m3 = m_buffer[3];
+
+    float rotation = (float)std::atan2(m1, m0);
+    float denom = m0 * m0 + m1 * m1;
+    float scaleX = (float)std::sqrt(denom);
+    float scaleY = scaleX == 0.0f ? 0.0f : (m0 * m3 - m2 * m1) / scaleX;
+    float skewX = (float)std::atan2(m0 * m2 + m1 * m3, denom);
+
+    TransformComponents result;
+    result.x(m_buffer[4]);
+    result.y(m_buffer[5]);
+    result.scaleX(scaleX);
+    result.scaleY(scaleY);
+    result.rotation(rotation);
+    result.skew(skewX);
+    return result;
+}
+
+Mat2D Mat2D::compose(const TransformComponents& components)
+{
+    auto result = Mat2D::fromRotation(components.rotation());
+    result[4] = components.x();
+    result[5] = components.y();
+    result = result.scale(components.scale());
+
+    float sk = components.skew();
+    if (sk != 0.0f)
+    {
+        result[2] = result[0] * sk + result[2];
+        result[3] = result[1] * sk + result[3];
+    }
+    return result;
+}
+
+void Mat2D::scaleByValues(float sx, float sy)
+{
+    m_buffer[0] *= sx;
+    m_buffer[1] *= sx;
+    m_buffer[2] *= sy;
+    m_buffer[3] *= sy;
+}
diff --git a/src/math/mat2d_find_max_scale.cpp b/src/math/mat2d_find_max_scale.cpp
new file mode 100644
index 0000000..9e45c79
--- /dev/null
+++ b/src/math/mat2d_find_max_scale.cpp
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2006 The Android Open Source Project
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ *
+ * Initial import from skia:src/core/SkMatrix.cpp
+ *
+ * Copyright 2022 Rive
+ */
+
+#include "rive/math/mat2d.hpp"
+
+#include "rive/math/math_types.hpp"
+#include <cmath>
+
+static inline float sdot(float a, float b, float c, float d) { return a * b + c * d; }
+
+namespace rive
+{
+float Mat2D::findMaxScale() const
+{
+    if (xy() == 0 && yx() == 0)
+    {
+        return std::max(fabsf(xx()), fabsf(yy()));
+    }
+
+    // ignore the translation part of the matrix, just look at 2x2 portion.
+    // compute singular values, take largest or smallest abs value.
+    // [a b; b c] = A^T*A
+    float a = sdot(xx(), xx(), xy(), xy());
+    float b = sdot(xx(), yx(), yy(), xy());
+    float c = sdot(yx(), yx(), yy(), yy());
+    // eigenvalues of A^T*A are the squared singular values of A.
+    // characteristic equation is det((A^T*A) - l*I) = 0
+    // l^2 - (a + c)l + (ac-b^2)
+    // solve using quadratic equation (divisor is non-zero since l^2 has 1 coeff
+    // and roots are guaranteed to be pos and real).
+    float bSqd = b * b;
+    // if upper left 2x2 is orthogonal save some math
+    float result;
+    if (bSqd <= math::EPSILON * math::EPSILON)
+    {
+        result = std::max(a, c);
+    }
+    else
+    {
+        float aminusc = a - c;
+        float apluscdiv2 = (a + c) * .5f;
+        float x = sqrtf(aminusc * aminusc + 4 * bSqd) * .5f;
+        result = apluscdiv2 + x;
+    }
+    if (!std::isfinite(result))
+    {
+        result = 0;
+    }
+    // Due to the floating point inaccuracy, there might be an error in a, b, c
+    // calculated by sdot, further deepened by subsequent arithmetic operations
+    // on them. Therefore, we allow and cap the nearly-zero negative values.
+    result = std::max(result, 0.f);
+    result = sqrtf(result);
+    return result;
+}
+} // namespace rive
diff --git a/src/math/raw_path.cpp b/src/math/raw_path.cpp
new file mode 100644
index 0000000..29a1d5e
--- /dev/null
+++ b/src/math/raw_path.cpp
@@ -0,0 +1,372 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "rive/math/raw_path.hpp"
+
+#include "rive/command_path.hpp"
+#include "rive/math/simd.hpp"
+#include <cmath>
+#include <cstring>
+#include <algorithm>
+
+namespace rive
+{
+bool RawPath::operator==(const RawPath& o) const
+{
+    return m_Points == o.m_Points && m_Verbs == o.m_Verbs;
+}
+
+AABB RawPath::bounds() const
+{
+    float4 mins, maxes;
+    size_t i;
+    if (m_Points.size() & 1)
+    {
+        mins = maxes = simd::load2f(&m_Points[0].x).xyxy;
+        i = 1;
+    }
+    else
+    {
+        mins = maxes = m_Points.empty() ? float4{0, 0, 0, 0} : simd::load4f(&m_Points[0].x);
+        i = 2;
+    }
+    for (; i < m_Points.size(); i += 2)
+    {
+        float4 pts = simd::load4f(&m_Points[i].x);
+        mins = simd::min(mins, pts);
+        maxes = simd::max(maxes, pts);
+    }
+    AABB bounds;
+    simd::store(&bounds.minX, simd::min(mins.xy, mins.zw));
+    simd::store(&bounds.maxX, simd::max(maxes.xy, maxes.zw));
+    return bounds;
+}
+
+size_t RawPath::countMoveTos() const
+{
+    size_t moveToCount = 0;
+    for (PathVerb verb : m_Verbs)
+    {
+        moveToCount += verb == PathVerb::move ? 1 : 0;
+    }
+    return moveToCount;
+}
+
+void RawPath::injectImplicitMoveIfNeeded()
+{
+    if (!m_contourIsOpen)
+    {
+        move(m_Points.empty() ? Vec2D{0, 0} : m_Points[m_lastMoveIdx]);
+    }
+}
+
+void RawPath::move(Vec2D a)
+{
+    m_contourIsOpen = true;
+    m_lastMoveIdx = m_Points.size();
+    m_Points.push_back(a);
+    m_Verbs.push_back(PathVerb::move);
+}
+
+void RawPath::line(Vec2D a)
+{
+    injectImplicitMoveIfNeeded();
+    m_Points.push_back(a);
+    m_Verbs.push_back(PathVerb::line);
+}
+
+void RawPath::quad(Vec2D a, Vec2D b)
+{
+    injectImplicitMoveIfNeeded();
+    m_Points.push_back(a);
+    m_Points.push_back(b);
+    m_Verbs.push_back(PathVerb::quad);
+}
+
+void RawPath::cubic(Vec2D a, Vec2D b, Vec2D c)
+{
+    injectImplicitMoveIfNeeded();
+    m_Points.push_back(a);
+    m_Points.push_back(b);
+    m_Points.push_back(c);
+    m_Verbs.push_back(PathVerb::cubic);
+}
+
+void RawPath::close()
+{
+    if (m_contourIsOpen)
+    {
+        m_Verbs.push_back(PathVerb::close);
+        m_contourIsOpen = false;
+    }
+}
+
+RawPath RawPath::transform(const Mat2D& m) const
+{
+    RawPath path;
+
+    path.m_Verbs = m_Verbs;
+
+    path.m_Points.resize(m_Points.size());
+    m.mapPoints(path.m_Points.data(), m_Points.data(), m_Points.size());
+    return path;
+}
+
+void RawPath::transformInPlace(const Mat2D& m)
+{
+    m.mapPoints(m_Points.data(), m_Points.data(), m_Points.size());
+}
+
+void RawPath::addRect(const AABB& r, PathDirection dir)
+{
+    // We manually close the rectangle, in case we want to stroke
+    // this path. We also call close() so we get proper joins
+    // (and not caps).
+
+    m_Points.reserve(5);
+    m_Verbs.reserve(6);
+
+    moveTo(r.left(), r.top());
+    if (dir == PathDirection::clockwise)
+    {
+        lineTo(r.right(), r.top());
+        lineTo(r.right(), r.bottom());
+        lineTo(r.left(), r.bottom());
+    }
+    else
+    {
+        lineTo(r.left(), r.bottom());
+        lineTo(r.right(), r.bottom());
+        lineTo(r.right(), r.top());
+    }
+    close();
+}
+
+void RawPath::addOval(const AABB& r, PathDirection dir)
+{
+    // see https://spencermortensen.com/articles/bezier-circle/
+    constexpr float C = 0.5519150244935105707435627f;
+    // precompute clockwise unit circle, starting and ending at {1, 0}
+    constexpr rive::Vec2D unit[] = {
+        {1, 0},
+        {1, C},
+        {C, 1}, // quadrant 1 ( 4:30)
+        {0, 1},
+        {-C, 1},
+        {-1, C}, // quadrant 2 ( 7:30)
+        {-1, 0},
+        {-1, -C},
+        {-C, -1}, // quadrant 3 (10:30)
+        {0, -1},
+        {C, -1},
+        {1, -C}, // quadrant 4 ( 1:30)
+        {1, 0},
+    };
+
+    const auto center = r.center();
+    const float dx = center.x;
+    const float dy = center.y;
+    const float sx = r.width() * 0.5f;
+    const float sy = r.height() * 0.5f;
+
+    auto map = [dx, dy, sx, sy](rive::Vec2D p) {
+        return rive::Vec2D(p.x * sx + dx, p.y * sy + dy);
+    };
+
+    m_Points.reserve(13);
+    m_Verbs.reserve(6);
+
+    if (dir == PathDirection::clockwise)
+    {
+        move(map(unit[0]));
+        for (int i = 1; i <= 12; i += 3)
+        {
+            cubic(map(unit[i + 0]), map(unit[i + 1]), map(unit[i + 2]));
+        }
+    }
+    else
+    {
+        move(map(unit[12]));
+        for (int i = 11; i >= 0; i -= 3)
+        {
+            cubic(map(unit[i - 0]), map(unit[i - 1]), map(unit[i - 2]));
+        }
+    }
+    close();
+}
+
+void RawPath::addPoly(Span<const Vec2D> span, bool isClosed)
+{
+    if (span.size() == 0)
+    {
+        return;
+    }
+
+    // should we permit must moveTo() or just moveTo()/close() ?
+
+    m_Points.reserve(span.size() + isClosed);
+    m_Verbs.reserve(span.size() + isClosed);
+
+    move(span[0]);
+    for (size_t i = 1; i < span.size(); ++i)
+    {
+        line(span[i]);
+    }
+    if (isClosed)
+    {
+        close();
+    }
+}
+
+RawPath::Iter RawPath::addPath(const RawPath& src, const Mat2D* mat)
+{
+    size_t initialVerbCount = m_Verbs.size();
+    size_t initialPointCount = m_Points.size();
+
+    m_Verbs.insert(m_Verbs.end(), src.m_Verbs.cbegin(), src.m_Verbs.cend());
+
+    if (mat)
+    {
+        const auto oldPointCount = m_Points.size();
+        m_Points.resize(oldPointCount + src.m_Points.size());
+        Vec2D* dst = m_Points.data() + oldPointCount;
+        mat->mapPoints(dst, src.m_Points.data(), src.m_Points.size());
+    }
+    else
+    {
+        m_Points.insert(m_Points.end(), src.m_Points.cbegin(), src.m_Points.cend());
+    }
+
+    // Return an iterator at the beginning of the newly added geometry.
+    return Iter{m_Verbs.data() + initialVerbCount, m_Points.data() + initialPointCount};
+}
+
+void RawPath::pruneEmptySegments(Iter start)
+{
+    auto dstVerb =
+        m_Verbs.begin() + std::distance<const PathVerb*>(m_Verbs.data(), start.rawVerbsPtr());
+    auto dstPts =
+        m_Points.begin() + std::distance<const Vec2D*>(m_Points.data(), start.rawPtsPtr());
+    decltype(m_Verbs)::const_iterator srcVerb = dstVerb;
+    decltype(m_Points)::const_iterator srcPts = dstPts;
+
+    int ptsAdvance;
+    for (auto end = m_Verbs.end(); srcVerb != end; ++srcVerb, srcPts += ptsAdvance)
+    {
+        PathVerb verb = *srcVerb;
+        ptsAdvance = Iter::PtsAdvanceAfterVerb(verb);
+
+        switch (verb)
+        {
+            case PathVerb::move:
+                break;
+            case PathVerb::close:
+                break;
+            case PathVerb::cubic:
+                if (srcPts[2] != srcPts[1])
+                {
+                    break;
+                }
+                RIVE_FALLTHROUGH;
+            case PathVerb::quad:
+                if (srcPts[1] != srcPts[0])
+                {
+                    break;
+                }
+                RIVE_FALLTHROUGH;
+            case PathVerb::line:
+                if (srcPts[0] != srcPts[-1])
+                {
+                    break;
+                }
+                // This segment is empty! Don't keep it.
+                continue;
+        }
+
+        if (srcVerb != dstVerb)
+        {
+            *dstVerb = verb;
+            std::copy(srcPts, srcPts + ptsAdvance, dstPts);
+        }
+
+        ++dstVerb;
+        dstPts += ptsAdvance;
+    }
+
+    if (dstVerb != srcVerb)
+    {
+        m_Verbs.erase(dstVerb, m_Verbs.end());
+        m_Points.erase(dstPts, m_Points.end());
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////
+int path_verb_to_point_count(PathVerb v)
+{
+    static uint8_t ptCounts[] = {
+        1, // move
+        1, // line
+        2, // quad
+        2, // conic (unused)
+        3, // cubic
+        0, // close
+    };
+    size_t index = (size_t)v;
+    assert(index < sizeof(ptCounts));
+    return ptCounts[index];
+}
+
+void RawPath::swap(RawPath& rawPath)
+{
+    m_Points.swap(rawPath.m_Points);
+    m_Verbs.swap(rawPath.m_Verbs);
+}
+
+void RawPath::reset()
+{
+    m_Points.clear();
+    m_Points.shrink_to_fit();
+    m_Verbs.clear();
+    m_Verbs.shrink_to_fit();
+    m_contourIsOpen = false;
+}
+
+void RawPath::rewind()
+{
+    m_Points.clear();
+    m_Verbs.clear();
+    m_contourIsOpen = false;
+}
+
+///////////////////////////////////
+
+void RawPath::addTo(CommandPath* result) const
+{
+    for (auto iter : *this)
+    {
+        PathVerb verb = std::get<0>(iter);
+        const Vec2D* pts = std::get<1>(iter);
+        switch (verb)
+        {
+            case PathVerb::move:
+                result->move(pts[0]);
+                break;
+            case PathVerb::line:
+                result->line(pts[1]);
+                break;
+            case PathVerb::cubic:
+                result->cubic(pts[1], pts[2], pts[3]);
+                break;
+            case PathVerb::close:
+                result->close();
+                break;
+            case PathVerb::quad:
+                // TODO: actually supports quads.
+                result->cubic(Vec2D::lerp(pts[0], pts[1], 2 / 3.f),
+                              Vec2D::lerp(pts[2], pts[1], 2 / 3.f),
+                              pts[2]);
+        }
+    }
+}
+} // namespace rive
diff --git a/src/math/raw_path_utils.cpp b/src/math/raw_path_utils.cpp
new file mode 100644
index 0000000..32bd268
--- /dev/null
+++ b/src/math/raw_path_utils.cpp
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "rive/math/math_types.hpp"
+#include "rive/math/raw_path_utils.hpp"
+#include <cmath>
+
+// Extract subsets
+
+void rive::quad_subdivide(const rive::Vec2D src[3], float t, rive::Vec2D dst[5])
+{
+    assert(t >= 0 && t <= 1);
+    auto ab = lerp(src[0], src[1], t);
+    auto bc = lerp(src[1], src[2], t);
+    dst[0] = src[0];
+    dst[1] = ab;
+    dst[2] = lerp(ab, bc, t);
+    dst[3] = bc;
+    dst[4] = src[2];
+}
+
+void rive::cubic_subdivide(const rive::Vec2D src[4], float t, rive::Vec2D dst[7])
+{
+    assert(t >= 0 && t <= 1);
+    auto ab = lerp(src[0], src[1], t);
+    auto bc = lerp(src[1], src[2], t);
+    auto cd = lerp(src[2], src[3], t);
+    auto abc = lerp(ab, bc, t);
+    auto bcd = lerp(bc, cd, t);
+    dst[0] = src[0];
+    dst[1] = ab;
+    dst[2] = abc;
+    dst[3] = lerp(abc, bcd, t);
+    dst[4] = bcd;
+    dst[5] = cd;
+    dst[6] = src[3];
+}
+
+void rive::line_extract(const rive::Vec2D src[2], float startT, float endT, rive::Vec2D dst[2])
+{
+    assert(startT <= endT);
+    assert(startT >= 0 && endT <= 1);
+
+    dst[0] = lerp(src[0], src[1], startT);
+    dst[1] = lerp(src[0], src[1], endT);
+}
+
+void rive::quad_extract(const rive::Vec2D src[3], float startT, float endT, rive::Vec2D dst[3])
+{
+    assert(startT <= endT);
+    assert(startT >= 0 && endT <= 1);
+
+    rive::Vec2D tmp[5];
+    if (startT == 0 && endT == 1)
+    {
+        std::copy(src, src + 3, dst);
+    }
+    else if (startT == 0)
+    {
+        rive::quad_subdivide(src, endT, tmp);
+        std::copy(tmp, tmp + 3, dst);
+    }
+    else if (endT == 1)
+    {
+        rive::quad_subdivide(src, startT, tmp);
+        std::copy(tmp + 2, tmp + 5, dst);
+    }
+    else
+    {
+        assert(endT > 0);
+        rive::quad_subdivide(src, endT, tmp);
+        rive::Vec2D tmp2[5];
+        rive::quad_subdivide(tmp, startT / endT, tmp2);
+        std::copy(tmp2 + 2, tmp2 + 5, dst);
+    }
+}
+
+void rive::cubic_extract(const rive::Vec2D src[4], float startT, float endT, rive::Vec2D dst[4])
+{
+    assert(startT <= endT);
+    assert(startT >= 0 && endT <= 1);
+
+    rive::Vec2D tmp[7];
+    if (startT == 0 && endT == 1)
+    {
+        std::copy(src, src + 4, dst);
+    }
+    else if (startT == 0)
+    {
+        rive::cubic_subdivide(src, endT, tmp);
+        std::copy(tmp, tmp + 4, dst);
+    }
+    else if (endT == 1)
+    {
+        rive::cubic_subdivide(src, startT, tmp);
+        std::copy(tmp + 3, tmp + 7, dst);
+    }
+    else
+    {
+        assert(endT > 0);
+        rive::cubic_subdivide(src, endT, tmp);
+        rive::Vec2D tmp2[7];
+        rive::cubic_subdivide(tmp, startT / endT, tmp2);
+        std::copy(tmp2 + 3, tmp2 + 7, dst);
+    }
+}
diff --git a/src/math/vec2d.cpp b/src/math/vec2d.cpp
new file mode 100644
index 0000000..e0b8233
--- /dev/null
+++ b/src/math/vec2d.cpp
@@ -0,0 +1,25 @@
+#include "rive/math/vec2d.hpp"
+#include "rive/math/mat2d.hpp"
+#include <cmath>
+
+using namespace rive;
+
+Vec2D Vec2D::transformDir(const Vec2D& a, const Mat2D& m)
+{
+    return {
+        m[0] * a.x + m[2] * a.y,
+        m[1] * a.x + m[3] * a.y,
+    };
+}
+Vec2D Vec2D::transformMat2D(const Vec2D& a, const Mat2D& m)
+{
+    return {m[0] * a.x + m[2] * a.y + m[4], m[1] * a.x + m[3] * a.y + m[5]};
+}
+float Vec2D::length() const { return std::sqrt(lengthSquared()); }
+
+Vec2D Vec2D::normalized() const
+{
+    auto len2 = lengthSquared();
+    auto scale = len2 > 0 ? (1 / std::sqrt(len2)) : 1;
+    return *this * scale;
+}
diff --git a/src/nested_artboard.cpp b/src/nested_artboard.cpp
new file mode 100644
index 0000000..6f30c4c
--- /dev/null
+++ b/src/nested_artboard.cpp
@@ -0,0 +1,302 @@
+#include "rive/nested_artboard.hpp"
+#include "rive/artboard.hpp"
+#include "rive/backboard.hpp"
+#include "rive/importers/import_stack.hpp"
+#include "rive/importers/backboard_importer.hpp"
+#include "rive/nested_animation.hpp"
+#include "rive/animation/nested_state_machine.hpp"
+#include "rive/clip_result.hpp"
+#include <limits>
+#include <cassert>
+
+using namespace rive;
+
+NestedArtboard::NestedArtboard() {}
+NestedArtboard::~NestedArtboard() {}
+
+Core* NestedArtboard::clone() const
+{
+    NestedArtboard* nestedArtboard = static_cast<NestedArtboard*>(NestedArtboardBase::clone());
+    if (m_Artboard == nullptr)
+    {
+        return nestedArtboard;
+    }
+    auto ni = m_Artboard->instance();
+    nestedArtboard->nest(ni.release());
+    return nestedArtboard;
+}
+
+void NestedArtboard::nest(Artboard* artboard)
+{
+    assert(artboard != nullptr);
+
+    m_Artboard = artboard;
+    if (!m_Artboard->isInstance())
+    {
+        // We're just marking the source artboard so we can later instance from
+        // it. No need to advance it or change any of its properties.
+        return;
+    }
+    m_Artboard->frameOrigin(false);
+    m_Artboard->opacity(renderOpacity());
+    m_Artboard->volume(artboard->volume());
+    m_Instance = nullptr;
+    if (artboard->isInstance())
+    {
+        m_Instance.reset(static_cast<ArtboardInstance*>(artboard)); // take ownership
+    }
+    // This allows for swapping after initial load (after onAddedClean has
+    // already been called).
+    m_Artboard->host(this);
+}
+
+static Mat2D makeTranslate(const Artboard* artboard)
+{
+    return Mat2D::fromTranslate(-artboard->originX() * artboard->width(),
+                                -artboard->originY() * artboard->height());
+}
+
+void NestedArtboard::draw(Renderer* renderer)
+{
+    if (m_Artboard == nullptr)
+    {
+        return;
+    }
+    ClipResult clipResult = applyClip(renderer);
+    if (clipResult == ClipResult::noClip)
+    {
+        // We didn't clip, so make sure to save as we'll be doing some
+        // transformations.
+        renderer->save();
+    }
+    if (clipResult != ClipResult::emptyClip)
+    {
+        renderer->transform(worldTransform());
+        m_Artboard->draw(renderer);
+    }
+    renderer->restore();
+}
+
+Core* NestedArtboard::hitTest(HitInfo* hinfo, const Mat2D& xform)
+{
+    if (m_Artboard == nullptr)
+    {
+        return nullptr;
+    }
+    hinfo->mounts.push_back(this);
+    auto mx = xform * worldTransform() * makeTranslate(m_Artboard);
+    if (auto c = m_Artboard->hitTest(hinfo, mx))
+    {
+        return c;
+    }
+    hinfo->mounts.pop_back();
+    return nullptr;
+}
+
+StatusCode NestedArtboard::import(ImportStack& importStack)
+{
+    auto backboardImporter = importStack.latest<BackboardImporter>(Backboard::typeKey);
+    if (backboardImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    backboardImporter->addNestedArtboard(this);
+
+    return Super::import(importStack);
+}
+
+void NestedArtboard::addNestedAnimation(NestedAnimation* nestedAnimation)
+{
+    m_NestedAnimations.push_back(nestedAnimation);
+}
+
+StatusCode NestedArtboard::onAddedClean(CoreContext* context)
+{
+    // N.B. The nested instance will be null here for the source artboards.
+    // Instances will have a nestedInstance available. This is a good thing as
+    // it ensures that we only instance animations in artboard instances. It
+    // does require that we always use an artboard instance (not just the source
+    // artboard) when working with nested artboards, but in general this is good
+    // practice for any loaded Rive file.
+    assert(m_Artboard == nullptr || m_Artboard == m_Instance.get());
+
+    if (m_Instance)
+    {
+        for (auto animation : m_NestedAnimations)
+        {
+            animation->initializeAnimation(m_Instance.get());
+        }
+        m_Artboard->host(this);
+    }
+    return Super::onAddedClean(context);
+}
+
+bool NestedArtboard::advance(float elapsedSeconds)
+{
+    bool keepGoing = false;
+    if (m_Artboard == nullptr || isCollapsed())
+    {
+        return keepGoing;
+    }
+    for (auto animation : m_NestedAnimations)
+    {
+        keepGoing = animation->advance(elapsedSeconds) || keepGoing;
+    }
+    return m_Artboard->advanceInternal(elapsedSeconds, false) || keepGoing;
+}
+
+void NestedArtboard::update(ComponentDirt value)
+{
+    Super::update(value);
+    if (hasDirt(value, ComponentDirt::RenderOpacity) && m_Artboard != nullptr)
+    {
+        m_Artboard->opacity(renderOpacity());
+    }
+}
+
+bool NestedArtboard::hasNestedStateMachines() const
+{
+    for (auto animation : m_NestedAnimations)
+    {
+        if (animation->is<NestedStateMachine>())
+        {
+            return true;
+        }
+    }
+    return false;
+}
+
+Span<NestedAnimation*> NestedArtboard::nestedAnimations() { return m_NestedAnimations; }
+
+NestedArtboard* NestedArtboard::nestedArtboard(std::string name) const
+{
+    if (m_Instance != nullptr)
+    {
+        return m_Instance->nestedArtboard(name);
+    }
+    return nullptr;
+}
+
+NestedStateMachine* NestedArtboard::stateMachine(std::string name) const
+{
+    for (auto animation : m_NestedAnimations)
+    {
+        if (animation->is<NestedStateMachine>() && animation->name() == name)
+        {
+            return animation->as<NestedStateMachine>();
+        }
+    }
+    return nullptr;
+}
+
+NestedInput* NestedArtboard::input(std::string name) const { return input(name, ""); }
+
+NestedInput* NestedArtboard::input(std::string name, std::string stateMachineName) const
+{
+    if (!stateMachineName.empty())
+    {
+        auto nestedSM = stateMachine(stateMachineName);
+        if (nestedSM != nullptr)
+        {
+            return nestedSM->input(name);
+        }
+    }
+    else
+    {
+        for (auto animation : m_NestedAnimations)
+        {
+            if (animation->is<NestedStateMachine>())
+            {
+                auto input = animation->as<NestedStateMachine>()->input(name);
+                if (input != nullptr)
+                {
+                    return input;
+                }
+            }
+        }
+    }
+    return nullptr;
+}
+
+bool NestedArtboard::worldToLocal(Vec2D world, Vec2D* local)
+{
+    assert(local != nullptr);
+    if (m_Artboard == nullptr)
+    {
+        return false;
+    }
+    Mat2D toMountedArtboard;
+    if (!worldTransform().invert(&toMountedArtboard))
+    {
+        return false;
+    }
+
+    *local = toMountedArtboard * world;
+
+    return true;
+}
+
+Vec2D NestedArtboard::measureLayout(float width,
+                                    LayoutMeasureMode widthMode,
+                                    float height,
+                                    LayoutMeasureMode heightMode)
+{
+    return Vec2D(
+        std::min(widthMode == LayoutMeasureMode::undefined ? std::numeric_limits<float>::max()
+                                                           : width,
+                 m_Instance ? m_Instance->width() : 0.0f),
+        std::min(heightMode == LayoutMeasureMode::undefined ? std::numeric_limits<float>::max()
+                                                            : height,
+                 m_Instance ? m_Instance->height() : 0.0f));
+}
+
+void NestedArtboard::syncStyleChanges()
+{
+    if (m_Artboard == nullptr)
+    {
+        return;
+    }
+    m_Artboard->syncStyleChanges();
+}
+
+void NestedArtboard::controlSize(Vec2D size) {}
+
+void NestedArtboard::decodeDataBindPathIds(Span<const uint8_t> value)
+{
+    BinaryReader reader(value);
+    while (!reader.reachedEnd())
+    {
+        auto val = reader.readVarUintAs<uint32_t>();
+        m_DataBindPathIdsBuffer.push_back(val);
+    }
+}
+
+void NestedArtboard::copyDataBindPathIds(const NestedArtboardBase& object)
+{
+    m_DataBindPathIdsBuffer = object.as<NestedArtboard>()->m_DataBindPathIdsBuffer;
+}
+
+void NestedArtboard::internalDataContext(DataContext* value, DataContext* parent)
+{
+    artboardInstance()->internalDataContext(value, parent, false);
+    for (auto animation : m_NestedAnimations)
+    {
+        if (animation->is<NestedStateMachine>())
+        {
+            animation->as<NestedStateMachine>()->dataContext(value);
+        }
+    }
+}
+
+void NestedArtboard::dataContextFromInstance(ViewModelInstance* viewModelInstance,
+                                             DataContext* parent)
+{
+    artboardInstance()->dataContextFromInstance(viewModelInstance, parent, false);
+    for (auto animation : m_NestedAnimations)
+    {
+        if (animation->is<NestedStateMachine>())
+        {
+            animation->as<NestedStateMachine>()->dataContextFromInstance(viewModelInstance);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/nested_artboard_layout.cpp b/src/nested_artboard_layout.cpp
new file mode 100644
index 0000000..501ffd6
--- /dev/null
+++ b/src/nested_artboard_layout.cpp
@@ -0,0 +1,138 @@
+#include "rive/nested_artboard_layout.hpp"
+#include "rive/artboard.hpp"
+
+using namespace rive;
+
+Core* NestedArtboardLayout::clone() const
+{
+    NestedArtboardLayout* nestedArtboard =
+        static_cast<NestedArtboardLayout*>(NestedArtboardLayoutBase::clone());
+    if (m_Artboard == nullptr)
+    {
+        return nestedArtboard;
+    }
+    auto ni = m_Artboard->instance();
+    nestedArtboard->nest(ni.release());
+    return nestedArtboard;
+}
+
+float NestedArtboardLayout::actualInstanceWidth()
+{
+    return instanceWidth() == -1.0f ? artboardInstance()->originalWidth() : instanceWidth();
+}
+
+float NestedArtboardLayout::actualInstanceHeight()
+{
+    return instanceHeight() == -1.0f ? artboardInstance()->originalHeight() : instanceHeight();
+}
+
+#ifdef WITH_RIVE_LAYOUT
+void* NestedArtboardLayout::layoutNode()
+{
+    if (artboardInstance() == nullptr)
+    {
+        return nullptr;
+    }
+    return artboardInstance()->takeLayoutNode();
+}
+#endif
+
+void NestedArtboardLayout::markNestedLayoutDirty()
+{
+    if (artboardInstance() != nullptr)
+    {
+        artboardInstance()->markLayoutNodeDirty();
+    }
+}
+
+void NestedArtboardLayout::update(ComponentDirt value)
+{
+    Super::update(value);
+    auto artboard = artboardInstance();
+    if (hasDirt(value, ComponentDirt::WorldTransform) && artboard != nullptr)
+    {
+        auto layoutPosition = Vec2D(artboard->layoutX(), artboard->layoutY());
+
+        if (parent()->is<Artboard>())
+        {
+            auto parentArtboard = parent()->as<Artboard>();
+            auto correctedArtboardSpace =
+                Mat2D::fromTranslation(parentArtboard->origin() + layoutPosition);
+            m_WorldTransform = correctedArtboardSpace * m_WorldTransform;
+        }
+        else
+        {
+            m_WorldTransform = Mat2D::fromTranslation(layoutPosition) * m_WorldTransform;
+        }
+        auto back = Mat2D::fromTranslation(-artboard->origin());
+        m_WorldTransform = back * m_WorldTransform;
+    }
+}
+
+StatusCode NestedArtboardLayout::onAddedClean(CoreContext* context)
+{
+    StatusCode code = Super::onAddedClean(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+
+    updateWidthOverride();
+    updateHeightOverride();
+
+    return StatusCode::Ok;
+}
+
+void NestedArtboardLayout::updateWidthOverride()
+{
+    if (artboardInstance() == nullptr)
+    {
+        return;
+    }
+    auto isRow =
+        parent()->is<LayoutComponent>() ? parent()->as<LayoutComponent>()->mainAxisIsRow() : true;
+    if (instanceWidthScaleType() == 0) // LayoutScaleType::fixed
+    {
+        // If we're set to fixed, pass the unit value (points|percent)
+        artboardInstance()->widthOverride(actualInstanceWidth(), instanceWidthUnitsValue(), isRow);
+    }
+    else if (instanceWidthScaleType() == 1) // LayoutScaleType::fill
+    {
+        // If we're set to fill, pass auto
+        artboardInstance()->widthOverride(actualInstanceWidth(), 3, isRow);
+    }
+}
+
+void NestedArtboardLayout::updateHeightOverride()
+{
+    if (artboardInstance() == nullptr)
+    {
+        return;
+    }
+    auto isRow =
+        parent()->is<LayoutComponent>() ? parent()->as<LayoutComponent>()->mainAxisIsRow() : true;
+    if (instanceHeightScaleType() == 0) // LayoutScaleType::fixed
+    {
+        // If we're set to fixed, pass the unit value (points|percent)
+        artboardInstance()->heightOverride(actualInstanceHeight(),
+                                           instanceHeightUnitsValue(),
+                                           isRow);
+    }
+    else if (instanceHeightScaleType() == 1) // LayoutScaleType::fill
+    {
+        // If we're set to fill, pass auto
+        artboardInstance()->heightOverride(actualInstanceHeight(), 3, isRow);
+    }
+}
+
+void NestedArtboardLayout::instanceWidthChanged() { updateWidthOverride(); }
+
+void NestedArtboardLayout::instanceHeightChanged() { updateHeightOverride(); }
+
+void NestedArtboardLayout::instanceWidthUnitsValueChanged() { updateWidthOverride(); }
+
+void NestedArtboardLayout::instanceHeightUnitsValueChanged() { updateHeightOverride(); }
+
+void NestedArtboardLayout::instanceWidthScaleTypeChanged() { updateWidthOverride(); }
+
+void NestedArtboardLayout::instanceHeightScaleTypeChanged() { updateHeightOverride(); }
\ No newline at end of file
diff --git a/src/nested_artboard_leaf.cpp b/src/nested_artboard_leaf.cpp
new file mode 100644
index 0000000..2ae92b6
--- /dev/null
+++ b/src/nested_artboard_leaf.cpp
@@ -0,0 +1,40 @@
+#include "rive/nested_artboard_leaf.hpp"
+#include "rive/renderer.hpp"
+#include "rive/layout_component.hpp"
+#include "rive/artboard.hpp"
+
+using namespace rive;
+
+Core* NestedArtboardLeaf::clone() const
+{
+    NestedArtboardLeaf* nestedArtboard =
+        static_cast<NestedArtboardLeaf*>(NestedArtboardLeafBase::clone());
+    if (m_Artboard == nullptr)
+    {
+        return nestedArtboard;
+    }
+    auto ni = m_Artboard->instance();
+    nestedArtboard->nest(ni.release());
+    return nestedArtboard;
+}
+
+void NestedArtboardLeaf::update(ComponentDirt value)
+{
+    Super::update(value);
+    auto artboard = artboardInstance();
+    if (hasDirt(value, ComponentDirt::WorldTransform) && artboard != nullptr)
+    {
+        auto p = parent();
+
+        AABB bounds = p != nullptr && p->is<LayoutComponent>()
+                          ? p->as<LayoutComponent>()->localBounds()
+                          : AABB();
+
+        auto viewTransform = computeAlignment((Fit)fit(),
+                                              Alignment(alignmentX(), alignmentY()),
+                                              bounds,
+                                              artboard->bounds());
+
+        m_WorldTransform *= viewTransform;
+    }
+}
diff --git a/src/node.cpp b/src/node.cpp
new file mode 100644
index 0000000..7cc4664
--- /dev/null
+++ b/src/node.cpp
@@ -0,0 +1,6 @@
+#include "rive/node.hpp"
+
+using namespace rive;
+
+void Node::xChanged() { markTransformDirty(); }
+void Node::yChanged() { markTransformDirty(); }
\ No newline at end of file
diff --git a/src/renderer.cpp b/src/renderer.cpp
new file mode 100644
index 0000000..0d34b8c
--- /dev/null
+++ b/src/renderer.cpp
@@ -0,0 +1,192 @@
+#include "rive/math/mat2d.hpp"
+#include "rive/renderer.hpp"
+#include "rive/text_engine.hpp"
+
+using namespace rive;
+
+Mat2D rive::computeAlignment(Fit fit, Alignment alignment, const AABB& frame, const AABB& content)
+{
+    float contentWidth = content.width();
+    float contentHeight = content.height();
+    float x = -content.left() - contentWidth * 0.5f - (alignment.x() * contentWidth * 0.5f);
+    float y = -content.top() - contentHeight * 0.5f - (alignment.y() * contentHeight * 0.5f);
+
+    float scaleX = 1.0f, scaleY = 1.0f;
+
+    switch (fit)
+    {
+        case Fit::fill:
+        {
+            scaleX = frame.width() / contentWidth;
+            scaleY = frame.height() / contentHeight;
+            break;
+        }
+        case Fit::contain:
+        {
+            float minScale =
+                std::fmin(frame.width() / contentWidth, frame.height() / contentHeight);
+            scaleX = scaleY = minScale;
+            break;
+        }
+        case Fit::cover:
+        {
+            float maxScale =
+                std::fmax(frame.width() / contentWidth, frame.height() / contentHeight);
+            scaleX = scaleY = maxScale;
+            break;
+        }
+        case Fit::fitHeight:
+        {
+            float minScale = frame.height() / contentHeight;
+            scaleX = scaleY = minScale;
+            break;
+        }
+        case Fit::fitWidth:
+        {
+            float minScale = frame.width() / contentWidth;
+            scaleX = scaleY = minScale;
+            break;
+        }
+        case Fit::none:
+        {
+            scaleX = scaleY = 1.0f;
+            break;
+        }
+        case Fit::scaleDown:
+        {
+            float minScale =
+                std::fmin(frame.width() / contentWidth, frame.height() / contentHeight);
+            scaleX = scaleY = minScale < 1.0f ? minScale : 1.0f;
+            break;
+        }
+    }
+
+    Mat2D translation;
+    translation[4] = frame.left() + frame.width() * 0.5f + (alignment.x() * frame.width() * 0.5f);
+    translation[5] = frame.top() + frame.height() * 0.5f + (alignment.y() * frame.height() * 0.5f);
+
+    return translation * Mat2D::fromScale(scaleX, scaleY) * Mat2D::fromTranslate(x, y);
+}
+
+void Renderer::translate(float tx, float ty) { this->transform(Mat2D(1, 0, 0, 1, tx, ty)); }
+
+void Renderer::scale(float sx, float sy) { this->transform(Mat2D(sx, 0, 0, sy, 0, 0)); }
+
+void Renderer::rotate(float radians)
+{
+    const float s = std::sin(radians);
+    const float c = std::cos(radians);
+    this->transform(Mat2D(c, s, -s, c, 0, 0));
+}
+
+RenderBuffer::RenderBuffer(RenderBufferType type, RenderBufferFlags flags, size_t sizeInBytes) :
+    m_type(type), m_flags(flags), m_sizeInBytes(sizeInBytes)
+{}
+
+RenderBuffer::~RenderBuffer() {}
+
+void* RenderBuffer::map()
+{
+    assert(m_mapCount == 0 || !(m_flags & RenderBufferFlags::mappedOnceAtInitialization));
+    assert(m_mapCount == m_unmapCount);
+    RIVE_DEBUG_CODE(++m_mapCount;)
+    return onMap();
+}
+
+void RenderBuffer::unmap()
+{
+    assert(m_unmapCount + 1 == m_mapCount);
+    RIVE_DEBUG_CODE(++m_unmapCount;)
+    onUnmap();
+}
+
+RenderShader::RenderShader() {}
+RenderShader::~RenderShader() {}
+
+RenderPaint::RenderPaint() {}
+RenderPaint::~RenderPaint() {}
+
+RenderImage::RenderImage(const Mat2D& uvTransform) : m_uvTransform(uvTransform) {}
+RenderImage::RenderImage() {}
+RenderImage::~RenderImage() {}
+
+RenderPath::RenderPath() {}
+RenderPath::~RenderPath() {}
+
+bool rive::isWhiteSpace(Unichar c) { return c <= ' ' || c == 0x2028; }
+
+SimpleArray<Paragraph> Font::shapeText(Span<const Unichar> text, Span<const TextRun> runs) const
+{
+#ifdef DEBUG
+    size_t count = 0;
+    for (const TextRun& tr : runs)
+    {
+        assert(tr.unicharCount > 0);
+        count += tr.unicharCount;
+    }
+    assert(count <= text.size());
+#endif
+
+    SimpleArray<Paragraph> paragraphs = onShapeText(text, runs);
+    bool wantWhiteSpace = false;
+    GlyphRun* lastRun = nullptr;
+    size_t reserveSize = text.size() / 4;
+    SimpleArrayBuilder<uint32_t> breakBuilder(reserveSize);
+    for (const Paragraph& para : paragraphs)
+    {
+        for (GlyphRun& gr : para.runs)
+        {
+            if (lastRun != nullptr)
+            {
+                lastRun->breaks = std::move(breakBuilder);
+                // Reset the builder.
+                breakBuilder = SimpleArrayBuilder<uint32_t>(reserveSize);
+            }
+            uint32_t glyphIndex = 0;
+            for (uint32_t offset : gr.textIndices)
+            {
+                Unichar unicode = text[offset];
+                if (unicode == '\n' || unicode == 0x2028)
+                {
+                    breakBuilder.add(glyphIndex);
+                    breakBuilder.add(glyphIndex);
+                }
+                if (wantWhiteSpace == isWhiteSpace(unicode))
+                {
+                    breakBuilder.add(glyphIndex);
+                    wantWhiteSpace = !wantWhiteSpace;
+                }
+                glyphIndex++;
+            }
+
+            lastRun = &gr;
+        }
+    }
+    if (lastRun != nullptr)
+    {
+        if (wantWhiteSpace)
+        {
+            breakBuilder.add((uint32_t)lastRun->glyphs.size());
+        }
+        else
+        {
+            // Consume the rest of the run.
+            breakBuilder.add(breakBuilder.empty() ? 0 : breakBuilder.back());
+            breakBuilder.add((uint32_t)lastRun->glyphs.size());
+        }
+        lastRun->breaks = std::move(breakBuilder);
+    }
+
+#ifdef DEBUG
+    for (const Paragraph& para : paragraphs)
+    {
+        for (const GlyphRun& gr : para.runs)
+        {
+            assert(gr.glyphs.size() > 0);
+            assert(gr.glyphs.size() == gr.textIndices.size());
+            assert(gr.glyphs.size() + 1 == gr.xpos.size());
+        }
+    }
+#endif
+    return paragraphs;
+}
diff --git a/src/scene.cpp b/src/scene.cpp
new file mode 100644
index 0000000..679f33e
--- /dev/null
+++ b/src/scene.cpp
@@ -0,0 +1,35 @@
+#include "rive/artboard.hpp"
+#include "rive/hit_result.hpp"
+#include "rive/scene.hpp"
+#include "rive/generated/core_registry.hpp"
+using namespace rive;
+
+Scene::Scene(ArtboardInstance* abi) : m_artboardInstance(abi)
+{
+    assert(m_artboardInstance->isInstance());
+}
+
+float Scene::width() const { return m_artboardInstance->width(); }
+
+float Scene::height() const { return m_artboardInstance->height(); }
+
+void Scene::draw(Renderer* renderer) { m_artboardInstance->draw(renderer); }
+
+HitResult Scene::pointerDown(Vec2D) { return HitResult::none; }
+HitResult Scene::pointerMove(Vec2D) { return HitResult::none; }
+HitResult Scene::pointerUp(Vec2D) { return HitResult::none; }
+HitResult Scene::pointerExit(Vec2D) { return HitResult::none; }
+
+size_t Scene::inputCount() const { return 0; }
+SMIInput* Scene::input(size_t index) const { return nullptr; }
+SMIBool* Scene::getBool(const std::string&) const { return nullptr; }
+SMINumber* Scene::getNumber(const std::string&) const { return nullptr; }
+SMITrigger* Scene::getTrigger(const std::string&) const { return nullptr; }
+void Scene::dataContextFromInstance(ViewModelInstance* viewModelInstance) {}
+
+void Scene::reportKeyedCallback(uint32_t objectId, uint32_t propertyKey, float elapsedSeconds)
+{
+    auto coreObject = m_artboardInstance->resolve(objectId);
+    CallbackData data(this, elapsedSeconds);
+    CoreRegistry::setCallback(coreObject, propertyKey, data);
+}
diff --git a/src/shapes/clipping_shape.cpp b/src/shapes/clipping_shape.cpp
new file mode 100644
index 0000000..e795a6c
--- /dev/null
+++ b/src/shapes/clipping_shape.cpp
@@ -0,0 +1,107 @@
+#include "rive/shapes/clipping_shape.hpp"
+#include "rive/artboard.hpp"
+#include "rive/core_context.hpp"
+#include "rive/factory.hpp"
+#include "rive/node.hpp"
+#include "rive/renderer.hpp"
+#include "rive/shapes/path_composer.hpp"
+#include "rive/shapes/shape.hpp"
+
+using namespace rive;
+
+StatusCode ClippingShape::onAddedClean(CoreContext* context)
+{
+    auto clippingHolder = parent();
+
+    auto artboard = static_cast<Artboard*>(context);
+    for (auto core : artboard->objects())
+    {
+        if (core == nullptr)
+        {
+            continue;
+        }
+        // Iterate artboard to find drawables that are parented to this clipping
+        // shape, they need to know they'll be clipped by this shape.
+        if (core->is<Drawable>())
+        {
+            auto drawable = core->as<Drawable>();
+            for (ContainerComponent* component = drawable; component != nullptr;
+                 component = component->parent())
+            {
+                if (component == clippingHolder)
+                {
+                    drawable->addClippingShape(this);
+                    break;
+                }
+            }
+        }
+
+        // Iterate artboard to find shapes that are parented to the source,
+        // their paths will need to be RenderPaths in order to be used for
+        // clipping operations.
+        if (core->is<Shape>())
+        {
+            auto component = core->as<ContainerComponent>();
+            while (component != nullptr)
+            {
+                if (component == m_Source)
+                {
+                    auto shape = core->as<Shape>();
+                    shape->addFlags(PathFlags::world | PathFlags::clipping);
+                    m_Shapes.push_back(shape);
+                    break;
+                }
+                component = component->parent();
+            }
+        }
+    }
+    m_RenderPath = artboard->factory()->makeEmptyRenderPath();
+
+    return StatusCode::Ok;
+}
+
+StatusCode ClippingShape::onAddedDirty(CoreContext* context)
+{
+    StatusCode code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+    auto coreObject = context->resolve(sourceId());
+    if (coreObject == nullptr || !coreObject->is<Node>())
+    {
+        return StatusCode::MissingObject;
+    }
+
+    m_Source = static_cast<Node*>(coreObject);
+
+    return StatusCode::Ok;
+}
+
+void ClippingShape::buildDependencies()
+{
+    for (auto shape : m_Shapes)
+    {
+        shape->pathComposer()->addDependent(this);
+    }
+}
+
+static Mat2D identity;
+void ClippingShape::update(ComponentDirt value)
+{
+    if (hasDirt(value, ComponentDirt::Path | ComponentDirt::WorldTransform))
+    {
+        m_RenderPath->rewind();
+
+        m_RenderPath->fillRule((FillRule)fillRule());
+        m_ClipRenderPath = nullptr;
+        for (auto shape : m_Shapes)
+        {
+            if (!shape->isEmpty())
+            {
+                m_RenderPath->addPath(shape->pathComposer()->worldPath(), identity);
+                m_ClipRenderPath = m_RenderPath.get();
+            }
+        }
+    }
+}
diff --git a/src/shapes/cubic_asymmetric_vertex.cpp b/src/shapes/cubic_asymmetric_vertex.cpp
new file mode 100644
index 0000000..2901adf
--- /dev/null
+++ b/src/shapes/cubic_asymmetric_vertex.cpp
@@ -0,0 +1,38 @@
+#include "rive/shapes/cubic_asymmetric_vertex.hpp"
+#include "rive/math/vec2d.hpp"
+#include <cmath>
+
+using namespace rive;
+
+static Vec2D get_point(const CubicAsymmetricVertex& v) { return Vec2D(v.x(), v.y()); }
+
+static Vec2D in_vector(const CubicAsymmetricVertex& v)
+{
+    return Vec2D(cos(v.rotation()) * v.inDistance(), sin(v.rotation()) * v.inDistance());
+}
+
+static Vec2D out_vector(const CubicAsymmetricVertex& v)
+{
+    return Vec2D(cos(v.rotation()) * v.outDistance(), sin(v.rotation()) * v.outDistance());
+}
+
+void CubicAsymmetricVertex::computeIn() { m_InPoint = get_point(*this) - in_vector(*this); }
+
+void CubicAsymmetricVertex::computeOut() { m_OutPoint = get_point(*this) + out_vector(*this); }
+
+void CubicAsymmetricVertex::rotationChanged()
+{
+    m_InValid = false;
+    m_OutValid = false;
+    markGeometryDirty();
+}
+void CubicAsymmetricVertex::inDistanceChanged()
+{
+    m_InValid = false;
+    markGeometryDirty();
+}
+void CubicAsymmetricVertex::outDistanceChanged()
+{
+    m_OutValid = false;
+    markGeometryDirty();
+}
diff --git a/src/shapes/cubic_detached_vertex.cpp b/src/shapes/cubic_detached_vertex.cpp
new file mode 100644
index 0000000..3706f08
--- /dev/null
+++ b/src/shapes/cubic_detached_vertex.cpp
@@ -0,0 +1,42 @@
+#include "rive/shapes/cubic_detached_vertex.hpp"
+#include "rive/math/vec2d.hpp"
+#include <cmath>
+
+using namespace rive;
+
+static Vec2D get_point(const CubicDetachedVertex& v) { return Vec2D(v.x(), v.y()); }
+
+static Vec2D in_vector(const CubicDetachedVertex& v)
+{
+    return Vec2D(cos(v.inRotation()) * v.inDistance(), sin(v.inRotation()) * v.inDistance());
+}
+
+static Vec2D out_vector(const CubicDetachedVertex& v)
+{
+    return Vec2D(cos(v.outRotation()) * v.outDistance(), sin(v.outRotation()) * v.outDistance());
+}
+
+void CubicDetachedVertex::computeIn() { m_InPoint = get_point(*this) + in_vector(*this); }
+
+void CubicDetachedVertex::computeOut() { m_OutPoint = get_point(*this) + out_vector(*this); }
+
+void CubicDetachedVertex::inRotationChanged()
+{
+    m_InValid = false;
+    markGeometryDirty();
+}
+void CubicDetachedVertex::inDistanceChanged()
+{
+    m_InValid = false;
+    markGeometryDirty();
+}
+void CubicDetachedVertex::outRotationChanged()
+{
+    m_OutValid = false;
+    markGeometryDirty();
+}
+void CubicDetachedVertex::outDistanceChanged()
+{
+    m_OutValid = false;
+    markGeometryDirty();
+}
diff --git a/src/shapes/cubic_mirrored_vertex.cpp b/src/shapes/cubic_mirrored_vertex.cpp
new file mode 100644
index 0000000..0a23c44
--- /dev/null
+++ b/src/shapes/cubic_mirrored_vertex.cpp
@@ -0,0 +1,27 @@
+#include "rive/shapes/cubic_mirrored_vertex.hpp"
+#include "rive/math/vec2d.hpp"
+#include <cmath>
+
+using namespace rive;
+
+static Vec2D get_point(const CubicMirroredVertex& v) { return Vec2D(v.x(), v.y()); }
+
+static Vec2D get_vector(const CubicMirroredVertex& v)
+{
+    return Vec2D(cos(v.rotation()) * v.distance(), sin(v.rotation()) * v.distance());
+}
+
+void CubicMirroredVertex::computeIn() { m_InPoint = get_point(*this) - get_vector(*this); }
+
+void CubicMirroredVertex::computeOut() { m_OutPoint = get_point(*this) + get_vector(*this); }
+
+void CubicMirroredVertex::rotationChanged()
+{
+    m_InValid = m_OutValid = false;
+    markGeometryDirty();
+}
+void CubicMirroredVertex::distanceChanged()
+{
+    m_InValid = m_OutValid = false;
+    markGeometryDirty();
+}
diff --git a/src/shapes/cubic_vertex.cpp b/src/shapes/cubic_vertex.cpp
new file mode 100644
index 0000000..7fe7eca
--- /dev/null
+++ b/src/shapes/cubic_vertex.cpp
@@ -0,0 +1,89 @@
+#include "rive/shapes/cubic_vertex.hpp"
+#include "rive/bones/cubic_weight.hpp"
+using namespace rive;
+
+const Vec2D& CubicVertex::renderIn()
+{
+    if (hasWeight())
+    {
+        return weight<CubicWeight>()->inTranslation();
+    }
+    else
+    {
+        return inPoint();
+    }
+}
+
+const Vec2D& CubicVertex::renderOut()
+{
+    if (hasWeight())
+    {
+        return weight<CubicWeight>()->outTranslation();
+    }
+    else
+    {
+        return outPoint();
+    }
+}
+
+const Vec2D& CubicVertex::inPoint()
+{
+    if (!m_InValid)
+    {
+        computeIn();
+        m_InValid = true;
+    }
+    return m_InPoint;
+}
+
+const Vec2D& CubicVertex::outPoint()
+{
+    if (!m_OutValid)
+    {
+        computeOut();
+        m_OutValid = true;
+    }
+    return m_OutPoint;
+}
+
+void CubicVertex::outPoint(const Vec2D& value)
+{
+    m_OutPoint = value;
+    m_OutValid = true;
+}
+
+void CubicVertex::inPoint(const Vec2D& value)
+{
+    m_InPoint = value;
+    m_InValid = true;
+}
+
+void CubicVertex::xChanged()
+{
+    Super::xChanged();
+    m_InValid = m_OutValid = false;
+}
+void CubicVertex::yChanged()
+{
+    Super::yChanged();
+    m_InValid = m_OutValid = false;
+}
+
+void CubicVertex::deform(const Mat2D& worldTransform, const float* boneTransforms)
+{
+    Super::deform(worldTransform, boneTransforms);
+
+    auto cubicWeight = weight<CubicWeight>();
+
+    cubicWeight->inTranslation() = Weight::deform(inPoint(),
+                                                  cubicWeight->inIndices(),
+                                                  cubicWeight->inValues(),
+                                                  worldTransform,
+                                                  boneTransforms);
+
+    cubicWeight->outTranslation() = Weight::deform(outPoint(),
+                                                   cubicWeight->outIndices(),
+                                                   cubicWeight->outValues(),
+                                                   worldTransform,
+                                                   boneTransforms);
+}
diff --git a/src/shapes/ellipse.cpp b/src/shapes/ellipse.cpp
new file mode 100644
index 0000000..06489e6
--- /dev/null
+++ b/src/shapes/ellipse.cpp
@@ -0,0 +1,47 @@
+#include "rive/shapes/ellipse.hpp"
+#include "rive/component_dirt.hpp"
+#include "rive/math/circle_constant.hpp"
+
+using namespace rive;
+
+Ellipse::Ellipse()
+{
+    addVertex(&m_Vertex1);
+    addVertex(&m_Vertex2);
+    addVertex(&m_Vertex3);
+    addVertex(&m_Vertex4);
+}
+
+void Ellipse::update(ComponentDirt value)
+{
+    if (hasDirt(value, ComponentDirt::Path))
+    {
+        auto radiusX = width() / 2.0f;
+        auto radiusY = height() / 2.0f;
+
+        auto ox = -originX() * width() + radiusX;
+        auto oy = -originY() * height() + radiusY;
+
+        m_Vertex1.x(ox);
+        m_Vertex1.y(oy - radiusY);
+        m_Vertex1.inPoint(Vec2D(ox - radiusX * circleConstant, oy - radiusY));
+        m_Vertex1.outPoint(Vec2D(ox + radiusX * circleConstant, oy - radiusY));
+
+        m_Vertex2.x(ox + radiusX);
+        m_Vertex2.y(oy);
+        m_Vertex2.inPoint(Vec2D(ox + radiusX, oy + circleConstant * -radiusY));
+        m_Vertex2.outPoint(Vec2D(ox + radiusX, oy + circleConstant * radiusY));
+
+        m_Vertex3.x(ox);
+        m_Vertex3.y(oy + radiusY);
+        m_Vertex3.inPoint(Vec2D(ox + radiusX * circleConstant, oy + radiusY));
+        m_Vertex3.outPoint(Vec2D(ox - radiusX * circleConstant, oy + radiusY));
+
+        m_Vertex4.x(ox - radiusX);
+        m_Vertex4.y(oy);
+        m_Vertex4.inPoint(Vec2D(ox - radiusX, oy + radiusY * circleConstant));
+        m_Vertex4.outPoint(Vec2D(ox - radiusX, oy - radiusY * circleConstant));
+    }
+
+    Super::update(value);
+}
\ No newline at end of file
diff --git a/src/shapes/image.cpp b/src/shapes/image.cpp
new file mode 100644
index 0000000..d86687e
--- /dev/null
+++ b/src/shapes/image.cpp
@@ -0,0 +1,201 @@
+#include "rive/math/hit_test.hpp"
+#include "rive/shapes/image.hpp"
+#include "rive/backboard.hpp"
+#include "rive/importers/backboard_importer.hpp"
+#include "rive/assets/file_asset.hpp"
+#include "rive/assets/image_asset.hpp"
+#include "rive/shapes/mesh.hpp"
+#include "rive/artboard.hpp"
+#include "rive/clip_result.hpp"
+
+using namespace rive;
+
+void Image::draw(Renderer* renderer)
+{
+    rive::ImageAsset* asset = this->imageAsset();
+    if (asset == nullptr || renderOpacity() == 0.0f)
+    {
+        return;
+    }
+
+    rive::RenderImage* renderImage = asset->renderImage();
+    if (renderImage == nullptr)
+    {
+        return;
+    }
+
+    ClipResult clipResult = applyClip(renderer);
+
+    if (clipResult == ClipResult::noClip)
+    {
+        // We didn't clip, so make sure to save as we'll be doing some
+        // transformations.
+        renderer->save();
+    }
+
+    if (clipResult != ClipResult::emptyClip)
+    {
+        auto width = renderImage->width();
+        auto height = renderImage->height();
+
+        if (m_Mesh != nullptr)
+        {
+            m_Mesh->draw(renderer, renderImage, blendMode(), renderOpacity());
+        }
+        else
+        {
+            renderer->transform(worldTransform());
+            renderer->translate(-width * originX(), -height * originY());
+            renderer->drawImage(renderImage, blendMode(), renderOpacity());
+        }
+    }
+
+    renderer->restore();
+}
+
+Core* Image::hitTest(HitInfo* hinfo, const Mat2D& xform)
+{
+    // TODO: handle clip?
+
+    auto renderImage = imageAsset()->renderImage();
+    int width = renderImage->width();
+    int height = renderImage->height();
+
+    if (m_Mesh)
+    {
+        printf("Missing mesh\n");
+        // TODO: hittest mesh
+    }
+    else
+    {
+        auto mx = xform * worldTransform() *
+                  Mat2D::fromTranslate(-width * originX(), -height * originY());
+        HitTester tester(hinfo->area);
+        tester.addRect(AABB(0, 0, (float)width, (float)height), mx);
+        if (tester.test())
+        {
+            return this;
+        }
+    }
+    return nullptr;
+}
+
+StatusCode Image::import(ImportStack& importStack)
+{
+    auto result = registerReferencer(importStack);
+    if (result != StatusCode::Ok)
+    {
+        return result;
+    }
+    return Super::import(importStack);
+}
+
+// Question: thoughts on this? it looks a bit odd to me,
+// maybe there's a trick i'm missing here .. (could also implement getAssetId...)
+uint32_t Image::assetId() { return ImageBase::assetId(); }
+
+void Image::setAsset(FileAsset* asset)
+{
+    if (asset->is<ImageAsset>())
+    {
+        FileAssetReferencer::setAsset(asset);
+
+        // If we have a mesh and we're in the source artboard, let's initialize
+        // the mesh buffers.
+        if (m_Mesh != nullptr && !artboard()->isInstance())
+        {
+            m_Mesh->initializeSharedBuffers(imageAsset()->renderImage());
+        }
+    }
+}
+
+Core* Image::clone() const
+{
+    Image* twin = ImageBase::clone()->as<Image>();
+    if (m_fileAsset != nullptr)
+    {
+        twin->setAsset(m_fileAsset);
+    }
+    return twin;
+}
+
+void Image::setMesh(Mesh* mesh) { m_Mesh = mesh; }
+Mesh* Image::mesh() const { return m_Mesh; }
+
+float Image::width() const
+{
+    rive::ImageAsset* asset = this->imageAsset();
+    if (asset == nullptr)
+    {
+        return 0.0f;
+    }
+
+    rive::RenderImage* renderImage = asset->renderImage();
+    if (renderImage == nullptr)
+    {
+        return 0.0f;
+    }
+    return renderImage->width();
+}
+
+float Image::height() const
+{
+    rive::ImageAsset* asset = this->imageAsset();
+    if (asset == nullptr)
+    {
+        return 0.0f;
+    }
+
+    rive::RenderImage* renderImage = asset->renderImage();
+    if (renderImage == nullptr)
+    {
+        return 0.0f;
+    }
+    return renderImage->height();
+}
+
+Vec2D Image::measureLayout(float width,
+                           LayoutMeasureMode widthMode,
+                           float height,
+                           LayoutMeasureMode heightMode)
+{
+    float measuredWidth, measuredHeight;
+    switch (widthMode)
+    {
+        case LayoutMeasureMode::atMost:
+            measuredWidth = std::max(Image::width(), width);
+            break;
+        case LayoutMeasureMode::exactly:
+            measuredWidth = width;
+            break;
+        case LayoutMeasureMode::undefined:
+            measuredWidth = Image::width();
+            break;
+    }
+    switch (heightMode)
+    {
+        case LayoutMeasureMode::atMost:
+            measuredHeight = std::max(Image::height(), height);
+            break;
+        case LayoutMeasureMode::exactly:
+            measuredHeight = height;
+            break;
+        case LayoutMeasureMode::undefined:
+            measuredHeight = Image::height();
+            break;
+    }
+    return Vec2D(measuredWidth, measuredHeight);
+}
+
+void Image::controlSize(Vec2D size)
+{
+    auto renderImage = imageAsset()->renderImage();
+    auto newScaleX = size.x / renderImage->width();
+    auto newScaleY = size.y / renderImage->height();
+    if (newScaleX != scaleX() || newScaleY != scaleY())
+    {
+        scaleX(newScaleX);
+        scaleY(newScaleY);
+        addDirt(ComponentDirt::WorldTransform, false);
+    }
+}
\ No newline at end of file
diff --git a/src/shapes/mesh.cpp b/src/shapes/mesh.cpp
new file mode 100644
index 0000000..c66fe1d
--- /dev/null
+++ b/src/shapes/mesh.cpp
@@ -0,0 +1,184 @@
+#include "rive/shapes/mesh.hpp"
+#include "rive/shapes/image.hpp"
+#include "rive/shapes/vertex.hpp"
+#include "rive/shapes/mesh_vertex.hpp"
+#include "rive/bones/skin.hpp"
+#include "rive/artboard.hpp"
+#include "rive/factory.hpp"
+#include "rive/span.hpp"
+#include "rive/assets/image_asset.hpp"
+#include <limits>
+
+using namespace rive;
+
+/// Called whenever a vertex moves (x/y change).
+void Mesh::markDrawableDirty()
+{
+    if (skin() != nullptr)
+    {
+        skin()->addDirt(ComponentDirt::Skin);
+    }
+
+    addDirt(ComponentDirt::Vertices);
+}
+
+void Mesh::addVertex(MeshVertex* vertex) { m_Vertices.push_back(vertex); }
+
+StatusCode Mesh::onAddedDirty(CoreContext* context)
+{
+    StatusCode result = Super::onAddedDirty(context);
+    if (result != StatusCode::Ok)
+    {
+        return result;
+    }
+
+    if (!parent()->is<Image>())
+    {
+        return StatusCode::MissingObject;
+    }
+
+    // All good, tell the image it has a mesh.
+    parent()->as<Image>()->setMesh(this);
+
+    return StatusCode::Ok;
+}
+
+StatusCode Mesh::onAddedClean(CoreContext* context)
+{
+    // Make sure Core found indices in the file for this Mesh.
+    if (m_IndexBuffer == nullptr)
+    {
+        return StatusCode::InvalidObject;
+    }
+
+    // Check the indices are all in range. We should consider having a better
+    // error reporting system to the implementor.
+    for (auto index : *m_IndexBuffer)
+    {
+        if (index >= m_Vertices.size())
+        {
+            return StatusCode::InvalidObject;
+        }
+    }
+    return Super::onAddedClean(context);
+}
+
+void Mesh::decodeTriangleIndexBytes(Span<const uint8_t> value)
+{
+    // decode the triangle index bytes
+    rcp<IndexBuffer> buffer = rcp<IndexBuffer>(new IndexBuffer());
+
+    BinaryReader reader(value);
+    while (!reader.reachedEnd())
+    {
+        buffer->push_back(reader.readVarUintAs<uint16_t>());
+    }
+    m_IndexBuffer = buffer;
+}
+
+void Mesh::copyTriangleIndexBytes(const MeshBase& object)
+{
+    m_IndexBuffer = object.as<Mesh>()->m_IndexBuffer;
+}
+
+/// Called whenever a bone moves that is connected to the skin.
+void Mesh::markSkinDirty() { addDirt(ComponentDirt::Vertices); }
+
+Core* Mesh::clone() const
+{
+    auto factory = artboard()->factory();
+    auto clone = static_cast<Mesh*>(MeshBase::clone());
+    clone->m_VertexRenderBufferDirty = true;
+    clone->m_VertexRenderBuffer = factory->makeRenderBuffer(RenderBufferType::vertex,
+                                                            RenderBufferFlags::none,
+                                                            m_Vertices.size() * sizeof(Vec2D));
+    clone->m_UVRenderBuffer = m_UVRenderBuffer;
+    clone->m_IndexRenderBuffer = m_IndexRenderBuffer;
+    return clone;
+}
+
+void Mesh::initializeSharedBuffers(RenderImage* renderImage)
+{
+    Mat2D uvTransform = renderImage != nullptr ? renderImage->uvTransform() : Mat2D();
+
+    auto factory = artboard()->factory();
+    m_VertexRenderBufferDirty = true;
+    m_VertexRenderBuffer = factory->makeRenderBuffer(RenderBufferType::vertex,
+                                                     RenderBufferFlags::none,
+                                                     m_Vertices.size() * sizeof(Vec2D));
+
+    m_UVRenderBuffer = factory->makeRenderBuffer(RenderBufferType::vertex,
+                                                 RenderBufferFlags::mappedOnceAtInitialization,
+                                                 m_Vertices.size() * sizeof(Vec2D));
+    if (m_UVRenderBuffer)
+    {
+        float* uv = static_cast<float*>(m_UVRenderBuffer->map());
+        for (auto vertex : m_Vertices)
+        {
+            Vec2D xformedUV = uvTransform * Vec2D(vertex->u(), vertex->v());
+            *uv++ = xformedUV.x;
+            *uv++ = xformedUV.y;
+        }
+        m_UVRenderBuffer->unmap();
+    }
+
+    m_IndexRenderBuffer = factory->makeRenderBuffer(RenderBufferType::index,
+                                                    RenderBufferFlags::mappedOnceAtInitialization,
+                                                    m_IndexBuffer->size() * sizeof(uint16_t));
+    if (m_IndexRenderBuffer)
+    {
+        void* indexData = m_IndexRenderBuffer->map();
+        memcpy(indexData, m_IndexBuffer->data(), m_IndexRenderBuffer->sizeInBytes());
+        m_IndexRenderBuffer->unmap();
+    }
+}
+
+void Mesh::buildDependencies()
+{
+    Super::buildDependencies();
+    if (skin() != nullptr)
+    {
+        skin()->addDependent(this);
+    }
+    parent()->addDependent(this);
+}
+
+void Mesh::update(ComponentDirt value)
+{
+    if (hasDirt(value, ComponentDirt::Vertices))
+    {
+        if (skin() != nullptr)
+        {
+            skin()->deform({(Vertex**)m_Vertices.data(), m_Vertices.size()});
+        }
+        m_VertexRenderBufferDirty = true;
+    }
+    Super::update(value);
+}
+
+void Mesh::draw(Renderer* renderer, const RenderImage* image, BlendMode blendMode, float opacity)
+{
+    if (m_VertexRenderBufferDirty && m_VertexRenderBuffer != nullptr)
+    {
+        Vec2D* mappedVertices = reinterpret_cast<Vec2D*>(m_VertexRenderBuffer->map());
+        for (auto vertex : m_Vertices)
+        {
+            *mappedVertices++ = vertex->renderTranslation();
+        }
+        m_VertexRenderBuffer->unmap();
+        m_VertexRenderBufferDirty = false;
+    }
+
+    if (skin() == nullptr)
+    {
+        renderer->transform(parent()->as<WorldTransformComponent>()->worldTransform());
+    }
+    renderer->drawImageMesh(image,
+                            m_VertexRenderBuffer,
+                            m_UVRenderBuffer,
+                            m_IndexRenderBuffer,
+                            static_cast<uint32_t>(m_Vertices.size()),
+                            static_cast<uint32_t>(m_IndexBuffer->size()),
+                            blendMode,
+                            opacity);
+}
diff --git a/src/shapes/mesh_vertex.cpp b/src/shapes/mesh_vertex.cpp
new file mode 100644
index 0000000..818ac22
--- /dev/null
+++ b/src/shapes/mesh_vertex.cpp
@@ -0,0 +1,20 @@
+#include "rive/shapes/mesh_vertex.hpp"
+#include "rive/shapes/mesh.hpp"
+
+using namespace rive;
+void MeshVertex::markGeometryDirty() { parent()->as<Mesh>()->markDrawableDirty(); }
+
+StatusCode MeshVertex::onAddedDirty(CoreContext* context)
+{
+    StatusCode code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+    if (!parent()->is<Mesh>())
+    {
+        return StatusCode::MissingObject;
+    }
+    parent()->as<Mesh>()->addVertex(this);
+    return StatusCode::Ok;
+}
\ No newline at end of file
diff --git a/src/shapes/paint/color.cpp b/src/shapes/paint/color.cpp
new file mode 100644
index 0000000..6b6e1e7
--- /dev/null
+++ b/src/shapes/paint/color.cpp
@@ -0,0 +1,77 @@
+#include "rive/shapes/paint/color.hpp"
+
+#include "rive/math/simd.hpp"
+#include <algorithm>
+#include <stdio.h>
+
+namespace rive
+{
+unsigned int colorARGB(int a, int r, int g, int b)
+{
+    return (((a & 0xff) << 24) | ((r & 0xff) << 16) | ((g & 0xff) << 8) | ((b & 0xff) << 0)) &
+           0xFFFFFFFF;
+}
+
+unsigned int colorRed(ColorInt value) { return (0x00ff0000 & value) >> 16; }
+
+unsigned int colorGreen(ColorInt value) { return (0x0000ff00 & value) >> 8; }
+
+unsigned int colorBlue(ColorInt value) { return (0x000000ff & value) >> 0; }
+
+unsigned int colorAlpha(ColorInt value) { return (0xff000000 & value) >> 24; }
+
+void UnpackColorToRGBA8(ColorInt color, uint8_t out[4])
+{
+    auto rgba = simd::cast<uint8_t>(uint4(color) >> uint4{16, 8, 0, 24});
+    simd::store(out, rgba);
+}
+
+void UnpackColorToRGBA32F(ColorInt color, float out[4])
+{
+    float4 color4f = simd::cast<float>(color << uint4{8, 16, 24, 0} >> 24u) * (1.f / 255.f);
+    simd::store(out, color4f);
+}
+
+void UnpackColorToRGBA32FPremul(ColorInt color, float out[4])
+{
+    float4 premulColor4f = simd::cast<float>(color << uint4{8, 16, 24, 0} >> 24u) * (1.f / 255.f);
+    float alpha = premulColor4f.w;
+    premulColor4f *= float4{alpha, alpha, alpha, 1.f};
+    simd::store(out, premulColor4f);
+}
+
+float colorOpacity(ColorInt value) { return (float)colorAlpha(value) / 0xFF; }
+
+static uint8_t opacityToAlpha(float opacity)
+{
+    return (uint8_t)std::lround(255.f * std::max(0.0f, std::min(1.0f, opacity)));
+}
+
+ColorInt colorWithAlpha(ColorInt value, unsigned int a)
+{
+    return colorARGB(a, colorRed(value), colorGreen(value), colorBlue(value));
+}
+
+ColorInt colorWithOpacity(ColorInt value, float opacity)
+{
+    return colorWithAlpha(value, opacityToAlpha(opacity));
+}
+
+ColorInt colorModulateOpacity(ColorInt value, float opacity)
+{
+    return colorWithAlpha(value, opacityToAlpha(colorOpacity(value) * opacity));
+}
+
+static unsigned int lerp(unsigned int a, unsigned int b, float mix)
+{
+    return std::lround(std::max(0.0f, std::min(255.f, a * (1.0f - mix) + b * mix)));
+}
+
+ColorInt colorLerp(ColorInt from, ColorInt to, float mix)
+{
+    return colorARGB(lerp(colorAlpha(from), colorAlpha(to), mix),
+                     lerp(colorRed(from), colorRed(to), mix),
+                     lerp(colorGreen(from), colorGreen(to), mix),
+                     lerp(colorBlue(from), colorBlue(to), mix));
+}
+} // namespace rive
diff --git a/src/shapes/paint/fill.cpp b/src/shapes/paint/fill.cpp
new file mode 100644
index 0000000..d0af7ef
--- /dev/null
+++ b/src/shapes/paint/fill.cpp
@@ -0,0 +1,30 @@
+#include "rive/shapes/paint/fill.hpp"
+
+using namespace rive;
+
+PathFlags Fill::pathFlags() const { return PathFlags::local; }
+
+RenderPaint* Fill::initRenderPaint(ShapePaintMutator* mutator)
+{
+    auto renderPaint = Super::initRenderPaint(mutator);
+    renderPaint->style(RenderPaintStyle::fill);
+    return renderPaint;
+}
+
+void Fill::applyTo(RenderPaint* renderPaint, float opacityModifier) const
+{
+    renderPaint->style(RenderPaintStyle::fill);
+    renderPaint->shader(nullptr);
+    m_PaintMutator->applyTo(renderPaint, opacityModifier);
+}
+
+void Fill::draw(Renderer* renderer, CommandPath* path, const RawPath* rawPath, RenderPaint* paint)
+{
+    if (!isVisible())
+    {
+        return;
+    }
+    auto renderPath = path->renderPath();
+    renderPath->fillRule((FillRule)fillRule());
+    renderer->drawPath(renderPath, paint);
+}
\ No newline at end of file
diff --git a/src/shapes/paint/gradient_stop.cpp b/src/shapes/paint/gradient_stop.cpp
new file mode 100644
index 0000000..fa616df
--- /dev/null
+++ b/src/shapes/paint/gradient_stop.cpp
@@ -0,0 +1,23 @@
+#include "rive/shapes/paint/gradient_stop.hpp"
+#include "rive/shapes/paint/linear_gradient.hpp"
+
+using namespace rive;
+
+StatusCode GradientStop::onAddedDirty(CoreContext* context)
+{
+    StatusCode code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+
+    if (!parent()->is<LinearGradient>())
+    {
+        return StatusCode::MissingObject;
+    }
+    parent()->as<LinearGradient>()->addStop(this);
+    return StatusCode::Ok;
+}
+
+void GradientStop::colorValueChanged() { parent()->as<LinearGradient>()->markGradientDirty(); }
+void GradientStop::positionChanged() { parent()->as<LinearGradient>()->markStopsDirty(); }
\ No newline at end of file
diff --git a/src/shapes/paint/linear_gradient.cpp b/src/shapes/paint/linear_gradient.cpp
new file mode 100644
index 0000000..0bcc1c6
--- /dev/null
+++ b/src/shapes/paint/linear_gradient.cpp
@@ -0,0 +1,169 @@
+#include "rive/shapes/paint/linear_gradient.hpp"
+#include "rive/math/vec2d.hpp"
+#include "rive/artboard.hpp"
+#include "rive/factory.hpp"
+#include "rive/node.hpp"
+#include "rive/renderer.hpp"
+#include "rive/shapes/paint/color.hpp"
+#include "rive/shapes/paint/gradient_stop.hpp"
+#include "rive/shapes/shape_paint_container.hpp"
+#include "rive/shapes/paint/shape_paint.hpp"
+#include <algorithm>
+
+using namespace rive;
+
+StatusCode LinearGradient::onAddedDirty(CoreContext* context)
+{
+    StatusCode code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+
+    if (!initPaintMutator(this))
+    {
+        return StatusCode::MissingObject;
+    }
+    return StatusCode::Ok;
+}
+
+void LinearGradient::buildDependencies()
+{
+    auto p = parent();
+    if (p != nullptr && p->parent() != nullptr)
+    {
+        ContainerComponent* grandParent = p->parent();
+        // Parent's parent must be a shape paint container.
+        assert(ShapePaintContainer::from(grandParent) != nullptr);
+
+        // We need to find the container that owns our world space transform.
+        // This is the first node up the chain (or none, meaning we are in world
+        // space).
+        m_ShapePaintContainer = nullptr;
+        for (ContainerComponent* container = grandParent; container != nullptr;
+             container = container->parent())
+        {
+            if (container->is<Node>())
+            {
+                m_ShapePaintContainer = container->as<Node>();
+                break;
+            }
+        }
+        if (m_ShapePaintContainer != nullptr)
+        {
+            m_ShapePaintContainer->addDependent(this);
+        }
+        else
+        {
+            grandParent->addDependent(this);
+        }
+    }
+}
+
+void LinearGradient::addStop(GradientStop* stop) { m_Stops.push_back(stop); }
+
+static bool stopsComparer(GradientStop* a, GradientStop* b)
+{
+    return a->position() < b->position();
+}
+
+void LinearGradient::update(ComponentDirt value)
+{
+    // Do the stops need to be re-ordered?
+    bool stopsChanged = hasDirt(value, ComponentDirt::Stops);
+    if (stopsChanged)
+    {
+        std::sort(m_Stops.begin(), m_Stops.end(), stopsComparer);
+    }
+
+    // We rebuild the gradient if the gradient is dirty or we paint in world
+    // space and the world space transform has changed, or the local transform
+    // has changed. Local transform changes when a stop moves in local space.
+    bool rebuildGradient =
+        hasDirt(value,
+                ComponentDirt::Paint | ComponentDirt::RenderOpacity | ComponentDirt::Transform) ||
+        (
+            // paints in world space
+            parent()->as<ShapePaint>()->isFlagged(PathFlags::world) &&
+            // and had a world transform change
+            hasDirt(value, ComponentDirt::WorldTransform));
+    if (rebuildGradient)
+    {
+        applyTo(renderPaint(), 1.0f);
+    }
+}
+
+void LinearGradient::applyTo(RenderPaint* renderPaint, float opacityModifier) const
+{
+    bool paintsInWorldSpace = parent()->as<ShapePaint>()->isFlagged(PathFlags::world);
+    Vec2D start(startX(), startY());
+    Vec2D end(endX(), endY());
+    // Check if we need to update the world space gradient (if there's no
+    // shape container, presumably it's the artboard and we're already in
+    // world).
+    if (paintsInWorldSpace && m_ShapePaintContainer != nullptr)
+    {
+        // Get the start and end of the gradient in world coordinates (world
+        // transform of the shape).
+        const Mat2D& world = m_ShapePaintContainer->worldTransform();
+        start = world * start;
+        end = world * end;
+    }
+
+    // build up the color and positions lists
+    const auto ro = opacity() * renderOpacity() * opacityModifier;
+    const auto count = m_Stops.size();
+
+    // need some temporary storage. Allocate enough for both arrays
+    assert(sizeof(ColorInt) == sizeof(float));
+    std::vector<ColorInt> storage(count * 2);
+    ColorInt* colors = storage.data();
+    float* stops = (float*)colors + count;
+
+    for (size_t i = 0; i < count; ++i)
+    {
+        colors[i] = colorModulateOpacity(m_Stops[i]->colorValue(), ro);
+        stops[i] = std::max(0.0f, std::min(m_Stops[i]->position(), 1.0f));
+    }
+
+    makeGradient(renderPaint, start, end, colors, stops, count);
+}
+
+void LinearGradient::makeGradient(RenderPaint* renderPaint,
+                                  Vec2D start,
+                                  Vec2D end,
+                                  const ColorInt colors[],
+                                  const float stops[],
+                                  size_t count) const
+{
+    auto factory = artboard()->factory();
+    renderPaint->shader(
+        factory->makeLinearGradient(start.x, start.y, end.x, end.y, colors, stops, count));
+}
+
+void LinearGradient::markGradientDirty() { addDirt(ComponentDirt::Paint); }
+void LinearGradient::markStopsDirty() { addDirt(ComponentDirt::Paint | ComponentDirt::Stops); }
+
+void LinearGradient::renderOpacityChanged() { markGradientDirty(); }
+
+void LinearGradient::startXChanged() { addDirt(ComponentDirt::Transform); }
+void LinearGradient::startYChanged() { addDirt(ComponentDirt::Transform); }
+void LinearGradient::endXChanged() { addDirt(ComponentDirt::Transform); }
+void LinearGradient::endYChanged() { addDirt(ComponentDirt::Transform); }
+void LinearGradient::opacityChanged() { markGradientDirty(); }
+
+bool LinearGradient::internalIsTranslucent() const
+{
+    if (opacity() < 1)
+    {
+        return true;
+    }
+    for (const auto stop : m_Stops)
+    {
+        if (colorAlpha(stop->colorValue()) != 0xFF)
+        {
+            return true;
+        }
+    }
+    return false; // all of our stops are opaque
+}
diff --git a/src/shapes/paint/radial_gradient.cpp b/src/shapes/paint/radial_gradient.cpp
new file mode 100644
index 0000000..4b099da
--- /dev/null
+++ b/src/shapes/paint/radial_gradient.cpp
@@ -0,0 +1,21 @@
+#include "rive/shapes/paint/radial_gradient.hpp"
+#include "rive/artboard.hpp"
+#include "rive/factory.hpp"
+
+using namespace rive;
+
+void RadialGradient::makeGradient(RenderPaint* renderPaint,
+                                  Vec2D start,
+                                  Vec2D end,
+                                  const ColorInt colors[],
+                                  const float stops[],
+                                  size_t count) const
+{
+    auto factory = artboard()->factory();
+    renderPaint->shader(factory->makeRadialGradient(start.x,
+                                                    start.y,
+                                                    Vec2D::distance(start, end),
+                                                    colors,
+                                                    stops,
+                                                    count));
+}
diff --git a/src/shapes/paint/shape_paint.cpp b/src/shapes/paint/shape_paint.cpp
new file mode 100644
index 0000000..bdc50c9
--- /dev/null
+++ b/src/shapes/paint/shape_paint.cpp
@@ -0,0 +1,34 @@
+#include "rive/shapes/paint/shape_paint.hpp"
+#include "rive/shapes/shape_paint_container.hpp"
+
+#include "rive/artboard.hpp"
+#include "rive/factory.hpp"
+
+using namespace rive;
+
+StatusCode ShapePaint::onAddedClean(CoreContext* context)
+{
+    auto container = ShapePaintContainer::from(parent());
+    if (container == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    container->addPaint(this);
+    return StatusCode::Ok;
+}
+
+RenderPaint* ShapePaint::initRenderPaint(ShapePaintMutator* mutator)
+{
+    assert(m_RenderPaint == nullptr);
+    m_PaintMutator = mutator;
+
+    auto factory = mutator->component()->artboard()->factory();
+    m_RenderPaint = factory->makeRenderPaint();
+    return m_RenderPaint.get();
+}
+
+void ShapePaint::blendMode(BlendMode value)
+{
+    assert(m_RenderPaint != nullptr);
+    m_RenderPaint->blendMode(value);
+}
\ No newline at end of file
diff --git a/src/shapes/paint/shape_paint_mutator.cpp b/src/shapes/paint/shape_paint_mutator.cpp
new file mode 100644
index 0000000..093e71b
--- /dev/null
+++ b/src/shapes/paint/shape_paint_mutator.cpp
@@ -0,0 +1,29 @@
+#include "rive/shapes/paint/shape_paint_mutator.hpp"
+#include "rive/component.hpp"
+#include "rive/shapes/paint/shape_paint.hpp"
+
+using namespace rive;
+
+bool ShapePaintMutator::initPaintMutator(Component* component)
+{
+    auto parent = component->parent();
+    m_Component = component;
+    if (parent->is<ShapePaint>())
+    {
+        // Set this object as the mutator for the shape paint and get a
+        // reference to the paint we'll be mutating.
+        m_RenderPaint = parent->as<ShapePaint>()->initRenderPaint(this);
+        return true;
+    }
+    return false;
+}
+
+void ShapePaintMutator::renderOpacity(float value)
+{
+    if (m_RenderOpacity == value)
+    {
+        return;
+    }
+    m_RenderOpacity = value;
+    renderOpacityChanged();
+}
\ No newline at end of file
diff --git a/src/shapes/paint/solid_color.cpp b/src/shapes/paint/solid_color.cpp
new file mode 100644
index 0000000..340b088
--- /dev/null
+++ b/src/shapes/paint/solid_color.cpp
@@ -0,0 +1,39 @@
+#include "rive/shapes/paint/solid_color.hpp"
+#include "rive/container_component.hpp"
+#include "rive/renderer.hpp"
+#include "rive/shapes/paint/color.hpp"
+
+using namespace rive;
+
+StatusCode SolidColor::onAddedDirty(CoreContext* context)
+{
+    StatusCode code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+    if (!initPaintMutator(this))
+    {
+        return StatusCode::MissingObject;
+    }
+    renderOpacityChanged();
+    return StatusCode::Ok;
+}
+
+void SolidColor::renderOpacityChanged()
+{
+    if (renderPaint() == nullptr)
+    {
+        return;
+    }
+    renderPaint()->color(colorModulateOpacity(colorValue(), renderOpacity()));
+}
+
+void SolidColor::applyTo(RenderPaint* renderPaint, float opacityModifier) const
+{
+    renderPaint->color(colorModulateOpacity(colorValue(), renderOpacity() * opacityModifier));
+}
+
+void SolidColor::colorValueChanged() { renderOpacityChanged(); }
+
+bool SolidColor::internalIsTranslucent() const { return colorAlpha(colorValue()) != 0xFF; }
diff --git a/src/shapes/paint/stroke.cpp b/src/shapes/paint/stroke.cpp
new file mode 100644
index 0000000..a76adae
--- /dev/null
+++ b/src/shapes/paint/stroke.cpp
@@ -0,0 +1,84 @@
+#include "rive/artboard.hpp"
+#include "rive/shapes/paint/stroke.hpp"
+#include "rive/shapes/paint/stroke_cap.hpp"
+#include "rive/shapes/paint/stroke_effect.hpp"
+#include "rive/shapes/paint/stroke_join.hpp"
+
+using namespace rive;
+
+PathFlags Stroke::pathFlags() const
+{
+    return transformAffectsStroke() ? PathFlags::local : PathFlags::world;
+}
+RenderPaint* Stroke::initRenderPaint(ShapePaintMutator* mutator)
+{
+    auto renderPaint = Super::initRenderPaint(mutator);
+    renderPaint->style(RenderPaintStyle::stroke);
+    renderPaint->thickness(thickness());
+    renderPaint->cap((StrokeCap)cap());
+    renderPaint->join((StrokeJoin)join());
+    return renderPaint;
+}
+
+void Stroke::applyTo(RenderPaint* renderPaint, float opacityModifier) const
+{
+    renderPaint->style(RenderPaintStyle::stroke);
+    renderPaint->thickness(thickness());
+    renderPaint->cap((StrokeCap)cap());
+    renderPaint->join((StrokeJoin)join());
+    renderPaint->shader(nullptr);
+    m_PaintMutator->applyTo(renderPaint, opacityModifier);
+}
+
+bool Stroke::isVisible() const { return Super::isVisible() && thickness() > 0.0f; }
+
+void Stroke::draw(Renderer* renderer, CommandPath* path, const RawPath* rawPath, RenderPaint* paint)
+{
+    if (!isVisible())
+    {
+        return;
+    }
+
+    if (m_Effect != nullptr && rawPath != nullptr)
+    {
+        auto factory = artboard()->factory();
+        path = m_Effect->effectPath(*rawPath, factory);
+    }
+
+    renderer->drawPath(path->renderPath(), paint);
+}
+
+void Stroke::thicknessChanged()
+{
+    assert(m_RenderPaint != nullptr);
+    m_RenderPaint->thickness(thickness());
+}
+
+void Stroke::capChanged()
+{
+    assert(m_RenderPaint != nullptr);
+    m_RenderPaint->cap((StrokeCap)cap());
+}
+
+void Stroke::joinChanged()
+{
+    assert(m_RenderPaint != nullptr);
+    m_RenderPaint->join((StrokeJoin)join());
+}
+
+void Stroke::addStrokeEffect(StrokeEffect* effect) { m_Effect = effect; }
+
+void Stroke::invalidateEffects()
+{
+    if (m_Effect != nullptr)
+    {
+        m_Effect->invalidateEffect();
+    }
+    invalidateRendering();
+}
+
+void Stroke::invalidateRendering()
+{
+    assert(m_RenderPaint != nullptr);
+    m_RenderPaint->invalidateStroke();
+}
diff --git a/src/shapes/paint/trim_path.cpp b/src/shapes/paint/trim_path.cpp
new file mode 100644
index 0000000..8cdabd7
--- /dev/null
+++ b/src/shapes/paint/trim_path.cpp
@@ -0,0 +1,176 @@
+#include "rive/shapes/paint/trim_path.hpp"
+#include "rive/shapes/paint/stroke.hpp"
+#include "rive/factory.hpp"
+
+using namespace rive;
+
+StatusCode TrimPath::onAddedClean(CoreContext* context)
+{
+    if (!parent()->is<Stroke>())
+    {
+        return StatusCode::InvalidObject;
+    }
+
+    parent()->as<Stroke>()->addStrokeEffect(this);
+
+    return StatusCode::Ok;
+}
+
+void TrimPath::trimRawPath(const RawPath& source)
+{
+    m_rawPath.rewind();
+    auto renderOffset = std::fmod(std::fmod(offset(), 1.0f) + 1.0f, 1.0f);
+
+    // Build up contours if they're empty.
+    if (m_contours.empty())
+    {
+        ContourMeasureIter iter(&source);
+        while (auto meas = iter.next())
+        {
+            m_contours.push_back(meas);
+        }
+    }
+    switch (mode())
+    {
+        case TrimPathMode::sequential:
+        {
+            float totalLength = 0.0f;
+            for (auto contour : m_contours)
+            {
+                totalLength += contour->length();
+            }
+            auto startLength = totalLength * (start() + renderOffset);
+            auto endLength = totalLength * (end() + renderOffset);
+
+            if (endLength < startLength)
+            {
+                float swap = startLength;
+                startLength = endLength;
+                endLength = swap;
+            }
+
+            if (startLength > totalLength)
+            {
+                startLength -= totalLength;
+                endLength -= totalLength;
+            }
+
+            int i = 0, subPathCount = (int)m_contours.size();
+            while (endLength > 0)
+            {
+                auto contour = m_contours[i % subPathCount];
+                auto contourLength = contour->length();
+
+                if (startLength < contourLength)
+                {
+                    contour->getSegment(startLength, endLength, &m_rawPath, true);
+                    endLength -= contourLength;
+                    startLength = 0;
+                }
+                else
+                {
+                    startLength -= contourLength;
+                    endLength -= contourLength;
+                }
+                i++;
+            }
+        }
+        break;
+
+        case TrimPathMode::synchronized:
+        {
+            for (auto contour : m_contours)
+            {
+                auto contourLength = contour->length();
+                auto startLength = contourLength * (start() + renderOffset);
+                auto endLength = contourLength * (end() + renderOffset);
+                if (endLength < startLength)
+                {
+                    auto length = startLength;
+                    startLength = endLength;
+                    endLength = length;
+                }
+
+                if (startLength > contourLength)
+                {
+                    startLength -= contourLength;
+                    endLength -= contourLength;
+                }
+                contour->getSegment(startLength, endLength, &m_rawPath, true);
+                while (endLength > contourLength)
+                {
+                    startLength = 0;
+                    endLength -= contourLength;
+                    contour->getSegment(startLength, endLength, &m_rawPath, true);
+                }
+            }
+        }
+        break;
+        default:
+            RIVE_UNREACHABLE();
+    }
+}
+
+RenderPath* TrimPath::effectPath(const RawPath& source, Factory* factory)
+{
+    if (m_renderPath != nullptr)
+    {
+        // Previous result hasn't been invalidated, it's still good.
+        return m_renderPath;
+    }
+
+    trimRawPath(source);
+
+    if (!m_trimmedPath)
+    {
+        m_trimmedPath = factory->makeEmptyRenderPath();
+    }
+    else
+    {
+        m_trimmedPath->rewind();
+    }
+
+    m_renderPath = m_trimmedPath.get();
+    m_rawPath.addTo(m_renderPath);
+    return m_renderPath;
+}
+
+void TrimPath::invalidateEffect()
+{
+    invalidateTrim();
+    // This is usually sent when the path is changed so we need to also
+    // invalidate the contours, not just the trim effect.
+    m_contours.clear();
+}
+
+void TrimPath::invalidateTrim()
+{
+    m_renderPath = nullptr;
+    if (parent() != nullptr)
+    {
+        auto stroke = parent()->as<Stroke>();
+        stroke->parent()->addDirt(ComponentDirt::Paint);
+        stroke->invalidateRendering();
+    }
+}
+
+void TrimPath::startChanged() { invalidateTrim(); }
+void TrimPath::endChanged() { invalidateTrim(); }
+void TrimPath::offsetChanged() { invalidateTrim(); }
+void TrimPath::modeValueChanged() { invalidateTrim(); }
+
+StatusCode TrimPath::onAddedDirty(CoreContext* context)
+{
+    auto code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+    switch (mode())
+    {
+        case TrimPathMode::sequential:
+        case TrimPathMode::synchronized:
+            return StatusCode::Ok;
+    }
+    return StatusCode::InvalidObject;
+}
\ No newline at end of file
diff --git a/src/shapes/parametric_path.cpp b/src/shapes/parametric_path.cpp
new file mode 100644
index 0000000..aa39cf3
--- /dev/null
+++ b/src/shapes/parametric_path.cpp
@@ -0,0 +1,62 @@
+#include "rive/layout_component.hpp"
+#include "rive/math/aabb.hpp"
+#include "rive/node.hpp"
+#include "rive/shapes/parametric_path.hpp"
+#include "rive/shapes/shape.hpp"
+
+using namespace rive;
+
+Vec2D ParametricPath::measureLayout(float width,
+                                    LayoutMeasureMode widthMode,
+                                    float height,
+                                    LayoutMeasureMode heightMode)
+{
+    return Vec2D(
+        std::min(
+            (widthMode == LayoutMeasureMode::undefined ? std::numeric_limits<float>::max() : width),
+            ParametricPath::width()),
+        std::min((heightMode == LayoutMeasureMode::undefined ? std::numeric_limits<float>::max()
+                                                             : height),
+                 ParametricPath::height()));
+}
+
+void ParametricPath::controlSize(Vec2D size)
+{
+    width(size.x);
+    height(size.y);
+    markWorldTransformDirty();
+    markPathDirty(false);
+}
+
+void ParametricPath::markPathDirty(bool sendToLayout)
+{
+    Super::markPathDirty();
+#ifdef WITH_RIVE_LAYOUT
+    if (sendToLayout)
+    {
+        for (ContainerComponent* p = parent(); p != nullptr; p = p->parent())
+        {
+            if (p->is<LayoutComponent>())
+            {
+                p->as<LayoutComponent>()->markLayoutNodeDirty();
+                break;
+            }
+            // If we're in a group we break out because objects in groups do
+            // not affect nor are affected by parent LayoutComponents
+            if (p->is<Node>())
+            {
+                if (p->is<Shape>() && p->as<Shape>() == shape())
+                {
+                    continue;
+                }
+                break;
+            }
+        }
+    }
+#endif
+}
+
+void ParametricPath::widthChanged() { markPathDirty(); }
+void ParametricPath::heightChanged() { markPathDirty(); }
+void ParametricPath::originXChanged() { markPathDirty(); }
+void ParametricPath::originYChanged() { markPathDirty(); }
\ No newline at end of file
diff --git a/src/shapes/path.cpp b/src/shapes/path.cpp
new file mode 100644
index 0000000..44784db
--- /dev/null
+++ b/src/shapes/path.cpp
@@ -0,0 +1,440 @@
+#include "rive/shapes/path.hpp"
+#include "rive/renderer.hpp"
+#include "rive/shapes/cubic_vertex.hpp"
+#include "rive/shapes/cubic_detached_vertex.hpp"
+#include "rive/shapes/path_vertex.hpp"
+#include "rive/shapes/shape.hpp"
+#include "rive/shapes/straight_vertex.hpp"
+#include "rive/math/math_types.hpp"
+#include <cassert>
+
+using namespace rive;
+
+/// Compute an ideal control point distance to create a curve of the given
+/// radius. Based on "natural rounding" https://observablehq.com/@daformat/rounding-polygon-corners
+static float computeIdealControlPointDistance(const Vec2D& toPrev,
+                                              const Vec2D& toNext,
+                                              float radius)
+{
+    // Get the angle between next and prev
+    float angle = fabs(atan2(Vec2D::cross(toPrev, toNext), Vec2D::dot(toPrev, toNext)));
+
+    return fmin(radius,
+                (4.0f / 3.0f) * tan(math::PI / (2.0f * ((2.0f * math::PI) / angle))) * radius *
+                    (angle < math::PI / 2 ? 1 + cos(angle) : 2.0f - sin(angle)));
+}
+
+StatusCode Path::onAddedClean(CoreContext* context)
+{
+    StatusCode code = Super::onAddedClean(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+
+    // Find the shape.
+    for (auto currentParent = parent(); currentParent != nullptr;
+         currentParent = currentParent->parent())
+    {
+        if (currentParent->is<Shape>())
+        {
+            m_Shape = currentParent->as<Shape>();
+            m_Shape->addPath(this);
+            return StatusCode::Ok;
+        }
+    }
+
+    return StatusCode::MissingObject;
+}
+
+void Path::buildDependencies() { Super::buildDependencies(); }
+
+void Path::addVertex(PathVertex* vertex) { m_Vertices.push_back(vertex); }
+
+void Path::addFlags(PathFlags flags) { m_pathFlags |= flags; }
+bool Path::isFlagged(PathFlags flags) const { return (int)(m_pathFlags & flags) != 0x00; }
+
+bool Path::canDeferPathUpdate()
+{
+    if (m_Shape == nullptr)
+    {
+        return false;
+    }
+    // A path cannot defer its update if the shapes requires an update. Note the
+    // nuance here where we track that the shape may be marked for follow path
+    // (meaning all child paths need to follow path). This doesn't mean the
+    // Shape is necessarily forced to update put the paths are, which is why we
+    // explicitly also check the shape's path space.
+
+    return m_Shape->canDeferPathUpdate() && !m_Shape->isFlagged(PathFlags::followPath) &&
+           !isFlagged(PathFlags::followPath | PathFlags::clipping);
+}
+
+const Mat2D& Path::pathTransform() const { return worldTransform(); }
+
+void Path::buildPath(RawPath& rawPath) const
+{
+    const bool isClosed = isPathClosed();
+    const std::vector<PathVertex*>& vertices = m_Vertices;
+
+    auto length = vertices.size();
+    if (length < 2)
+    {
+        return;
+    }
+    auto firstPoint = vertices[0];
+
+    // Init out to translation
+    Vec2D out;
+    bool prevIsCubic;
+
+    Vec2D start, startIn;
+    bool startIsCubic;
+
+    if (firstPoint->is<CubicVertex>())
+    {
+        auto cubic = firstPoint->as<CubicVertex>();
+        startIsCubic = prevIsCubic = true;
+        startIn = cubic->renderIn();
+        out = cubic->renderOut();
+        start = cubic->renderTranslation();
+        rawPath.move(start);
+    }
+    else
+    {
+        startIsCubic = prevIsCubic = false;
+        auto point = *firstPoint->as<StraightVertex>();
+        auto radius = point.radius();
+        if (radius > 0.0f)
+        {
+            auto prev = vertices[length - 1];
+
+            Vec2D pos = point.renderTranslation();
+
+            Vec2D toPrev = (prev->is<CubicVertex>() ? prev->as<CubicVertex>()->renderOut()
+                                                    : prev->renderTranslation()) -
+                           pos;
+
+            auto toPrevLength = toPrev.normalizeLength();
+
+            auto next = vertices[1];
+
+            Vec2D toNext = (next->is<CubicVertex>() ? next->as<CubicVertex>()->renderIn()
+                                                    : next->renderTranslation()) -
+                           pos;
+            auto toNextLength = toNext.normalizeLength();
+
+            float renderRadius =
+                std::min(toPrevLength / 2.0f, std::min(toNextLength / 2.0f, radius));
+            float idealDistance = computeIdealControlPointDistance(toPrev, toNext, renderRadius);
+
+            startIn = start = Vec2D::scaleAndAdd(pos, toPrev, renderRadius);
+            rawPath.move(startIn);
+
+            Vec2D outPoint = Vec2D::scaleAndAdd(pos, toPrev, renderRadius - idealDistance);
+            Vec2D inPoint = Vec2D::scaleAndAdd(pos, toNext, renderRadius - idealDistance);
+            out = Vec2D::scaleAndAdd(pos, toNext, renderRadius);
+            rawPath.cubic(outPoint, inPoint, out);
+            prevIsCubic = false;
+        }
+        else
+        {
+            startIn = start = out = point.renderTranslation();
+            rawPath.move(out);
+        }
+    }
+
+    for (size_t i = 1; i < length; i++)
+    {
+        auto vertex = vertices[i];
+
+        if (vertex->is<CubicVertex>())
+        {
+            auto cubic = vertex->as<CubicVertex>();
+            auto inPoint = cubic->renderIn();
+            auto translation = cubic->renderTranslation();
+
+            rawPath.cubic(out, inPoint, translation);
+
+            prevIsCubic = true;
+            out = cubic->renderOut();
+        }
+        else
+        {
+            auto point = *vertex->as<StraightVertex>();
+            Vec2D pos = point.renderTranslation();
+            auto radius = point.radius();
+            if (radius > 0.0f)
+            {
+                auto prev = vertices[i - 1];
+                Vec2D toPrev = (prev->is<CubicVertex>() ? prev->as<CubicVertex>()->renderOut()
+                                                        : prev->renderTranslation()) -
+                               pos;
+                auto toPrevLength = toPrev.normalizeLength();
+
+                auto next = vertices[(i + 1) % length];
+
+                Vec2D toNext = (next->is<CubicVertex>() ? next->as<CubicVertex>()->renderIn()
+                                                        : next->renderTranslation()) -
+                               pos;
+                auto toNextLength = toNext.normalizeLength();
+
+                float renderRadius =
+                    std::min(toPrevLength / 2.0f, std::min(toNextLength / 2.0f, radius));
+                float idealDistance =
+                    computeIdealControlPointDistance(toPrev, toNext, renderRadius);
+
+                Vec2D translation = Vec2D::scaleAndAdd(pos, toPrev, renderRadius);
+                if (prevIsCubic)
+                {
+                    rawPath.cubic(out, translation, translation);
+                }
+                else
+                {
+                    rawPath.line(translation);
+                }
+
+                Vec2D outPoint = Vec2D::scaleAndAdd(pos, toPrev, renderRadius - idealDistance);
+                Vec2D inPoint = Vec2D::scaleAndAdd(pos, toNext, renderRadius - idealDistance);
+                out = Vec2D::scaleAndAdd(pos, toNext, renderRadius);
+                rawPath.cubic(outPoint, inPoint, out);
+                prevIsCubic = false;
+            }
+            else if (prevIsCubic)
+            {
+                rawPath.cubic(out, pos, pos);
+
+                prevIsCubic = false;
+                out = pos;
+            }
+            else
+            {
+                out = pos;
+                rawPath.line(out);
+            }
+        }
+    }
+    if (isClosed)
+    {
+        if (prevIsCubic || startIsCubic)
+        {
+            rawPath.cubic(out, startIn, start);
+        }
+        else
+        {
+            rawPath.line(start);
+        }
+        rawPath.close();
+    }
+}
+
+void Path::markPathDirty(bool sendToLayout)
+{
+    addDirt(ComponentDirt::Path);
+    if (m_Shape != nullptr)
+    {
+        m_Shape->pathChanged();
+    }
+}
+
+void Path::onDirty(ComponentDirt value)
+{
+    if (hasDirt(value, ComponentDirt::WorldTransform) && m_Shape != nullptr)
+    {
+        m_Shape->pathChanged();
+    }
+    if (m_deferredPathDirt)
+    {
+        addDirt(ComponentDirt::Path);
+    }
+}
+
+void Path::update(ComponentDirt value)
+{
+    Super::update(value);
+
+    if (hasDirt(value, ComponentDirt::Path))
+    {
+        if (canDeferPathUpdate())
+        {
+            m_deferredPathDirt = true;
+            return;
+        }
+        m_deferredPathDirt = false;
+        // Build path doesn't explicitly rewind because we use it to concatenate
+        // multiple built paths into a single command path (like the hit
+        // tester).
+        m_rawPath.rewind();
+        buildPath(m_rawPath);
+    }
+    // if (hasDirt(value, ComponentDirt::WorldTransform) && m_Shape != nullptr)
+    // {
+    // 	// Make sure the path composer has an opportunity to rebuild the path
+    // 	// (this is why the composer depends on the shape and all its paths,
+    // 	// ascertaning it updates after both)
+    // 	m_Shape->pathChanged();
+    // }
+}
+
+bool Path::collapse(bool value)
+{
+    bool changed = Super::collapse(value);
+    if (changed && m_Shape != nullptr)
+    {
+        m_Shape->pathCollapseChanged();
+    }
+
+    return changed;
+}
+
+#ifdef ENABLE_QUERY_FLAT_VERTICES
+
+class DisplayCubicVertex : public CubicVertex
+{
+public:
+    DisplayCubicVertex(const Vec2D& in, const Vec2D& out, const Vec2D& translation)
+
+    {
+        m_InPoint = in;
+        m_OutPoint = out;
+        m_InValid = true;
+        m_OutValid = true;
+        x(translation.x);
+        y(translation.y);
+    }
+
+    void computeIn() override {}
+    void computeOut() override {}
+};
+
+FlattenedPath* Path::makeFlat(bool transformToParent)
+{
+    if (m_Vertices.empty())
+    {
+        return nullptr;
+    }
+
+    // Path transform always puts the path into world space.
+    auto transform = pathTransform();
+
+    if (transformToParent && parent()->is<TransformComponent>())
+    {
+        // Put the transform in parent space.
+        auto world = parent()->as<TransformComponent>()->worldTransform();
+        transform = world.invertOrIdentity() * transform;
+    }
+
+    FlattenedPath* flat = new FlattenedPath();
+    auto length = m_Vertices.size();
+    PathVertex* previous = isPathClosed() ? m_Vertices[length - 1] : nullptr;
+    bool deletePrevious = false;
+    for (size_t i = 0; i < length; i++)
+    {
+        auto vertex = m_Vertices[i];
+
+        switch (vertex->coreType())
+        {
+            case StraightVertex::typeKey:
+            {
+                auto point = *vertex->as<StraightVertex>();
+                if (point.radius() > 0.0f && (isPathClosed() || (i != 0 && i != length - 1)))
+                {
+                    auto next = m_Vertices[(i + 1) % length];
+
+                    Vec2D prevPoint = previous->is<CubicVertex>()
+                                          ? previous->as<CubicVertex>()->renderOut()
+                                          : previous->renderTranslation();
+                    Vec2D nextPoint = next->is<CubicVertex>() ? next->as<CubicVertex>()->renderIn()
+                                                              : next->renderTranslation();
+
+                    Vec2D pos = point.renderTranslation();
+
+                    Vec2D toPrev = prevPoint - pos;
+                    auto toPrevLength = toPrev.normalizeLength();
+
+                    Vec2D toNext = nextPoint - pos;
+                    auto toNextLength = toNext.normalizeLength();
+
+                    auto renderRadius = std::min(toPrevLength / 2.0f,
+                                                 std::min(toNextLength / 2.0f, point.radius()));
+                    float idealDistance =
+                        computeIdealControlPointDistance(toPrev, toNext, renderRadius);
+                    Vec2D translation = Vec2D::scaleAndAdd(pos, toPrev, renderRadius);
+
+                    Vec2D out = Vec2D::scaleAndAdd(pos, toPrev, renderRadius - idealDistance);
+                    {
+                        auto v1 = new DisplayCubicVertex(translation, out, translation);
+                        flat->addVertex(v1, transform);
+                        delete v1;
+                    }
+
+                    translation = Vec2D::scaleAndAdd(pos, toNext, renderRadius);
+
+                    Vec2D in = Vec2D::scaleAndAdd(pos, toNext, renderRadius - idealDistance);
+                    auto v2 = new DisplayCubicVertex(in, translation, translation);
+
+                    flat->addVertex(v2, transform);
+                    if (deletePrevious)
+                    {
+                        delete previous;
+                    }
+                    previous = v2;
+                    deletePrevious = true;
+                    break;
+                }
+                RIVE_FALLTHROUGH;
+            }
+            default:
+                if (deletePrevious)
+                {
+                    delete previous;
+                }
+                previous = vertex;
+                deletePrevious = false;
+                flat->addVertex(previous, transform);
+                break;
+        }
+    }
+    if (deletePrevious)
+    {
+        delete previous;
+    }
+    return flat;
+}
+
+void FlattenedPath::addVertex(PathVertex* vertex, const Mat2D& transform)
+{
+    // To make this easy and relatively clean we just transform the vertices.
+    // Requires the vertex to be passed in as a clone.
+    if (vertex->is<CubicVertex>())
+    {
+        auto cubic = vertex->as<CubicVertex>();
+
+        // Cubics need to be transformed so we create a Display version which
+        // has set in/out values.
+        const auto in = transform * cubic->renderIn();
+        const auto out = transform * cubic->renderOut();
+        const auto translation = transform * cubic->renderTranslation();
+
+        auto displayCubic = new DisplayCubicVertex(in, out, translation);
+        m_Vertices.push_back(displayCubic);
+    }
+    else
+    {
+        auto point = new PathVertex();
+        Vec2D translation = transform * vertex->renderTranslation();
+        point->x(translation.x);
+        point->y(translation.y);
+        m_Vertices.push_back(point);
+    }
+}
+
+FlattenedPath::~FlattenedPath()
+{
+    for (auto vertex : m_Vertices)
+    {
+        delete vertex;
+    }
+}
+
+#endif
diff --git a/src/shapes/path_composer.cpp b/src/shapes/path_composer.cpp
new file mode 100644
index 0000000..5efda24
--- /dev/null
+++ b/src/shapes/path_composer.cpp
@@ -0,0 +1,111 @@
+#include "rive/shapes/path_composer.hpp"
+#include "rive/artboard.hpp"
+#include "rive/renderer.hpp"
+#include "rive/shapes/path.hpp"
+#include "rive/shapes/shape.hpp"
+#include "rive/factory.hpp"
+
+using namespace rive;
+
+PathComposer::PathComposer(Shape* shape) : m_shape(shape), m_deferredPathDirt(false) {}
+
+void PathComposer::buildDependencies()
+{
+    assert(m_shape != nullptr);
+    m_shape->addDependent(this);
+    for (auto path : m_shape->paths())
+    {
+        path->addDependent(this);
+    }
+}
+
+void PathComposer::onDirty(ComponentDirt dirt)
+{
+    if (m_deferredPathDirt)
+    {
+        // We'd deferred the update, let's make sure the rest of our
+        // dependencies update too. Constraints need to update too, stroke
+        // effects, etc.
+        m_shape->pathChanged();
+    }
+}
+
+void PathComposer::update(ComponentDirt value)
+{
+    if (hasDirt(value, ComponentDirt::Path))
+    {
+        if (m_shape->canDeferPathUpdate())
+        {
+            m_deferredPathDirt = true;
+            return;
+        }
+        m_deferredPathDirt = false;
+
+        if (m_shape->isFlagged(PathFlags::local))
+        {
+            if (m_localPath == nullptr)
+            {
+                m_localPath = artboard()->factory()->makeEmptyRenderPath();
+            }
+            else
+            {
+                m_localPath->rewind();
+                m_localRawPath.rewind();
+            }
+            auto world = m_shape->worldTransform();
+            Mat2D inverseWorld = world.invertOrIdentity();
+            // Get all the paths into local shape space.
+            for (auto path : m_shape->paths())
+            {
+                if (!path->isHidden() && !path->isCollapsed())
+                {
+                    const auto localTransform = inverseWorld * path->pathTransform();
+                    m_localRawPath.addPath(path->rawPath(), &localTransform);
+                }
+            }
+
+            // TODO: add a CommandPath::copy(RawPath)
+            m_localRawPath.addTo(m_localPath.get());
+        }
+        if (m_shape->isFlagged(PathFlags::world))
+        {
+            if (m_worldPath == nullptr)
+            {
+                m_worldPath = artboard()->factory()->makeEmptyRenderPath();
+            }
+            else
+            {
+                m_worldPath->rewind();
+                m_worldRawPath.rewind();
+            }
+            for (auto path : m_shape->paths())
+            {
+                if (!path->isHidden() && !path->isCollapsed())
+                {
+                    const Mat2D& transform = path->pathTransform();
+                    m_worldRawPath.addPath(path->rawPath(), &transform);
+                }
+            }
+            // TODO: add a CommandPath::copy(RawPath)
+            m_worldRawPath.addTo(m_worldPath.get());
+        }
+        m_shape->markBoundsDirty();
+    }
+}
+
+// Instead of adding dirt and rely on the recursive behavior of the addDirt method,
+// we need to explicitly add dirt to the dependents. The reason is that a collapsed
+// shape will not clear its dirty path flag in the current frame since it is collapsed.
+// So in a future frame if it is uncollapsed, we mark its path flag as dirty again,
+// but since it was already dirty, the recursive part will not kick in and the dependents
+// won't update.
+// This scenario is not common, but it can happen when a solo toggles between an empty
+// group and a path for example.
+void PathComposer::pathCollapseChanged()
+{
+    addDirt(ComponentDirt::Path);
+    for (auto d : dependents())
+    {
+        d->addDirt(ComponentDirt::Path, true);
+    }
+}
\ No newline at end of file
diff --git a/src/shapes/path_vertex.cpp b/src/shapes/path_vertex.cpp
new file mode 100644
index 0000000..637ec0a
--- /dev/null
+++ b/src/shapes/path_vertex.cpp
@@ -0,0 +1,30 @@
+#include "rive/shapes/path_vertex.hpp"
+#include "rive/shapes/path.hpp"
+
+using namespace rive;
+
+StatusCode PathVertex::onAddedDirty(CoreContext* context)
+{
+    StatusCode code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+    if (!parent()->is<Path>())
+    {
+        return StatusCode::MissingObject;
+    }
+    parent()->as<Path>()->addVertex(this);
+    return StatusCode::Ok;
+}
+
+void PathVertex::markGeometryDirty()
+{
+    if (parent() == nullptr)
+    {
+        // This is an acceptable condition as the parametric paths create points
+        // that are not part of the core context.
+        return;
+    }
+    parent()->as<Path>()->markPathDirty();
+}
\ No newline at end of file
diff --git a/src/shapes/points_path.cpp b/src/shapes/points_path.cpp
new file mode 100644
index 0000000..5795357
--- /dev/null
+++ b/src/shapes/points_path.cpp
@@ -0,0 +1,49 @@
+#include "rive/shapes/points_path.hpp"
+#include "rive/shapes/vertex.hpp"
+#include "rive/shapes/path_vertex.hpp"
+#include "rive/shapes/shape.hpp"
+#include "rive/bones/skin.hpp"
+#include "rive/span.hpp"
+
+using namespace rive;
+
+void PointsPath::buildDependencies()
+{
+    Super::buildDependencies();
+    if (skin() != nullptr)
+    {
+        skin()->addDependent(this);
+    }
+}
+
+const Mat2D& PointsPath::pathTransform() const
+{
+    if (skin() != nullptr)
+    {
+        static Mat2D identity;
+        return identity;
+    }
+    return worldTransform();
+}
+
+void PointsPath::update(ComponentDirt value)
+{
+    if (hasDirt(value, ComponentDirt::Path) && skin() != nullptr)
+    {
+        // Path tracks re-adding ComponentDirt::Path if we deferred due to to
+        // shape being invisible.
+        skin()->deform(Span<Vertex*>((Vertex**)m_Vertices.data(), m_Vertices.size()));
+    }
+    Super::update(value);
+}
+
+void PointsPath::markPathDirty(bool sendToLayout)
+{
+    if (skin() != nullptr)
+    {
+        skin()->addDirt(ComponentDirt::Skin);
+    }
+    Super::markPathDirty();
+}
+
+void PointsPath::markSkinDirty() { markPathDirty(); }
diff --git a/src/shapes/polygon.cpp b/src/shapes/polygon.cpp
new file mode 100644
index 0000000..6791b4e
--- /dev/null
+++ b/src/shapes/polygon.cpp
@@ -0,0 +1,55 @@
+#include "rive/shapes/polygon.hpp"
+#include "rive/shapes/star.hpp"
+#include "rive/shapes/straight_vertex.hpp"
+#include "rive/math/math_types.hpp"
+#include <cmath>
+
+using namespace rive;
+
+Polygon::Polygon() {}
+
+Polygon::~Polygon() {}
+
+void Polygon::cornerRadiusChanged() { markPathDirty(); }
+
+void Polygon::pointsChanged() { markPathDirty(); }
+
+std::size_t Polygon::vertexCount() { return points(); }
+
+void Polygon::buildPolygon()
+{
+    auto halfWidth = width() / 2;
+    auto halfHeight = height() / 2;
+
+    auto ox = -originX() * width() + halfWidth;
+    auto oy = -originY() * height() + halfHeight;
+
+    auto angle = -math::PI / 2;
+    auto inc = 2 * math::PI / points();
+
+    for (StraightVertex& vertex : m_PolygonVertices)
+    {
+        vertex.x(ox + cos(angle) * halfWidth);
+        vertex.y(oy + sin(angle) * halfHeight);
+        vertex.radius(cornerRadius());
+        angle += inc;
+    }
+}
+
+void Polygon::update(ComponentDirt value)
+{
+    if (hasDirt(value, ComponentDirt::Path))
+    {
+        if (m_PolygonVertices.size() != vertexCount())
+        {
+            m_PolygonVertices.resize(vertexCount());
+            m_Vertices.clear();
+            for (StraightVertex& vertex : m_PolygonVertices)
+            {
+                m_Vertices.push_back(&vertex);
+            }
+        }
+        buildPolygon();
+    }
+    Super::update(value);
+}
diff --git a/src/shapes/rectangle.cpp b/src/shapes/rectangle.cpp
new file mode 100644
index 0000000..e70cb0e
--- /dev/null
+++ b/src/shapes/rectangle.cpp
@@ -0,0 +1,46 @@
+#include "rive/shapes/rectangle.hpp"
+
+using namespace rive;
+
+Rectangle::Rectangle()
+{
+    addVertex(&m_Vertex1);
+    addVertex(&m_Vertex2);
+    addVertex(&m_Vertex3);
+    addVertex(&m_Vertex4);
+}
+
+void Rectangle::cornerRadiusTLChanged() { markPathDirty(); }
+void Rectangle::cornerRadiusTRChanged() { markPathDirty(); }
+void Rectangle::cornerRadiusBLChanged() { markPathDirty(); }
+void Rectangle::cornerRadiusBRChanged() { markPathDirty(); }
+
+void Rectangle::update(ComponentDirt value)
+{
+    if (hasDirt(value, ComponentDirt::Path))
+    {
+        auto radius = cornerRadiusTL();
+        auto link = linkCornerRadius();
+
+        auto ox = -originX() * width();
+        auto oy = -originY() * height();
+
+        m_Vertex1.x(ox);
+        m_Vertex1.y(oy);
+        m_Vertex1.radius(radius);
+
+        m_Vertex2.x(ox + width());
+        m_Vertex2.y(oy);
+        m_Vertex2.radius(link ? radius : cornerRadiusTR());
+
+        m_Vertex3.x(ox + width());
+        m_Vertex3.y(oy + height());
+        m_Vertex3.radius(link ? radius : cornerRadiusBR());
+
+        m_Vertex4.x(ox);
+        m_Vertex4.y(oy + height());
+        m_Vertex4.radius(link ? radius : cornerRadiusBL());
+    }
+
+    Super::update(value);
+}
\ No newline at end of file
diff --git a/src/shapes/shape.cpp b/src/shapes/shape.cpp
new file mode 100644
index 0000000..68ccf59
--- /dev/null
+++ b/src/shapes/shape.cpp
@@ -0,0 +1,319 @@
+#include "rive/constraints/constraint.hpp"
+#include "rive/hittest_command_path.hpp"
+#include "rive/shapes/path.hpp"
+#include "rive/shapes/points_path.hpp"
+#include "rive/shapes/shape.hpp"
+#include "rive/shapes/clipping_shape.hpp"
+#include "rive/shapes/paint/blend_mode.hpp"
+#include "rive/shapes/paint/shape_paint.hpp"
+#include "rive/shapes/path_composer.hpp"
+#include "rive/clip_result.hpp"
+#include "rive/math/raw_path.hpp"
+#include <algorithm>
+
+using namespace rive;
+
+Shape::Shape() : m_PathComposer(this) {}
+
+void Shape::addPath(Path* path)
+{
+    // Make sure the path is not already in the shape.
+    assert(std::find(m_Paths.begin(), m_Paths.end(), path) == m_Paths.end());
+    m_Paths.push_back(path);
+}
+
+void Shape::addFlags(PathFlags flags) { m_pathFlags |= flags; }
+bool Shape::isFlagged(PathFlags flags) const { return (int)(pathFlags() & flags) != 0x00; }
+
+bool Shape::canDeferPathUpdate()
+{
+    auto canDefer =
+        renderOpacity() == 0 && !isFlagged(PathFlags::clipping | PathFlags::neverDeferUpdate);
+    if (canDefer)
+    {
+        // If we have a dependent Skin, don't defer the update
+        for (auto d : dependents())
+        {
+            if (d->is<PointsPath>() && d->as<PointsPath>()->skin() != nullptr)
+            {
+                return false;
+            }
+        }
+    }
+    return canDefer;
+}
+
+void Shape::update(ComponentDirt value)
+{
+    Super::update(value);
+
+    if (hasDirt(value, ComponentDirt::RenderOpacity))
+    {
+        propagateOpacity(renderOpacity());
+    }
+}
+
+bool Shape::collapse(bool value)
+{
+    if (!Super::collapse(value))
+    {
+        return false;
+    }
+    m_PathComposer.collapse(value);
+    return true;
+}
+
+void Shape::pathChanged()
+{
+    m_PathComposer.addDirt(ComponentDirt::Path, true);
+    for (auto constraint : constraints())
+    {
+        constraint->addDirt(ComponentDirt::Path);
+    }
+    invalidateStrokeEffects();
+}
+
+void Shape::addToRenderPath(RenderPath* path, const Mat2D& transform)
+{
+    if (isFlagged(PathFlags::local))
+    {
+        path->addPath(m_PathComposer.localPath(), transform * worldTransform());
+    }
+    else
+    {
+        path->addPath(m_PathComposer.worldPath(), transform);
+    }
+}
+
+void Shape::draw(Renderer* renderer)
+{
+    if (renderOpacity() == 0.0f)
+    {
+        return;
+    }
+    ClipResult clipResult = applyClip(renderer);
+
+    if (clipResult != ClipResult::emptyClip)
+    {
+        for (auto shapePaint : m_ShapePaints)
+        {
+            if (!shapePaint->isVisible())
+            {
+                continue;
+            }
+            renderer->save();
+            bool paintsInLocal = shapePaint->isFlagged(PathFlags::local);
+            if (paintsInLocal)
+            {
+                renderer->transform(worldTransform());
+            }
+            shapePaint->draw(
+                renderer,
+                paintsInLocal ? m_PathComposer.localPath() : m_PathComposer.worldPath(),
+                paintsInLocal ? &m_PathComposer.localRawPath() : &m_PathComposer.worldRawPath());
+            renderer->restore();
+        }
+    }
+
+    if (clipResult != ClipResult::noClip)
+    {
+        renderer->restore();
+    }
+}
+
+bool Shape::hitTest(const IAABB& area) const
+{
+    HitTestCommandPath tester(area);
+
+    for (auto path : m_Paths)
+    {
+        if (!path->isCollapsed())
+        {
+            tester.setXform(path->pathTransform());
+            path->rawPath().addTo(&tester);
+        }
+    }
+    return tester.wasHit();
+}
+
+Core* Shape::hitTest(HitInfo* hinfo, const Mat2D& xform)
+{
+    if (renderOpacity() == 0.0f)
+    {
+        return nullptr;
+    }
+
+    // TODO: clip:
+
+    const bool shapeIsLocal = isFlagged(PathFlags::local);
+
+    for (auto rit = m_ShapePaints.rbegin(); rit != m_ShapePaints.rend(); ++rit)
+    {
+        auto shapePaint = *rit;
+        if (shapePaint->isTranslucent())
+        {
+            continue;
+        }
+        if (!shapePaint->isVisible())
+        {
+            continue;
+        }
+
+        auto paintIsLocal = shapePaint->isFlagged(PathFlags::local);
+
+        auto mx = xform;
+        if (paintIsLocal)
+        {
+            mx *= worldTransform();
+        }
+
+        HitTestCommandPath tester(hinfo->area);
+
+        for (auto path : m_Paths)
+        {
+            if (shapeIsLocal)
+            {
+                tester.setXform(xform * path->pathTransform());
+            }
+            else
+            {
+                tester.setXform(mx * path->pathTransform());
+            }
+            path->rawPath().addTo(&tester);
+        }
+        if (tester.wasHit())
+        {
+            return this;
+        }
+    }
+    return nullptr;
+}
+
+void Shape::buildDependencies()
+{
+    // Make sure to propagate the call to PathComposer as it's no longer part of
+    // Core and owned only by the Shape.
+    m_PathComposer.buildDependencies();
+
+    Super::buildDependencies();
+
+    // Set the blend mode on all the shape paints. If we ever animate this
+    // property, we'll need to update it in the update cycle/mark dirty when the
+    // blend mode changes.
+    for (auto paint : m_ShapePaints)
+    {
+        paint->blendMode(blendMode());
+    }
+}
+
+StatusCode Shape::onAddedDirty(CoreContext* context)
+{
+    auto code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+    // This ensures context propagates to path composer too.
+    return m_PathComposer.onAddedDirty(context);
+}
+
+bool Shape::isEmpty()
+{
+    for (auto path : m_Paths)
+    {
+        if (!path->isHidden() && !path->isCollapsed())
+        {
+            return false;
+        }
+    }
+    return true;
+}
+
+// Do constraints need to be marked as dirty too? From tests it doesn't seem they do.
+void Shape::pathCollapseChanged() { m_PathComposer.pathCollapseChanged(); }
+
+class ComputeBoundsCommandPath : public CommandPath
+{
+public:
+    ComputeBoundsCommandPath() {}
+
+    AABB bounds(const Mat2D& xform)
+    {
+        m_rawPath.transformInPlace(xform);
+        return m_rawPath.bounds();
+    }
+
+    void rewind() override { m_rawPath.rewind(); }
+    void fillRule(FillRule value) override {}
+    void addPath(CommandPath* path, const Mat2D& transform) override { assert(false); }
+
+    void moveTo(float x, float y) override { m_rawPath.moveTo(x, y); }
+    void lineTo(float x, float y) override { m_rawPath.lineTo(x, y); }
+    void cubicTo(float ox, float oy, float ix, float iy, float x, float y) override
+    {
+        m_rawPath.cubicTo(ox, oy, ix, iy, x, y);
+    }
+    void close() override { m_rawPath.close(); }
+
+    RenderPath* renderPath() override
+    {
+        assert(false);
+        return nullptr;
+    }
+
+private:
+    RawPath m_rawPath;
+};
+
+AABB Shape::computeWorldBounds(const Mat2D* xform) const
+{
+    bool first = true;
+    AABB computedBounds = AABB::forExpansion();
+
+    ComputeBoundsCommandPath boundsCalculator;
+    for (auto path : m_Paths)
+    {
+        if (path->isCollapsed())
+        {
+            continue;
+        }
+        path->rawPath().addTo(&boundsCalculator);
+
+        AABB aabb = boundsCalculator.bounds(xform == nullptr ? path->pathTransform()
+                                                             : path->pathTransform() * *xform);
+
+        if (first)
+        {
+            first = false;
+            computedBounds = aabb;
+        }
+        else
+        {
+            computedBounds.expand(aabb);
+        }
+        boundsCalculator.rewind();
+    }
+
+    return computedBounds;
+}
+
+AABB Shape::computeLocalBounds() const
+{
+    const Mat2D& world = worldTransform();
+    Mat2D inverseWorld = world.invertOrIdentity();
+    return computeWorldBounds(&inverseWorld);
+}
+
+Vec2D Shape::measureLayout(float width,
+                           LayoutMeasureMode widthMode,
+                           float height,
+                           LayoutMeasureMode heightMode)
+{
+    Vec2D size = Vec2D();
+    for (auto path : m_Paths)
+    {
+        Vec2D measured = path->measureLayout(width, widthMode, height, heightMode);
+        size = Vec2D(std::max(size.x, measured.x), std::max(size.y, measured.y));
+    }
+    return size;
+}
\ No newline at end of file
diff --git a/src/shapes/shape_paint_container.cpp b/src/shapes/shape_paint_container.cpp
new file mode 100644
index 0000000..6a702eb
--- /dev/null
+++ b/src/shapes/shape_paint_container.cpp
@@ -0,0 +1,57 @@
+#include "rive/shapes/shape_paint_container.hpp"
+#include "rive/artboard.hpp"
+#include "rive/factory.hpp"
+#include "rive/component.hpp"
+#include "rive/layout_component.hpp"
+#include "rive/shapes/paint/stroke.hpp"
+#include "rive/shapes/shape.hpp"
+#include "rive/text/text_style.hpp"
+
+using namespace rive;
+
+ShapePaintContainer* ShapePaintContainer::from(Component* component)
+{
+    switch (component->coreType())
+    {
+        case Artboard::typeKey:
+            return component->as<Artboard>();
+        case LayoutComponent::typeKey:
+            return component->as<LayoutComponent>();
+        case Shape::typeKey:
+            return component->as<Shape>();
+        case TextStyle::typeKey:
+            return component->as<TextStyle>();
+    }
+    return nullptr;
+}
+
+void ShapePaintContainer::addPaint(ShapePaint* paint) { m_ShapePaints.push_back(paint); }
+
+PathFlags ShapePaintContainer::pathFlags() const
+{
+    PathFlags space = m_pathFlags;
+    for (auto paint : m_ShapePaints)
+    {
+        space |= paint->pathFlags();
+    }
+    return space;
+}
+
+void ShapePaintContainer::invalidateStrokeEffects()
+{
+    for (auto paint : m_ShapePaints)
+    {
+        if (paint->is<Stroke>())
+        {
+            paint->as<Stroke>()->invalidateEffects();
+        }
+    }
+}
+
+void ShapePaintContainer::propagateOpacity(float opacity)
+{
+    for (auto shapePaint : m_ShapePaints)
+    {
+        shapePaint->renderOpacity(opacity);
+    }
+}
diff --git a/src/shapes/star.cpp b/src/shapes/star.cpp
new file mode 100644
index 0000000..20723d8
--- /dev/null
+++ b/src/shapes/star.cpp
@@ -0,0 +1,47 @@
+#include "rive/shapes/star.hpp"
+#include "rive/shapes/straight_vertex.hpp"
+#include "rive/math/math_types.hpp"
+#include <cmath>
+#include <cstdio>
+
+using namespace rive;
+
+Star::Star() {}
+
+void Star::innerRadiusChanged() { markPathDirty(); }
+
+std::size_t Star::vertexCount() { return points() * 2; }
+
+void Star::buildPolygon()
+{
+    auto halfWidth = width() / 2;
+    auto halfHeight = height() / 2;
+    auto innerHalfWidth = width() * innerRadius() / 2;
+    auto innerHalfHeight = height() * innerRadius() / 2;
+    auto ox = -originX() * width() + halfWidth;
+    auto oy = -originY() * height() + halfHeight;
+
+    std::size_t length = vertexCount();
+    auto angle = -math::PI / 2;
+    auto inc = 2 * math::PI / length;
+
+    for (std::size_t i = 0; i < length; i += 2)
+    {
+        {
+            StraightVertex& vertex = m_PolygonVertices[i];
+            vertex.x(ox + cos(angle) * halfWidth);
+            vertex.y(oy + sin(angle) * halfHeight);
+            vertex.radius(cornerRadius());
+            angle += inc;
+        }
+        {
+            StraightVertex& vertex = m_PolygonVertices[i + 1];
+            vertex.x(ox + cos(angle) * innerHalfWidth);
+            vertex.y(oy + sin(angle) * innerHalfHeight);
+            vertex.radius(cornerRadius());
+            angle += inc;
+        }
+    }
+}
+
+void Star::update(ComponentDirt value) { Super::update(value); }
diff --git a/src/shapes/straight_vertex.cpp b/src/shapes/straight_vertex.cpp
new file mode 100644
index 0000000..30c30cd
--- /dev/null
+++ b/src/shapes/straight_vertex.cpp
@@ -0,0 +1,5 @@
+#include "rive/shapes/straight_vertex.hpp"
+
+using namespace rive;
+
+void StraightVertex::radiusChanged() { markGeometryDirty(); }
diff --git a/src/shapes/triangle.cpp b/src/shapes/triangle.cpp
new file mode 100644
index 0000000..6b9ca51
--- /dev/null
+++ b/src/shapes/triangle.cpp
@@ -0,0 +1,32 @@
+#include "rive/shapes/triangle.hpp"
+#include "rive/component_dirt.hpp"
+#include "rive/math/circle_constant.hpp"
+
+using namespace rive;
+
+Triangle::Triangle()
+{
+    addVertex(&m_Vertex1);
+    addVertex(&m_Vertex2);
+    addVertex(&m_Vertex3);
+}
+
+void Triangle::update(ComponentDirt value)
+{
+    if (hasDirt(value, ComponentDirt::Path))
+    {
+        auto ox = -originX() * width();
+        auto oy = -originY() * height();
+
+        m_Vertex1.x(ox + width() / 2);
+        m_Vertex1.y(oy);
+
+        m_Vertex2.x(ox + width());
+        m_Vertex2.y(oy + height());
+
+        m_Vertex3.x(ox);
+        m_Vertex3.y(oy + height());
+    }
+
+    Super::update(value);
+}
\ No newline at end of file
diff --git a/src/shapes/vertex.cpp b/src/shapes/vertex.cpp
new file mode 100644
index 0000000..477122d
--- /dev/null
+++ b/src/shapes/vertex.cpp
@@ -0,0 +1,24 @@
+#include "rive/shapes/vertex.hpp"
+
+using namespace rive;
+
+Vec2D Vertex::renderTranslation()
+{
+    if (hasWeight())
+    {
+        return m_Weight->translation();
+    }
+    return Vec2D(x(), y());
+}
+
+void Vertex::xChanged() { markGeometryDirty(); }
+void Vertex::yChanged() { markGeometryDirty(); }
+
+void Vertex::deform(const Mat2D& worldTransform, const float* boneTransforms)
+{
+    m_Weight->translation() = Weight::deform(Vec2D(x(), y()),
+                                             m_Weight->indices(),
+                                             m_Weight->values(),
+                                             worldTransform,
+                                             boneTransforms);
+}
\ No newline at end of file
diff --git a/src/simple_array.cpp b/src/simple_array.cpp
new file mode 100644
index 0000000..095ee22
--- /dev/null
+++ b/src/simple_array.cpp
@@ -0,0 +1,19 @@
+#ifdef TESTING
+#include "rive/simple_array.hpp"
+namespace rive
+{
+namespace SimpleArrayTesting
+{
+int mallocCount = 0;
+int reallocCount = 0;
+int freeCount = 0;
+void resetCounters()
+{
+    mallocCount = 0;
+    reallocCount = 0;
+    freeCount = 0;
+}
+} // namespace SimpleArrayTesting
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/src/solo.cpp b/src/solo.cpp
new file mode 100644
index 0000000..c958620
--- /dev/null
+++ b/src/solo.cpp
@@ -0,0 +1,53 @@
+#include "rive/solo.hpp"
+#include "rive/constraints/constraint.hpp"
+#include "rive/shapes/clipping_shape.hpp"
+#include "rive/artboard.hpp"
+
+using namespace rive;
+
+void Solo::propagateCollapse(bool collapse)
+{
+    Core* active = collapse ? nullptr : artboard()->resolve(activeComponentId());
+    for (Component* child : children())
+    {
+        // Some child components shouldn't be considered as part of the solo set
+        // as they are more aking to properties of the solo itself. For those
+        // components, simply pass on the collapse value of the solo itself.
+        if (child->is<Constraint>() || child->is<ClippingShape>())
+        {
+            child->collapse(collapse);
+            continue;
+        }
+
+        // This child is part of the solo set so only make it active if it's the
+        // currently marked solo object.
+        child->collapse(child != active);
+    }
+}
+
+bool Solo::collapse(bool value)
+{
+    // Intentionally using Component instead of Super as we don't want to call
+    // collapse on the Container logic which just propagates blindly to
+    // children.
+    if (!Component::collapse(value))
+    {
+        return false;
+    }
+    propagateCollapse(value);
+    return true;
+}
+
+void Solo::activeComponentIdChanged() { propagateCollapse(isCollapsed()); }
+
+StatusCode Solo::onAddedClean(CoreContext* context)
+{
+    StatusCode code = Super::onAddedClean(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+
+    propagateCollapse(isCollapsed());
+    return StatusCode::Ok;
+}
\ No newline at end of file
diff --git a/src/static_scene.cpp b/src/static_scene.cpp
new file mode 100644
index 0000000..97b0286
--- /dev/null
+++ b/src/static_scene.cpp
@@ -0,0 +1,23 @@
+#include "rive/static_scene.hpp"
+#include "rive/artboard.hpp"
+
+using namespace rive;
+
+StaticScene::StaticScene(ArtboardInstance* instance) : Scene(instance) {}
+
+StaticScene::~StaticScene() {}
+
+bool StaticScene::isTranslucent() const { return m_artboardInstance->isTranslucent(); };
+
+std::string StaticScene::name() const { return m_artboardInstance->name(); };
+
+Loop StaticScene::loop() const { return Loop::oneShot; };
+
+float StaticScene::durationSeconds() const { return 0; }
+
+bool StaticScene::advanceAndApply(float seconds)
+{
+    // We ignore the 'seconds' argument because it's not an animated scene
+    m_artboardInstance->advance(0);
+    return true;
+};
\ No newline at end of file
diff --git a/src/text/font_hb.cpp b/src/text/font_hb.cpp
new file mode 100644
index 0000000..4c47708
--- /dev/null
+++ b/src/text/font_hb.cpp
@@ -0,0 +1,618 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "rive/text_engine.hpp"
+
+#ifdef WITH_RIVE_TEXT
+#include "rive/text/font_hb.hpp"
+
+#include "rive/factory.hpp"
+#include "rive/renderer_utils.hpp"
+
+#include "hb.h"
+#include "hb-ot.h"
+#include <unordered_set>
+
+extern "C"
+{
+#include "SheenBidi.h"
+}
+
+// Initialized to null. Client can set this to a callback.
+rive::Font::FallbackProc rive::Font::gFallbackProc;
+
+bool rive::Font::gFallbackProcEnabled = true;
+
+rive::rcp<rive::Font> HBFont::Decode(rive::Span<const uint8_t> span)
+{
+    auto blob = hb_blob_create_or_fail((const char*)span.data(),
+                                       (unsigned)span.size(),
+                                       HB_MEMORY_MODE_DUPLICATE,
+                                       nullptr,
+                                       nullptr);
+    if (blob)
+    {
+        auto face = hb_face_create(blob, 0);
+        hb_blob_destroy(blob);
+        if (face)
+        {
+            auto font = hb_font_create(face);
+            hb_face_destroy(face);
+            if (font)
+            {
+                return rive::rcp<rive::Font>(new HBFont(font));
+            }
+        }
+    }
+    return nullptr;
+}
+
+#ifndef __APPLE__
+rive::rcp<rive::Font> HBFont::FromSystem(void* systemFont) { return nullptr; }
+#endif
+
+//////////////
+
+constexpr int kStdScale = 2048;
+constexpr float gInvScale = 1.0f / kStdScale;
+
+extern "C"
+{
+    static void rpath_move_to(hb_draw_funcs_t*,
+                              void* rpath,
+                              hb_draw_state_t*,
+                              float x,
+                              float y,
+                              void*)
+    {
+        ((rive::RawPath*)rpath)->moveTo(x * gInvScale, -y * gInvScale);
+    }
+    static void rpath_line_to(hb_draw_funcs_t*,
+                              void* rpath,
+                              hb_draw_state_t*,
+                              float x1,
+                              float y1,
+                              void*)
+    {
+        ((rive::RawPath*)rpath)->lineTo(x1 * gInvScale, -y1 * gInvScale);
+    }
+    static void rpath_quad_to(hb_draw_funcs_t*,
+                              void* rpath,
+                              hb_draw_state_t*,
+                              float x1,
+                              float y1,
+                              float x2,
+                              float y2,
+                              void*)
+    {
+        ((rive::RawPath*)rpath)
+            ->quadTo(x1 * gInvScale, -y1 * gInvScale, x2 * gInvScale, -y2 * gInvScale);
+    }
+    static void rpath_cubic_to(hb_draw_funcs_t*,
+                               void* rpath,
+                               hb_draw_state_t*,
+                               float x1,
+                               float y1,
+                               float x2,
+                               float y2,
+                               float x3,
+                               float y3,
+                               void*)
+    {
+        ((rive::RawPath*)rpath)
+            ->cubicTo(x1 * gInvScale,
+                      -y1 * gInvScale,
+                      x2 * gInvScale,
+                      -y2 * gInvScale,
+                      x3 * gInvScale,
+                      -y3 * gInvScale);
+    }
+    static void rpath_close(hb_draw_funcs_t*, void* rpath, hb_draw_state_t*, void*)
+    {
+        ((rive::RawPath*)rpath)->close();
+    }
+}
+
+static rive::Font::LineMetrics make_lmx(hb_font_t* font)
+{
+    // premable on font...
+    hb_ot_font_set_funcs(font);
+    hb_font_set_scale(font, kStdScale, kStdScale);
+
+    hb_font_extents_t extents;
+    hb_font_get_h_extents(font, &extents);
+    return {-extents.ascender * gInvScale, -extents.descender * gInvScale};
+}
+
+HBFont::HBFont(hb_font_t* font) : HBFont(font, {}, {}, {}) {}
+
+HBFont::HBFont(hb_font_t* font,
+               std::unordered_map<hb_tag_t, float> axisValues,
+               std::unordered_map<hb_tag_t, uint32_t> featureValues,
+               std::vector<hb_feature_t> features) :
+    Font(make_lmx(font)),
+    m_font(font),
+    m_features(features),
+    m_featureValues(featureValues),
+    m_axisValues(axisValues)
+{
+    m_drawFuncs = hb_draw_funcs_create();
+    hb_draw_funcs_set_move_to_func(m_drawFuncs, rpath_move_to, nullptr, nullptr);
+    hb_draw_funcs_set_line_to_func(m_drawFuncs, rpath_line_to, nullptr, nullptr);
+    hb_draw_funcs_set_quadratic_to_func(m_drawFuncs, rpath_quad_to, nullptr, nullptr);
+    hb_draw_funcs_set_cubic_to_func(m_drawFuncs, rpath_cubic_to, nullptr, nullptr);
+    hb_draw_funcs_set_close_path_func(m_drawFuncs, rpath_close, nullptr, nullptr);
+    hb_draw_funcs_make_immutable(m_drawFuncs);
+}
+
+HBFont::~HBFont()
+{
+    hb_draw_funcs_destroy(m_drawFuncs);
+    hb_font_destroy(m_font);
+}
+
+static void fillLanguageFeatures(hb_face_t* face,
+                                 hb_tag_t tag,
+                                 uint32_t scriptIndex,
+                                 uint32_t languageIndex,
+                                 std::unordered_set<uint32_t>& features)
+{
+    auto featureCount = hb_ot_layout_language_get_feature_tags(face,
+                                                               tag,
+                                                               scriptIndex,
+                                                               languageIndex,
+                                                               0,
+                                                               nullptr,
+                                                               nullptr);
+    auto featureTags = std::vector<hb_tag_t>(featureCount);
+    hb_ot_layout_language_get_feature_tags(face,
+                                           tag,
+                                           scriptIndex,
+                                           languageIndex,
+                                           0,
+                                           &featureCount,
+                                           featureTags.data());
+
+    for (auto featureTag : featureTags)
+    {
+        features.emplace(featureTag);
+    }
+}
+
+static void fillFeatures(hb_face_t* face, hb_tag_t tag, std::unordered_set<uint32_t>& features)
+{
+    auto scriptCount = hb_ot_layout_table_get_script_tags(face, tag, 0, nullptr, nullptr);
+    auto scripts = std::vector<hb_tag_t>(scriptCount);
+    hb_ot_layout_table_get_script_tags(face, tag, 0, &scriptCount, scripts.data());
+    for (uint32_t i = 0; i < scriptCount; ++i)
+    {
+        auto languageCount =
+            hb_ot_layout_script_get_language_tags(face, tag, i, 0, nullptr, nullptr);
+
+        if (languageCount > 0)
+        {
+            auto languages = std::vector<hb_tag_t>(languageCount);
+            hb_ot_layout_script_get_language_tags(face,
+                                                  tag,
+                                                  i,
+                                                  0,
+                                                  &languageCount,
+                                                  languages.data());
+
+            for (uint32_t j = 0; j < languageCount; ++j)
+            {
+                fillLanguageFeatures(face, tag, i, j, features);
+            }
+        }
+        else
+        {
+            fillLanguageFeatures(face, tag, i, HB_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX, features);
+        }
+    }
+}
+
+rive::SimpleArray<uint32_t> HBFont::features() const
+{
+    std::unordered_set<uint32_t> features;
+    auto face = hb_font_get_face(m_font);
+    fillFeatures(face, HB_OT_TAG_GSUB, features);
+    fillFeatures(face, HB_OT_TAG_GPOS, features);
+
+    rive::SimpleArray<uint32_t> result(features.size());
+    uint32_t index = 0;
+    for (auto tag : features)
+    {
+        result[index++] = tag;
+    }
+    return result;
+}
+
+rive::Font::Axis HBFont::getAxis(uint16_t index) const
+{
+    auto face = hb_font_get_face(m_font);
+    assert(index < hb_ot_var_get_axis_count(face));
+    unsigned n = 1;
+    hb_ot_var_axis_info_t info;
+    hb_ot_var_get_axis_infos(face, index, &n, &info);
+    assert(n == 1);
+    return {info.tag, info.min_value, info.default_value, info.max_value};
+}
+
+uint16_t HBFont::getAxisCount() const
+{
+    auto face = hb_font_get_face(m_font);
+    return (uint16_t)hb_ot_var_get_axis_count(face);
+}
+
+float HBFont::getAxisValue(uint32_t axisTag) const
+{
+    auto itr = m_axisValues.find(axisTag);
+    if (itr != m_axisValues.end())
+    {
+        return itr->second;
+    }
+    auto face = hb_font_get_face(m_font);
+
+    // No value specified, we're using a default.
+    uint32_t axisCount = hb_ot_var_get_axis_count(face);
+    for (uint32_t i = 0; i < axisCount; ++i)
+    {
+        hb_ot_var_axis_info_t info;
+        uint32_t n = 1;
+        hb_ot_var_get_axis_infos(face, i, &n, &info);
+        if (info.tag == axisTag)
+        {
+            return info.default_value;
+        }
+    }
+    return 0.0f;
+}
+
+uint32_t HBFont::getFeatureValue(uint32_t featureTag) const
+{
+    auto itr = m_featureValues.find(featureTag);
+    if (itr != m_featureValues.end())
+    {
+        return itr->second;
+    }
+    return (uint32_t)-1;
+}
+
+rive::rcp<rive::Font> HBFont::withOptions(rive::Span<const Coord> coords,
+                                          rive::Span<const Feature> features) const
+{
+    // Merges previous options with current ones.
+    std::unordered_map<hb_tag_t, float> axisValues = m_axisValues;
+    for (size_t i = 0; i < coords.size(); ++i)
+    {
+        axisValues[coords[i].axis] = coords[i].value;
+    }
+
+    AutoSTArray<16, hb_variation_t> vars(axisValues.size());
+    size_t i = 0;
+    for (auto itr = axisValues.begin(); itr != axisValues.end(); itr++)
+    {
+        vars[i++] = {itr->first, itr->second};
+    }
+
+    auto font = hb_font_create_sub_font(m_font);
+    hb_font_set_variations(font, vars.data(), (unsigned int)vars.size());
+    std::vector<hb_feature_t> hbFeatures;
+    std::unordered_map<hb_tag_t, uint32_t> featureValues = m_featureValues;
+    for (auto feature : features)
+    {
+        featureValues[feature.tag] = feature.value;
+    }
+    for (auto itr = featureValues.begin(); itr != featureValues.end(); itr++)
+    {
+        hbFeatures.push_back(
+            {itr->first, itr->second, HB_FEATURE_GLOBAL_START, HB_FEATURE_GLOBAL_END});
+    }
+
+    return rive::rcp<rive::Font>(new HBFont(font, axisValues, featureValues, hbFeatures));
+}
+
+rive::RawPath HBFont::getPath(rive::GlyphID glyph) const
+{
+    rive::RawPath rpath;
+    hb_font_draw_glyph(m_font, glyph, m_drawFuncs, &rpath);
+    return rpath;
+}
+
+///////////////////////////////////////////////////////////
+
+static rive::GlyphRun shape_run(const rive::Unichar text[],
+                                const rive::TextRun& tr,
+                                unsigned textOffset)
+{
+    hb_buffer_t* buf = hb_buffer_create();
+    hb_buffer_add_utf32(buf, text, tr.unicharCount, 0, tr.unicharCount);
+
+    hb_buffer_set_direction(buf,
+                            tr.dir == rive::TextDirection::rtl ? HB_DIRECTION_RTL
+                                                               : HB_DIRECTION_LTR);
+    hb_buffer_set_script(buf, (hb_script_t)tr.script);
+    hb_buffer_set_language(buf, hb_language_get_default());
+
+    auto hbfont = (HBFont*)tr.font.get();
+
+    hb_shape(hbfont->m_font,
+             buf,
+             hbfont->m_features.data(),
+             (unsigned int)hbfont->m_features.size());
+
+    unsigned int glyph_count;
+    hb_glyph_info_t* glyph_info = hb_buffer_get_glyph_infos(buf, &glyph_count);
+    hb_glyph_position_t* glyph_pos = hb_buffer_get_glyph_positions(buf, &glyph_count);
+
+    // todo: check for missing glyphs, and perform font-substitution
+    rive::GlyphRun gr(glyph_count);
+    gr.font = tr.font;
+    gr.size = tr.size;
+    gr.lineHeight = tr.lineHeight;
+    gr.letterSpacing = tr.letterSpacing;
+    gr.styleId = tr.styleId;
+    gr.dir = tr.dir;
+
+    const float scale = tr.size / kStdScale;
+    for (unsigned int i = 0; i < glyph_count; i++)
+    {
+        unsigned int index = tr.dir == rive::TextDirection::rtl ? glyph_count - 1 - i : i;
+        gr.glyphs[i] = (uint16_t)glyph_info[index].codepoint;
+        gr.textIndices[i] = textOffset + glyph_info[index].cluster;
+        gr.advances[i] = gr.xpos[i] = glyph_pos[index].x_advance * scale + tr.letterSpacing;
+        gr.offsets[i] =
+            rive::Vec2D(glyph_pos[index].x_offset * scale, -glyph_pos[index].y_offset * scale);
+    }
+    gr.xpos[glyph_count] = 0; // so the next run can line up snug
+    hb_buffer_destroy(buf);
+    return gr;
+}
+
+static rive::GlyphRun extract_subset(const rive::GlyphRun& orig, size_t start, size_t end)
+{
+    auto count = end - start;
+    rive::GlyphRun subset(rive::SimpleArray<rive::GlyphID>(&orig.glyphs[start], count),
+                          rive::SimpleArray<uint32_t>(&orig.textIndices[start], count),
+                          rive::SimpleArray<float>(&orig.advances[start], count),
+                          rive::SimpleArray<float>(&orig.xpos[start], count + 1),
+                          rive::SimpleArray<rive::Vec2D>(&orig.offsets[start], count));
+    subset.font = std::move(orig.font);
+    subset.size = orig.size;
+    subset.lineHeight = orig.lineHeight;
+    subset.letterSpacing = orig.letterSpacing;
+    subset.dir = orig.dir;
+    subset.xpos.back() = 0; // since we're now the end of a run
+    subset.styleId = orig.styleId;
+
+    return subset;
+}
+
+static void perform_fallback(rive::rcp<rive::Font> fallbackFont,
+                             rive::SimpleArrayBuilder<rive::GlyphRun>& gruns,
+                             const rive::Unichar text[],
+                             const rive::GlyphRun& orig,
+                             const rive::TextRun& origTextRun)
+{
+    assert(orig.glyphs.size() > 0);
+
+    const size_t count = orig.glyphs.size();
+    size_t startI = 0;
+    while (startI < count)
+    {
+        size_t endI = startI + 1;
+        if (orig.glyphs[startI] == 0)
+        {
+            while (endI < count && orig.glyphs[endI] == 0)
+            {
+                ++endI;
+            }
+            auto textStart = orig.textIndices[startI];
+            auto textCount = orig.textIndices[endI - 1] - textStart + 1;
+            auto tr = rive::TextRun{
+                fallbackFont,
+                orig.size,
+                orig.lineHeight,
+                origTextRun.letterSpacing,
+                textCount,
+                origTextRun.script,
+                orig.styleId,
+                orig.dir,
+            };
+            auto gr = shape_run(&text[textStart], tr, textStart);
+            if (gr.glyphs.size() > 0)
+            {
+                gruns.add(std::move(gr));
+            }
+        }
+        else
+        {
+            while (endI < count && orig.glyphs[endI] != 0)
+            {
+                ++endI;
+            }
+            gruns.add(extract_subset(orig, startI, endI));
+        }
+        startI = endI;
+    }
+}
+
+rive::SimpleArray<rive::Paragraph> HBFont::onShapeText(rive::Span<const rive::Unichar> text,
+                                                       rive::Span<const rive::TextRun> truns) const
+{
+
+    rive::SimpleArrayBuilder<rive::Paragraph> paragraphs;
+    SBCodepointSequence codepointSequence = {SBStringEncodingUTF32,
+                                             (void*)text.data(),
+                                             text.size()};
+
+    hb_unicode_funcs_t* ufuncs = hb_unicode_funcs_get_default();
+
+    // Split runs by bidi types.
+    uint32_t textIndex = 0;
+    uint32_t runIndex = 0;
+    uint32_t runStartTextIndex = 0;
+
+    SBUInteger paragraphStart = 0;
+
+    SBAlgorithmRef bidiAlgorithm = SBAlgorithmCreate(&codepointSequence);
+    uint32_t unicharIndex = 0;
+    uint32_t runTextIndex = 0;
+
+    while (paragraphStart < text.size())
+    {
+        SBParagraphRef paragraph =
+            SBAlgorithmCreateParagraph(bidiAlgorithm, paragraphStart, INT32_MAX, SBLevelDefaultLTR);
+        SBUInteger paragraphLength = SBParagraphGetLength(paragraph);
+        // Next iteration reads the next paragraph (if any remain).
+        paragraphStart += paragraphLength;
+        const SBLevel* bidiLevels = SBParagraphGetLevelsPtr(paragraph);
+        SBLevel paragraphLevel = SBParagraphGetBaseLevel(paragraph);
+        uint32_t paragraphTextIndex = 0;
+
+        std::vector<rive::TextRun> bidiRuns;
+        bidiRuns.reserve(truns.size());
+
+        while (runIndex < truns.size())
+        {
+            const auto& tr = truns[runIndex];
+            assert(tr.unicharCount != 0);
+            SBLevel lastLevel = bidiLevels[paragraphTextIndex];
+            hb_script_t lastScript = hb_unicode_script(ufuncs, text[textIndex]);
+            rive::TextRun splitRun = {
+                tr.font,
+                tr.size,
+                tr.lineHeight,
+                tr.letterSpacing,
+                tr.unicharCount - runTextIndex,
+                (uint32_t)lastScript,
+                tr.styleId,
+                lastLevel & 1 ? rive::TextDirection::rtl : rive::TextDirection::ltr,
+            };
+
+            runStartTextIndex = textIndex;
+
+            runTextIndex++;
+            textIndex++;
+            paragraphTextIndex++;
+            bidiRuns.push_back(splitRun);
+
+            while (runTextIndex < tr.unicharCount && paragraphTextIndex < paragraphLength)
+            {
+                hb_script_t script = hb_unicode_script(ufuncs, text[textIndex]);
+                switch (script)
+                {
+                    case HB_SCRIPT_COMMON:
+                    case HB_SCRIPT_INHERITED:
+                        // Propagate last seen "real" script value.
+                        script = lastScript;
+                        break;
+                    default:
+                        break;
+                }
+                if (bidiLevels[paragraphTextIndex] != lastLevel || script != lastScript)
+                {
+                    lastScript = script;
+                    auto& back = bidiRuns.back();
+                    back.unicharCount = textIndex - runStartTextIndex;
+                    lastLevel = bidiLevels[paragraphTextIndex];
+
+                    rive::TextRun backRun = {
+                        back.font,
+                        back.size,
+                        back.lineHeight,
+                        back.letterSpacing,
+                        tr.unicharCount - runTextIndex,
+                        (uint32_t)script,
+                        back.styleId,
+                        lastLevel & 1 ? rive::TextDirection::rtl : rive::TextDirection::ltr,
+                    };
+                    runStartTextIndex = textIndex;
+                    bidiRuns.push_back(backRun);
+                }
+                runTextIndex++;
+                textIndex++;
+                paragraphTextIndex++;
+            }
+            // Reached the end of the run?
+            if (runTextIndex == tr.unicharCount)
+            {
+                runIndex++;
+                runTextIndex = 0;
+            }
+            // We consumed the whole paragraph.
+            if (paragraphTextIndex == paragraphLength)
+            {
+                // Close off the last run.
+                auto& back = bidiRuns.back();
+                back.unicharCount = textIndex - runStartTextIndex;
+                break;
+            }
+        }
+
+        rive::SimpleArrayBuilder<rive::GlyphRun> gruns(bidiRuns.size());
+
+        for (const auto& tr : bidiRuns)
+        {
+            auto gr = shape_run(&text[unicharIndex], tr, unicharIndex);
+            unicharIndex += tr.unicharCount;
+
+            auto end = gr.glyphs.end();
+            auto iter = std::find(gr.glyphs.begin(), end, 0);
+            if (!gFallbackProc || iter == end || !gFallbackProcEnabled)
+            {
+                if (gr.glyphs.size() > 0)
+                {
+                    gruns.add(std::move(gr));
+                }
+            }
+            else
+            {
+                // found at least 1 zero in glyphs, so need to perform font-fallback
+                size_t index = iter - gr.glyphs.begin();
+                rive::Unichar missing = text[gr.textIndices[index]];
+                // todo: consider sending more chars if that helps choose a font
+                auto fallback = gFallbackProc({&missing, 1});
+                if (fallback)
+                {
+                    perform_fallback(fallback, gruns, text.data(), gr, tr);
+                }
+                else if (gr.glyphs.size() > 0)
+                {
+                    gruns.add(std::move(gr)); // oh well, just keep the missing glyphs
+                }
+            }
+        }
+
+        // turn the advances we stored in xpos[] into actual x-positions
+        // for logical order.
+        float pos = 0;
+        for (auto& gr : gruns)
+        {
+            for (auto& xp : gr.xpos)
+            {
+                float adv = xp;
+                xp = pos;
+                pos += adv;
+            }
+        }
+
+        paragraphs.add({
+            std::move(gruns),
+            paragraphLevel & 1 ? rive::TextDirection::rtl : rive::TextDirection::ltr,
+        });
+        SBParagraphRelease(paragraph);
+    }
+
+    SBAlgorithmRelease(bidiAlgorithm);
+    return paragraphs;
+}
+
+bool HBFont::hasGlyph(rive::Span<const rive::Unichar> missing) const
+{
+    hb_codepoint_t glyph;
+    return !missing.empty() && hb_font_get_nominal_glyph(m_font, missing[0], &glyph);
+}
+
+#endif
diff --git a/src/text/font_hb_apple.mm b/src/text/font_hb_apple.mm
new file mode 100644
index 0000000..0277d74
--- /dev/null
+++ b/src/text/font_hb_apple.mm
@@ -0,0 +1,18 @@
+#include "rive/text_engine.hpp"
+
+#ifdef WITH_RIVE_TEXT
+#include "rive/text/font_hb.hpp"
+#import <CoreText/CoreText.h>
+
+#include "hb-coretext.h"
+
+rive::rcp<rive::Font> HBFont::FromSystem(void* systemFont)
+{
+    auto font = hb_coretext_font_create((CTFontRef)systemFont);
+    if (font)
+    {
+        return rive::rcp<rive::Font>(new HBFont(font));
+    }
+    return nullptr;
+}
+#endif
diff --git a/src/text/glyph_lookup.cpp b/src/text/glyph_lookup.cpp
new file mode 100644
index 0000000..225d851
--- /dev/null
+++ b/src/text/glyph_lookup.cpp
@@ -0,0 +1,52 @@
+#include "rive/text/glyph_lookup.hpp"
+#include "rive/text_engine.hpp"
+
+using namespace rive;
+
+void GlyphLookup::compute(Span<const Unichar> text, const SimpleArray<Paragraph>& shape)
+{
+    size_t codeUnitCount = text.size();
+    m_glyphIndices.resize(codeUnitCount + 1);
+    // Build a mapping of codePoints to glyphs indices.
+    uint32_t glyphIndex = 0;
+    uint32_t lastTextIndex = 0;
+    for (const Paragraph& paragraph : shape)
+    {
+        for (const GlyphRun& run : paragraph.runs)
+        {
+            for (size_t i = 0; i < run.glyphs.size(); i++)
+            {
+                uint32_t textIndex = run.textIndices[i];
+                for (uint32_t j = lastTextIndex; j < textIndex; j++)
+                {
+                    assert(glyphIndex != 0);
+                    m_glyphIndices[j] = glyphIndex - 1;
+                }
+                lastTextIndex = textIndex;
+                glyphIndex++;
+            }
+        }
+    }
+    for (size_t i = lastTextIndex; i < codeUnitCount; i++)
+    {
+        m_glyphIndices[i] = glyphIndex - 1;
+    }
+
+    // Store a fake unreachable glyph at the end to allow selecting the last
+    // one.
+    m_glyphIndices[codeUnitCount] = codeUnitCount == 0 ? 0 : m_glyphIndices[codeUnitCount - 1] + 1;
+}
+
+uint32_t GlyphLookup::count(uint32_t index) const
+{
+    assert(index < (uint32_t)m_glyphIndices.size());
+
+    uint32_t value = m_glyphIndices[index];
+    uint32_t count = 1;
+    uint32_t size = (uint32_t)m_glyphIndices.size();
+    while (++index < size && m_glyphIndices[index] == value)
+    {
+        count++;
+    }
+    return count;
+}
\ No newline at end of file
diff --git a/src/text/line_breaker.cpp b/src/text/line_breaker.cpp
new file mode 100644
index 0000000..298c3e7
--- /dev/null
+++ b/src/text/line_breaker.cpp
@@ -0,0 +1,350 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "rive/text_engine.hpp"
+#include <limits>
+#include <algorithm>
+using namespace rive;
+
+static bool autowidth(float width) { return width < 0.0f; }
+
+float GlyphLine::ComputeMaxWidth(Span<GlyphLine> lines, Span<const GlyphRun> runs)
+{
+    float maxLineWidth = 0.0f;
+    for (auto& line : lines)
+    {
+        maxLineWidth = std::max(maxLineWidth,
+                                runs[line.endRunIndex].xpos[line.endGlyphIndex] -
+                                    runs[line.startRunIndex].xpos[line.startGlyphIndex] -
+                                    runs[line.endRunIndex].letterSpacing);
+    }
+    return maxLineWidth;
+}
+
+static const rive::Font::LineMetrics computeLineMetrics(const rive::Font::LineMetrics& metrics,
+                                                        float customLineHeight,
+                                                        float fontSize)
+{
+    if (customLineHeight < 0.0f)
+    {
+        return {metrics.ascent * fontSize, metrics.descent * fontSize};
+    }
+    float baseline = -metrics.ascent;
+    float height = baseline + metrics.descent;
+    float baselineFactor = baseline / height;
+
+    float actualAscent = -baselineFactor * customLineHeight;
+
+    return {actualAscent, customLineHeight + actualAscent};
+}
+
+void GlyphLine::ComputeLineSpacing(bool isFirstLine,
+                                   Span<GlyphLine> lines,
+                                   Span<const GlyphRun> runs,
+                                   float width,
+                                   TextAlign align)
+{
+    bool first = isFirstLine;
+    float Y = 0; // top of our frame
+    for (auto& line : lines)
+    {
+        float asc = 0;
+        float realAscent = 0;
+        float des = 0;
+        float lh = 0;
+        for (uint32_t i = line.startRunIndex; i <= line.endRunIndex; ++i)
+        {
+            const auto& run = runs[i];
+            const auto& metrics =
+                computeLineMetrics(run.font->lineMetrics(), run.lineHeight, run.size);
+            realAscent = std::min(realAscent, run.font->lineMetrics().ascent * run.size);
+            asc = std::min(asc, metrics.ascent);
+            des = std::max(des, metrics.descent);
+            if (run.lineHeight >= 0.0f)
+            {
+                lh = std::max(lh, run.lineHeight);
+            }
+            else
+            {
+                lh = std::max(lh, -asc + des);
+            }
+        }
+        line.top = Y;
+        if (first)
+        {
+            Y = -realAscent;
+            first = false;
+        }
+        else
+        {
+            Y -= asc;
+        }
+        line.baseline = Y;
+        Y += des;
+        line.bottom = Y;
+
+        auto lineWidth = runs[line.endRunIndex].xpos[line.endGlyphIndex] -
+                         runs[line.startRunIndex].xpos[line.startGlyphIndex] -
+                         runs[line.endRunIndex].letterSpacing;
+        switch (align)
+        {
+            case TextAlign::right:
+                line.startX = width - lineWidth;
+                break;
+            case TextAlign::left:
+                line.startX = 0;
+                break;
+            case TextAlign::center:
+                line.startX = width / 2.0f - lineWidth / 2.0f;
+                break;
+        }
+    }
+}
+
+struct WordMarker
+{
+    const GlyphRun* run;
+    uint32_t index;
+
+    bool next(Span<const GlyphRun> runs)
+    {
+        index += 2;
+        while (index >= run->breaks.size())
+        {
+            index -= run->breaks.size();
+            run++;
+            if (run == runs.end())
+            {
+                return false;
+            }
+        }
+        return true;
+    }
+};
+
+class RunIterator
+{
+    Span<const GlyphRun> m_runs;
+    const GlyphRun* m_run;
+    uint32_t m_index;
+
+public:
+    RunIterator(Span<const GlyphRun> runs, const GlyphRun* run, uint32_t index) :
+        m_runs(runs), m_run(run), m_index(index)
+    {}
+
+    bool back()
+    {
+        if (m_index == 0)
+        {
+            if (m_run == m_runs.begin())
+            {
+                return false;
+            }
+            m_run--;
+            if (m_run->glyphs.size() == 0)
+            {
+                m_index = 0;
+                return back();
+            }
+            else
+            {
+                m_index = m_run->glyphs.size() == 0 ? 0 : (uint32_t)m_run->glyphs.size() - 1;
+            }
+        }
+        else
+        {
+            m_index--;
+        }
+        return true;
+    }
+
+    bool forward()
+    {
+        if (m_index == m_run->glyphs.size())
+        {
+            if (m_run == m_runs.end())
+            {
+                return false;
+            }
+            m_run++;
+            m_index = 0;
+            if (m_index == m_run->glyphs.size())
+            {
+                return forward();
+            }
+        }
+        else
+        {
+            m_index++;
+        }
+        return true;
+    }
+
+    float x() const { return m_run->xpos[m_index]; }
+
+    const GlyphRun* run() const { return m_run; }
+    uint32_t index() const { return m_index; }
+
+    bool operator==(const RunIterator& o) const { return m_run == o.m_run && m_index == o.m_index; }
+};
+
+SimpleArray<GlyphLine> GlyphLine::BreakLines(Span<const GlyphRun> runs, float width)
+{
+    float maxLineWidth = autowidth(width) ? std::numeric_limits<float>::max() : width;
+
+    SimpleArrayBuilder<GlyphLine> lines;
+
+    if (runs.empty())
+    {
+        return lines;
+    }
+
+    auto limit = maxLineWidth;
+
+    bool advanceWord = false;
+
+    // We iterate the breaks list with a WordMarker helper which is basically an
+    // iterator. The breaks lists contains tightly packed start/end indices per
+    // run. So the first valid word is at break index 0,1 (per run). Because a
+    // run can be created with no valid breaks, we start the word iterator at a
+    // negative index and attempt to move it to the first valid index (which
+    // could be in the Nth run in the paragraph). If that fails, we know we have
+    // no words to break in the entire paragraph and can early out. See how
+    // WordMarker::next works and notice how we also use it below in this same
+    // method.
+    WordMarker start = {runs.begin(), (uint32_t)-2};
+    WordMarker end = {runs.begin(), (uint32_t)-1};
+    if (!start.next(runs) || !end.next(runs))
+    {
+        return lines;
+    }
+
+    GlyphLine line = GlyphLine();
+
+    uint32_t breakIndex = end.run->breaks[end.index];
+    const GlyphRun* breakRun = end.run;
+    uint32_t lastEndGlyphIndex = end.index;
+    uint32_t startBreakIndex = start.run->breaks[start.index];
+    const GlyphRun* startBreakRun = start.run;
+
+    float x = end.run->xpos[breakIndex];
+    while (true)
+    {
+        if (advanceWord)
+        {
+            lastEndGlyphIndex = end.index;
+
+            if (!start.next(runs))
+            {
+                break;
+            }
+            if (!end.next(runs))
+            {
+                break;
+            }
+
+            advanceWord = false;
+
+            breakIndex = end.run->breaks[end.index];
+            breakRun = end.run;
+            startBreakIndex = start.run->breaks[start.index];
+            startBreakRun = start.run;
+            x = end.run->xpos[breakIndex];
+        }
+
+        bool isForcedBreak = breakRun == startBreakRun && breakIndex == startBreakIndex;
+
+        if (!isForcedBreak && x > limit)
+        {
+            uint32_t startRunIndex = (uint32_t)(start.run - runs.begin());
+
+            // A whole word overflowed, break until we can no longer break (or
+            // it fits).
+            if (line.startRunIndex == startRunIndex && line.startGlyphIndex == startBreakIndex)
+            {
+                bool canBreakMore = true;
+                while (canBreakMore && x > limit)
+                {
+
+                    RunIterator lineStart =
+                        RunIterator(runs, runs.begin() + line.startRunIndex, line.startGlyphIndex);
+                    RunIterator lineEnd = RunIterator(runs, end.run, end.run->breaks[end.index]);
+                    // Look for the next character that doesn't overflow.
+                    while (true)
+                    {
+                        if (!lineEnd.back())
+                        {
+                            // Hit the start of the text, can't go back.
+                            canBreakMore = false;
+                            break;
+                        }
+                        else if (lineEnd.x() <= limit)
+                        {
+                            if (lineStart == lineEnd && !lineEnd.forward())
+                            {
+                                // Hit the start of the line and could not
+                                // go forward to consume a single character.
+                                // We can't break any further.
+                                canBreakMore = false;
+                            }
+                            else
+                            {
+                                line.endRunIndex = (uint32_t)(lineEnd.run() - runs.begin());
+                                line.endGlyphIndex = lineEnd.index();
+                            }
+                            break;
+                        }
+                    }
+                    if (canBreakMore)
+                    {
+                        // Add the line and push the limit out.
+                        limit = lineEnd.x() + maxLineWidth;
+                        if (!line.empty())
+                        {
+                            lines.add(line);
+                        }
+                        // Setup the next line.
+                        line = GlyphLine((uint32_t)(lineEnd.run() - runs.begin()), lineEnd.index());
+                    }
+                }
+            }
+            else
+            {
+                // word overflowed, knock it to a new line
+                auto startX = start.run->xpos[start.run->breaks[start.index]];
+                limit = startX + maxLineWidth;
+
+                if (!line.empty() || start.index - lastEndGlyphIndex > 1)
+                {
+                    lines.add(line);
+                }
+
+                line = GlyphLine(startRunIndex, startBreakIndex);
+            }
+        }
+        else
+        {
+            line.endRunIndex = (uint32_t)(end.run - runs.begin());
+            line.endGlyphIndex = end.run->breaks[end.index];
+            advanceWord = true;
+            // Forced BR.
+            if (isForcedBreak)
+            {
+                lines.add(line);
+                auto startX = start.run->xpos[start.run->breaks[start.index] + 1];
+                limit = startX + maxLineWidth;
+                line = GlyphLine((uint32_t)(start.run - runs.begin()), startBreakIndex + 1);
+            }
+        }
+    }
+
+    // Add the last line.
+    if (!line.empty())
+    {
+        lines.add(line);
+    }
+
+    return lines;
+}
diff --git a/src/text/raw_text.cpp b/src/text/raw_text.cpp
new file mode 100644
index 0000000..abc7bba
--- /dev/null
+++ b/src/text/raw_text.cpp
@@ -0,0 +1,344 @@
+#ifdef WITH_RIVE_TEXT
+#include "rive/text/raw_text.hpp"
+#include "rive/text_engine.hpp"
+#include "rive/factory.hpp"
+
+using namespace rive;
+
+RawText::RawText(Factory* factory) : m_factory(factory) {}
+bool RawText::empty() const { return m_styled.empty(); }
+
+void RawText::append(const std::string& text,
+                     rcp<RenderPaint> paint,
+                     rcp<Font> font,
+                     float size,
+                     float lineHeight,
+                     float letterSpacing)
+{
+    int styleIndex = 0;
+    for (RenderStyle& style : m_styles)
+    {
+        if (style.paint == paint)
+        {
+            break;
+        }
+        styleIndex++;
+    }
+    if (styleIndex == m_styles.size())
+    {
+        m_styles.push_back({paint, m_factory->makeEmptyRenderPath(), true});
+    }
+    m_styled.append(font, size, lineHeight, letterSpacing, text, styleIndex);
+    m_dirty = true;
+}
+
+void RawText::clear()
+{
+    m_styled.clear();
+    m_dirty = true;
+}
+
+TextSizing RawText::sizing() const { return m_sizing; }
+
+TextOverflow RawText::overflow() const { return m_overflow; }
+
+TextAlign RawText::align() const { return m_align; }
+
+float RawText::maxWidth() const { return m_maxWidth; }
+
+float RawText::maxHeight() const { return m_maxHeight; }
+
+float RawText::paragraphSpacing() const { return m_paragraphSpacing; }
+
+void RawText::sizing(TextSizing value)
+{
+    if (m_sizing != value)
+    {
+        m_sizing = value;
+        m_dirty = true;
+    }
+}
+
+void RawText::overflow(TextOverflow value)
+{
+    if (m_overflow != value)
+    {
+        m_overflow = value;
+        m_dirty = true;
+    }
+}
+
+void RawText::align(TextAlign value)
+{
+    if (m_align != value)
+    {
+        m_align = value;
+        m_dirty = true;
+    }
+}
+
+void RawText::paragraphSpacing(float value)
+{
+    if (m_paragraphSpacing != value)
+    {
+        m_paragraphSpacing = value;
+        m_dirty = true;
+    }
+}
+
+void RawText::maxWidth(float value)
+{
+    if (m_maxWidth != value)
+    {
+        m_maxWidth = value;
+        m_dirty = true;
+    }
+}
+
+void RawText::maxHeight(float value)
+{
+    if (m_maxHeight != value)
+    {
+        m_maxHeight = value;
+        m_dirty = true;
+    }
+}
+
+void RawText::update()
+{
+    for (RenderStyle& style : m_styles)
+    {
+        style.path->rewind();
+        style.isEmpty = true;
+    }
+    m_renderStyles.clear();
+    if (m_styled.empty())
+    {
+        return;
+    }
+    auto runs = m_styled.runs();
+    m_shape = runs[0].font->shapeText(m_styled.unichars(), runs);
+    m_lines =
+        Text::BreakLines(m_shape, m_sizing == TextSizing::autoWidth ? -1.0f : m_maxWidth, m_align);
+
+    m_orderedLines.clear();
+    m_ellipsisRun = {};
+
+    // build render styles.
+    if (m_shape.empty())
+    {
+        m_bounds = AABB(0.0f, 0.0f, 0.0f, 0.0f);
+        return;
+    }
+
+    // Build up ordered runs as we go.
+    int paragraphIndex = 0;
+    float y = 0.0f;
+    float minY = 0.0f;
+    float measuredWidth = 0.0f;
+    if (m_origin == TextOrigin::baseline && !m_lines.empty() && !m_lines[0].empty())
+    {
+        y -= m_lines[0][0].baseline;
+        minY = y;
+    }
+
+    int ellipsisLine = -1;
+    bool isEllipsisLineLast = false;
+    // Find the line to put the ellipsis on (line before the one that
+    // overflows).
+    bool wantEllipsis = m_overflow == TextOverflow::ellipsis && m_sizing == TextSizing::fixed;
+
+    int lastLineIndex = -1;
+    for (const SimpleArray<GlyphLine>& paragraphLines : m_lines)
+    {
+        const Paragraph& paragraph = m_shape[paragraphIndex++];
+        for (const GlyphLine& line : paragraphLines)
+        {
+            const GlyphRun& endRun = paragraph.runs[line.endRunIndex];
+            const GlyphRun& startRun = paragraph.runs[line.startRunIndex];
+            float width = endRun.xpos[line.endGlyphIndex] - startRun.xpos[line.startGlyphIndex] -
+                          endRun.letterSpacing;
+            if (width > measuredWidth)
+            {
+                measuredWidth = width;
+            }
+            lastLineIndex++;
+            if (wantEllipsis && y + line.bottom <= m_maxHeight)
+            {
+                ellipsisLine++;
+            }
+        }
+
+        if (!paragraphLines.empty())
+        {
+            y += paragraphLines.back().bottom;
+        }
+        y += m_paragraphSpacing;
+    }
+    if (wantEllipsis && ellipsisLine == -1)
+    {
+        // Nothing fits, just show the first line and ellipse it.
+        ellipsisLine = 0;
+    }
+    isEllipsisLineLast = lastLineIndex == ellipsisLine;
+
+    int lineIndex = 0;
+    paragraphIndex = 0;
+    switch (m_sizing)
+    {
+        case TextSizing::autoWidth:
+            m_bounds = AABB(0.0f, minY, measuredWidth, std::max(minY, y - m_paragraphSpacing));
+            break;
+        case TextSizing::autoHeight:
+            m_bounds = AABB(0.0f, minY, m_maxWidth, std::max(minY, y - m_paragraphSpacing));
+            break;
+        case TextSizing::fixed:
+            m_bounds = AABB(0.0f, minY, m_maxWidth, minY + m_maxHeight);
+            break;
+    }
+
+    // Build the clip path if we want it.
+    if (m_overflow == TextOverflow::clipped)
+    {
+        if (m_clipRenderPath == nullptr)
+        {
+            m_clipRenderPath = m_factory->makeEmptyRenderPath();
+        }
+        else
+        {
+            m_clipRenderPath->rewind();
+        }
+
+        m_clipRenderPath->addRect(m_bounds.minX,
+                                  m_bounds.minY,
+                                  m_bounds.width(),
+                                  m_bounds.height());
+    }
+    else
+    {
+        m_clipRenderPath = nullptr;
+    }
+
+    y = 0;
+    if (m_origin == TextOrigin::baseline && !m_lines.empty() && !m_lines[0].empty())
+    {
+        y -= m_lines[0][0].baseline;
+    }
+    paragraphIndex = 0;
+
+    for (const SimpleArray<GlyphLine>& paragraphLines : m_lines)
+    {
+        const Paragraph& paragraph = m_shape[paragraphIndex++];
+        for (const GlyphLine& line : paragraphLines)
+        {
+            switch (m_overflow)
+            {
+                case TextOverflow::hidden:
+                    if (m_sizing == TextSizing::fixed && y + line.bottom > m_maxHeight)
+                    {
+                        return;
+                    }
+                    break;
+                case TextOverflow::clipped:
+                    if (m_sizing == TextSizing::fixed && y + line.top > m_maxHeight)
+                    {
+                        return;
+                    }
+                    break;
+                default:
+                    break;
+            }
+
+            if (lineIndex >= m_orderedLines.size())
+            {
+                // We need to still compute this line's ordered runs.
+                m_orderedLines.emplace_back(OrderedLine(paragraph,
+                                                        line,
+                                                        m_maxWidth,
+                                                        ellipsisLine == lineIndex,
+                                                        isEllipsisLineLast,
+                                                        &m_ellipsisRun));
+            }
+
+            const OrderedLine& orderedLine = m_orderedLines[lineIndex];
+            float x = line.startX;
+            float renderY = y + line.baseline;
+            for (auto glyphItr : orderedLine)
+            {
+                const GlyphRun* run = std::get<0>(glyphItr);
+                size_t glyphIndex = std::get<1>(glyphItr);
+
+                const Font* font = run->font.get();
+                const Vec2D& offset = run->offsets[glyphIndex];
+
+                GlyphID glyphId = run->glyphs[glyphIndex];
+                float advance = run->advances[glyphIndex];
+
+                RawPath path = font->getPath(glyphId);
+
+                path.transformInPlace(
+                    Mat2D(run->size, 0.0f, 0.0f, run->size, x + offset.x, renderY + offset.y));
+
+                x += advance;
+
+                assert(run->styleId < m_styles.size());
+                RenderStyle* style = &m_styles[run->styleId];
+                assert(style != nullptr);
+                path.addTo(style->path.get());
+
+                if (style->isEmpty)
+                {
+                    // This was the first path added to the style, so let's mark
+                    // it in our draw list.
+                    style->isEmpty = false;
+
+                    m_renderStyles.push_back(style);
+                }
+            }
+            if (lineIndex == ellipsisLine)
+            {
+                return;
+            }
+            lineIndex++;
+        }
+        if (!paragraphLines.empty())
+        {
+            y += paragraphLines.back().bottom;
+        }
+        y += m_paragraphSpacing;
+    }
+}
+
+AABB RawText::bounds()
+{
+    if (m_dirty)
+    {
+        update();
+        m_dirty = false;
+    }
+    return m_bounds;
+}
+
+void RawText::render(Renderer* renderer, rcp<RenderPaint> paint)
+{
+    if (m_dirty)
+    {
+        update();
+        m_dirty = false;
+    }
+
+    if (m_overflow == TextOverflow::clipped && m_clipRenderPath)
+    {
+        renderer->save();
+        renderer->clipPath(m_clipRenderPath.get());
+    }
+    for (auto style : m_renderStyles)
+    {
+        renderer->drawPath(style->path.get(), paint ? paint.get() : style->paint.get());
+    }
+    if (m_overflow == TextOverflow::clipped && m_clipRenderPath)
+    {
+        renderer->restore();
+    }
+}
+#endif
diff --git a/src/text/text.cpp b/src/text/text.cpp
new file mode 100644
index 0000000..03a47ef
--- /dev/null
+++ b/src/text/text.cpp
@@ -0,0 +1,914 @@
+#include "rive/text/text.hpp"
+using namespace rive;
+#ifdef WITH_RIVE_TEXT
+#include "rive/text_engine.hpp"
+#include "rive/component_dirt.hpp"
+#include "rive/text/utf.hpp"
+#include "rive/text/text_style.hpp"
+#include "rive/text/text_value_run.hpp"
+#include "rive/text/text_modifier_group.hpp"
+#include "rive/shapes/paint/shape_paint.hpp"
+#include "rive/artboard.hpp"
+#include "rive/factory.hpp"
+#include "rive/clip_result.hpp"
+#include <limits>
+
+void GlyphItr::tryAdvanceRun()
+{
+    while (true)
+    {
+        auto run = *m_run;
+        if (m_glyphIndex == m_line->endGlyphIndex(run) && run != m_line->lastRun())
+        {
+            m_run++;
+            m_glyphIndex = m_line->startGlyphIndex(*m_run);
+        }
+        else
+        {
+            break;
+        }
+    }
+}
+GlyphItr& GlyphItr::operator++()
+{
+    auto run = *m_run;
+    m_glyphIndex += run->dir == TextDirection::ltr ? 1 : -1;
+    tryAdvanceRun();
+    return *this;
+}
+
+OrderedLine::OrderedLine(const Paragraph& paragraph,
+                         const GlyphLine& line,
+                         float lineWidth,
+                         bool wantEllipsis,
+                         bool isEllipsisLineLast,
+                         GlyphRun* ellipsisRun) :
+    m_startGlyphIndex(line.startGlyphIndex), m_endGlyphIndex(line.endGlyphIndex)
+{
+    std::vector<const GlyphRun*> logicalRuns;
+    const SimpleArray<GlyphRun>& glyphRuns = paragraph.runs;
+
+    if (!wantEllipsis || !buildEllipsisRuns(logicalRuns,
+                                            paragraph,
+                                            line,
+                                            lineWidth,
+                                            isEllipsisLineLast,
+                                            ellipsisRun))
+    {
+        for (uint32_t i = line.startRunIndex; i < line.endRunIndex + 1; i++)
+        {
+            logicalRuns.push_back(&glyphRuns[i]);
+        }
+
+        if (!logicalRuns.empty())
+        {
+            m_startLogical = logicalRuns.front();
+            m_endLogical = logicalRuns.back();
+        }
+    }
+
+    // Now sort the runs visually.
+    if (paragraph.baseDirection == TextDirection::ltr || logicalRuns.empty())
+    {
+        m_runs = std::move(logicalRuns);
+    }
+    else
+    {
+        std::vector<const GlyphRun*> visualRuns;
+        visualRuns.reserve(logicalRuns.size());
+
+        auto itr = logicalRuns.rbegin();
+        auto end = logicalRuns.rend();
+        const GlyphRun* first = *itr;
+        visualRuns.push_back(first);
+        size_t ltrIndex = 0;
+        TextDirection previousDirection = first->dir;
+        while (++itr != end)
+        {
+            const GlyphRun* run = *itr;
+            if (run->dir == TextDirection::ltr && previousDirection == run->dir)
+            {
+                visualRuns.insert(visualRuns.begin() + ltrIndex, run);
+            }
+            else
+            {
+                if (run->dir == TextDirection::ltr)
+                {
+                    ltrIndex = visualRuns.size();
+                }
+                visualRuns.push_back(run);
+            }
+            previousDirection = run->dir;
+        }
+        m_runs = std::move(visualRuns);
+    }
+}
+
+static void appendUnicode(std::vector<rive::Unichar>& unichars, const char text[])
+{
+    const uint8_t* ptr = (const uint8_t*)text;
+    while (*ptr)
+    {
+        unichars.push_back(rive::UTF::NextUTF8(&ptr));
+    }
+}
+
+bool OrderedLine::buildEllipsisRuns(std::vector<const GlyphRun*>& logicalRuns,
+                                    const Paragraph& paragraph,
+                                    const GlyphLine& line,
+                                    float lineWidth,
+                                    bool isEllipsisLineLast,
+                                    GlyphRun* storedEllipsisRun)
+{
+    float x = 0.0f;
+    const SimpleArray<GlyphRun>& glyphRuns = paragraph.runs;
+    uint32_t startGIndex = line.startGlyphIndex;
+    // If it's the last line we can actually early out if the whole things fits,
+    // so check that first with no extra shaping.
+    if (isEllipsisLineLast)
+    {
+        bool fits = true;
+
+        for (uint32_t i = line.startRunIndex; i < line.endRunIndex + 1; i++)
+        {
+            const GlyphRun& run = glyphRuns[i];
+            uint32_t endGIndex =
+                i == line.endRunIndex ? line.endGlyphIndex : (uint32_t)run.glyphs.size();
+
+            for (uint32_t j = startGIndex; j != endGIndex; j++)
+            {
+                x += run.advances[j];
+                if (x > lineWidth)
+                {
+                    fits = false;
+                    goto measured;
+                }
+            }
+            startGIndex = 0;
+        }
+    measured:
+        if (fits)
+        {
+            // It fits, just get the regular glyphs.
+            return false;
+        }
+    }
+
+    std::vector<Unichar> ellipsisCodePoints;
+    appendUnicode(ellipsisCodePoints, "...");
+
+    rcp<Font> ellipsisFont = nullptr;
+    float ellipsisFontSize = 0.0f;
+
+    GlyphRun ellipsisRun = {};
+    float ellipsisWidth = 0.0f;
+
+    bool ellipsisOverflowed = false;
+    startGIndex = line.startGlyphIndex;
+
+    for (uint32_t i = line.startRunIndex; i < line.endRunIndex + 1; i++)
+    {
+        const GlyphRun& run = glyphRuns[i];
+        if (run.font != ellipsisFont && run.size != ellipsisFontSize)
+        {
+            // Track the latest we've checked (even if we discard it so we don't try
+            // to do this again for this ellipsis).
+            ellipsisFont = run.font;
+            ellipsisFontSize = run.size;
+
+            // Get the next shape so we can check if it fits, otherwise keep using
+            // the last one.
+            TextRun ellipsisRuns[] = {{ellipsisFont,
+                                       ellipsisFontSize,
+                                       run.lineHeight,
+                                       run.letterSpacing,
+                                       (uint32_t)ellipsisCodePoints.size()}};
+            auto nextEllipsisShape =
+                ellipsisFont->shapeText(ellipsisCodePoints, Span<TextRun>(ellipsisRuns, 1));
+
+            // Hard assumption one run and para
+            const Paragraph& para = nextEllipsisShape[0];
+            const GlyphRun& nextEllipsisRun = para.runs.front();
+
+            float nextEllipsisWidth = 0;
+            for (size_t j = 0; j < nextEllipsisRun.glyphs.size(); j++)
+            {
+                nextEllipsisWidth += nextEllipsisRun.advances[j];
+            }
+
+            if (ellipsisRun.font == nullptr || x + nextEllipsisWidth <= lineWidth)
+            {
+                // This ellipsis still fits, go ahead and use it. Otherwise stick with
+                // the old one.
+                ellipsisWidth = nextEllipsisWidth;
+                ellipsisRun = std::move(para.runs.front());
+            }
+        }
+
+        uint32_t endGIndex =
+            i == line.endRunIndex ? line.endGlyphIndex : (uint32_t)run.glyphs.size();
+        for (uint32_t j = startGIndex; j != endGIndex; j++)
+        {
+            float advance = run.advances[j];
+            if (x + advance + ellipsisWidth > lineWidth)
+            {
+                m_endGlyphIndex = j;
+                ellipsisOverflowed = true;
+                break;
+            }
+            x += advance;
+        }
+        startGIndex = 0;
+        logicalRuns.push_back(&run);
+        m_endLogical = &run;
+
+        if (ellipsisOverflowed && ellipsisRun.font != nullptr)
+        {
+            *storedEllipsisRun = std::move(ellipsisRun);
+            logicalRuns.push_back(storedEllipsisRun);
+            break;
+        }
+    }
+
+    // There was enough space for it, so let's add the ellipsis (if we didn't
+    // already). Note that we already checked if this is the last line and found
+    // that the whole text didn't fit.
+    if (!ellipsisOverflowed && ellipsisRun.font != nullptr)
+    {
+        *storedEllipsisRun = std::move(ellipsisRun);
+        logicalRuns.push_back(storedEllipsisRun);
+    }
+    m_startLogical = storedEllipsisRun == logicalRuns.front() ? nullptr : logicalRuns.front();
+    return true;
+}
+
+Vec2D Text::measureLayout(float width,
+                          LayoutMeasureMode widthMode,
+                          float height,
+                          LayoutMeasureMode heightMode)
+{
+    return measure(Vec2D(
+        widthMode == LayoutMeasureMode::undefined ? std::numeric_limits<float>::max() : width,
+        heightMode == LayoutMeasureMode::undefined ? std::numeric_limits<float>::max() : height));
+}
+
+void Text::controlSize(Vec2D size)
+{
+    if (m_layoutWidth != size.x || m_layoutHeight != size.y)
+    {
+        m_layoutWidth = size.x;
+        m_layoutHeight = size.y;
+        markShapeDirty(false);
+    }
+}
+
+void Text::buildRenderStyles()
+{
+    for (TextStyle* style : m_renderStyles)
+    {
+        style->rewindPath();
+    }
+    m_renderStyles.clear();
+    if (m_shape.empty())
+    {
+        m_bounds = AABB(0.0f, 0.0f, 0.0f, 0.0f);
+        return;
+    }
+    const float paragraphSpace = paragraphSpacing();
+
+    // Build up ordered runs as we go.
+    int paragraphIndex = 0;
+    float y = 0.0f;
+    float minY = 0.0f;
+    float maxWidth = 0.0f;
+    if (textOrigin() == TextOrigin::baseline && !m_lines.empty() && !m_lines[0].empty())
+    {
+        y -= m_lines[0][0].baseline;
+        minY = y;
+    }
+
+    int ellipsisLine = -1;
+    bool isEllipsisLineLast = false;
+    // Find the line to put the ellipsis on (line before the one that
+    // overflows).
+    bool wantEllipsis =
+        overflow() == TextOverflow::ellipsis && effectiveSizing() == TextSizing::fixed;
+
+    int lastLineIndex = -1;
+    for (const SimpleArray<GlyphLine>& paragraphLines : m_lines)
+    {
+        const Paragraph& paragraph = m_shape[paragraphIndex++];
+        for (const GlyphLine& line : paragraphLines)
+        {
+            const GlyphRun& endRun = paragraph.runs[line.endRunIndex];
+            const GlyphRun& startRun = paragraph.runs[line.startRunIndex];
+            float width = endRun.xpos[line.endGlyphIndex] - startRun.xpos[line.startGlyphIndex] -
+                          endRun.letterSpacing;
+            if (width > maxWidth)
+            {
+                maxWidth = width;
+            }
+            lastLineIndex++;
+            if (wantEllipsis && y + line.bottom <= effectiveHeight())
+            {
+                ellipsisLine++;
+            }
+        }
+
+        if (!paragraphLines.empty())
+        {
+            y += paragraphLines.back().bottom;
+        }
+        y += paragraphSpace;
+    }
+    if (wantEllipsis && ellipsisLine == -1)
+    {
+        // Nothing fits, just show the first line and ellipse it.
+        ellipsisLine = 0;
+    }
+    isEllipsisLineLast = lastLineIndex == ellipsisLine;
+
+    int lineIndex = 0;
+    paragraphIndex = 0;
+    switch (effectiveSizing())
+    {
+        case TextSizing::autoWidth:
+            m_bounds = AABB(0.0f, minY, maxWidth, std::max(minY, y - paragraphSpace));
+            break;
+        case TextSizing::autoHeight:
+            m_bounds = AABB(0.0f, minY, effectiveWidth(), std::max(minY, y - paragraphSpace));
+            break;
+        case TextSizing::fixed:
+            m_bounds = AABB(0.0f, minY, effectiveWidth(), minY + effectiveHeight());
+            break;
+    }
+
+    // Build the clip path if we want it.
+    if (overflow() == TextOverflow::clipped)
+    {
+        if (m_clipRenderPath == nullptr)
+        {
+            m_clipRenderPath = artboard()->factory()->makeEmptyRenderPath();
+        }
+        else
+        {
+            m_clipRenderPath->rewind();
+        }
+
+        AABB bounds = localBounds();
+
+        m_clipRenderPath->addRect(bounds.minX, bounds.minY, bounds.width(), bounds.height());
+    }
+    else
+    {
+        m_clipRenderPath = nullptr;
+    }
+
+    y = -m_bounds.height() * originY();
+    if (textOrigin() == TextOrigin::baseline && !m_lines.empty() && !m_lines[0].empty())
+    {
+        y -= m_lines[0][0].baseline;
+    }
+    paragraphIndex = 0;
+
+    bool hasModifiers = haveModifiers();
+    if (hasModifiers)
+    {
+        uint32_t textSize = (uint32_t)m_styledText.unichars().size();
+        for (TextModifierGroup* modifierGroup : m_modifierGroups)
+        {
+            modifierGroup->computeCoverage(textSize);
+        }
+    }
+    for (const SimpleArray<GlyphLine>& paragraphLines : m_lines)
+    {
+        const Paragraph& paragraph = m_shape[paragraphIndex++];
+        for (const GlyphLine& line : paragraphLines)
+        {
+            switch (overflow())
+            {
+                case TextOverflow::hidden:
+                    if (effectiveSizing() == TextSizing::fixed &&
+                        y + line.bottom > effectiveHeight())
+                    {
+                        return;
+                    }
+                    break;
+                case TextOverflow::clipped:
+                    if (effectiveSizing() == TextSizing::fixed && y + line.top > effectiveHeight())
+                    {
+                        return;
+                    }
+                    break;
+                default:
+                    break;
+            }
+
+            if (lineIndex >= m_orderedLines.size())
+            {
+                // We need to still compute this line's ordered runs.
+                m_orderedLines.emplace_back(OrderedLine(paragraph,
+                                                        line,
+                                                        effectiveWidth(),
+                                                        ellipsisLine == lineIndex,
+                                                        isEllipsisLineLast,
+                                                        &m_ellipsisRun));
+            }
+
+            const OrderedLine& orderedLine = m_orderedLines[lineIndex];
+            float x = -m_bounds.width() * originX() + line.startX;
+            float renderY = y + line.baseline;
+            for (auto glyphItr : orderedLine)
+            {
+                const GlyphRun* run = std::get<0>(glyphItr);
+                size_t glyphIndex = std::get<1>(glyphItr);
+
+                const Font* font = run->font.get();
+                const Vec2D& offset = run->offsets[glyphIndex];
+
+                GlyphID glyphId = run->glyphs[glyphIndex];
+                float advance = run->advances[glyphIndex];
+
+                RawPath path = font->getPath(glyphId);
+
+                uint32_t textIndex = 0;
+                uint32_t glyphCount = 0;
+                if (hasModifiers)
+                {
+                    textIndex = run->textIndices[glyphIndex];
+                    glyphCount = m_glyphLookup.count(textIndex);
+
+                    float centerX = advance / 2.0f;
+                    Mat2D transform =
+                        Mat2D::fromScaleAndTranslation(run->size, run->size, -centerX, 0.0f);
+                    for (TextModifierGroup* modifierGroup : m_modifierGroups)
+                    {
+                        float coverage = modifierGroup->glyphCoverage(textIndex, glyphCount);
+                        modifierGroup->transform(coverage, transform);
+                    }
+                    transform =
+                        Mat2D::fromTranslate(centerX + x + offset.x, y + line.baseline + offset.y) *
+                        transform;
+
+                    path.transformInPlace(transform);
+                }
+                else
+                {
+                    path.transformInPlace(
+                        Mat2D(run->size, 0.0f, 0.0f, run->size, x + offset.x, renderY + offset.y));
+                }
+
+                x += advance;
+
+                assert(run->styleId < m_runs.size());
+                TextStyle* style = m_runs[run->styleId]->style();
+                // TextValueRun::onAddedDirty botches loading if it cannot
+                // resolve a style, so we're confident we have a style here.
+                assert(style != nullptr);
+                // Consider this the "local" opacity.
+                float opacity = 1.0f;
+                if (hasModifiers)
+                {
+                    for (TextModifierGroup* modifierGroup : m_modifierGroups)
+                    {
+                        if (modifierGroup->modifiesOpacity())
+                        {
+                            float coverage = modifierGroup->glyphCoverage(textIndex, glyphCount);
+                            opacity = modifierGroup->computeOpacity(opacity, coverage);
+                        }
+                    }
+                }
+                if (style->addPath(path, opacity))
+                {
+                    // This was the first path added to the style, so let's mark
+                    // it in our draw list.
+                    m_renderStyles.push_back(style);
+                    style->propagateOpacity(renderOpacity());
+                }
+            }
+            if (lineIndex == ellipsisLine)
+            {
+                return;
+            }
+            lineIndex++;
+        }
+        if (!paragraphLines.empty())
+        {
+            y += paragraphLines.back().bottom;
+        }
+        y += paragraphSpace;
+    }
+}
+
+const TextStyle* Text::styleFromShaperId(uint16_t id) const
+{
+    assert(id < m_runs.size());
+    return m_runs[id]->style();
+}
+
+void Text::draw(Renderer* renderer)
+{
+    ClipResult clipResult = applyClip(renderer);
+    if (clipResult == ClipResult::noClip)
+    {
+        // We didn't clip, so make sure to save as we'll be doing some
+        // transformations.
+        renderer->save();
+    }
+    if (clipResult != ClipResult::emptyClip)
+    {
+        renderer->transform(m_WorldTransform);
+        if (overflow() == TextOverflow::clipped && m_clipRenderPath)
+        {
+            renderer->clipPath(m_clipRenderPath.get());
+        }
+        for (auto style : m_renderStyles)
+        {
+            style->draw(renderer);
+        }
+    }
+    renderer->restore();
+}
+
+void Text::addRun(TextValueRun* run) { m_runs.push_back(run); }
+
+void Text::addModifierGroup(TextModifierGroup* group) { m_modifierGroups.push_back(group); }
+
+void Text::markShapeDirty(bool sendToLayout)
+{
+    addDirt(ComponentDirt::Path);
+    for (TextModifierGroup* group : m_modifierGroups)
+    {
+        group->clearRangeMaps();
+    }
+    markWorldTransformDirty();
+#ifdef WITH_RIVE_LAYOUT
+    if (sendToLayout)
+    {
+        for (ContainerComponent* p = parent(); p != nullptr; p = p->parent())
+        {
+            if (p->is<LayoutComponent>())
+            {
+                p->as<LayoutComponent>()->markLayoutNodeDirty();
+            }
+        }
+    }
+#endif
+}
+
+void Text::modifierShapeDirty() { addDirt(ComponentDirt::Path); }
+
+void Text::markPaintDirty() { addDirt(ComponentDirt::Paint); }
+
+void Text::alignValueChanged() { markShapeDirty(); }
+
+void Text::sizingValueChanged() { markShapeDirty(); }
+
+void Text::overflowValueChanged()
+{
+    if (effectiveSizing() != TextSizing::autoWidth)
+    {
+        markShapeDirty();
+    }
+}
+
+void Text::widthChanged()
+{
+    if (effectiveSizing() != TextSizing::autoWidth)
+    {
+        markShapeDirty();
+    }
+}
+
+void Text::paragraphSpacingChanged() { markPaintDirty(); }
+
+void Text::heightChanged()
+{
+    if (effectiveSizing() == TextSizing::fixed)
+    {
+        markShapeDirty();
+    }
+}
+
+void StyledText::clear()
+{
+    m_value.clear();
+    m_runs.clear();
+}
+
+bool StyledText::empty() const { return m_runs.empty(); }
+
+void StyledText::append(rcp<Font> font,
+                        float size,
+                        float lineHeight,
+                        float letterSpacing,
+                        const std::string& text,
+                        uint16_t styleId)
+{
+    const uint8_t* ptr = (const uint8_t*)text.c_str();
+    uint32_t n = 0;
+    while (*ptr)
+    {
+        m_value.push_back(UTF::NextUTF8(&ptr));
+        n += 1;
+    }
+    m_runs.push_back({std::move(font), size, lineHeight, letterSpacing, n, 0, styleId});
+}
+
+bool Text::makeStyled(StyledText& styledText, bool withModifiers) const
+{
+    styledText.clear();
+    uint16_t runIndex = 0;
+    for (auto valueRun : m_runs)
+    {
+        auto style = valueRun->style();
+        const std::string& text = valueRun->text();
+        if (style == nullptr || style->font() == nullptr || text.empty())
+        {
+            runIndex++;
+            continue;
+        }
+        styledText.append(style->font(),
+                          style->fontSize(),
+                          style->lineHeight(),
+                          style->letterSpacing(),
+                          text,
+                          runIndex++);
+    }
+    if (withModifiers)
+    {
+        for (TextModifierGroup* group : m_modifierGroups)
+        {
+            group->applyShapeModifiers(*this, styledText);
+        }
+    }
+    return !styledText.empty();
+}
+
+SimpleArray<SimpleArray<GlyphLine>> Text::BreakLines(const SimpleArray<Paragraph>& paragraphs,
+                                                     float width,
+                                                     TextAlign align)
+{
+    bool autoWidth = width == -1.0f;
+    float paragraphWidth = width;
+
+    SimpleArray<SimpleArray<GlyphLine>> lines(paragraphs.size());
+
+    size_t paragraphIndex = 0;
+    for (auto& para : paragraphs)
+    {
+        lines[paragraphIndex] = GlyphLine::BreakLines(para.runs, autoWidth ? -1.0f : width);
+        if (autoWidth)
+        {
+            paragraphWidth = std::max(paragraphWidth,
+                                      GlyphLine::ComputeMaxWidth(lines[paragraphIndex], para.runs));
+        }
+        paragraphIndex++;
+    }
+    paragraphIndex = 0;
+    for (auto& para : paragraphs)
+    {
+        GlyphLine::ComputeLineSpacing(paragraphIndex == 0,
+                                      lines[paragraphIndex],
+                                      para.runs,
+                                      paragraphWidth,
+                                      align);
+        paragraphIndex++;
+    }
+    return lines;
+}
+
+bool Text::modifierRangesNeedShape() const
+{
+    for (const TextModifierGroup* modifierGroup : m_modifierGroups)
+    {
+        if (modifierGroup->needsShape())
+        {
+            return true;
+        }
+    }
+    return false;
+}
+
+void Text::update(ComponentDirt value)
+{
+    Super::update(value);
+
+    if (hasDirt(value, ComponentDirt::Path))
+    {
+        // We have modifiers that need shaping we'll need to compute the coverage
+        // right before we build the actual shape.
+        bool precomputeModifierCoverage = modifierRangesNeedShape();
+        if (precomputeModifierCoverage)
+        {
+            makeStyled(m_modifierStyledText, false);
+            auto runs = m_modifierStyledText.runs();
+            m_modifierShape = runs[0].font->shapeText(m_modifierStyledText.unichars(), runs);
+            m_modifierLines =
+                BreakLines(m_modifierShape,
+                           effectiveSizing() == TextSizing::autoWidth ? -1.0f : effectiveWidth(),
+                           (TextAlign)alignValue());
+            m_glyphLookup.compute(m_modifierStyledText.unichars(), m_modifierShape);
+            uint32_t textSize = (uint32_t)m_modifierStyledText.unichars().size();
+            for (TextModifierGroup* group : m_modifierGroups)
+            {
+                group->computeRangeMap(m_modifierStyledText.unichars(),
+                                       m_modifierShape,
+                                       m_modifierLines,
+                                       m_glyphLookup);
+                group->computeCoverage(textSize);
+            }
+        }
+        if (makeStyled(m_styledText))
+        {
+            auto runs = m_styledText.runs();
+            m_shape = runs[0].font->shapeText(m_styledText.unichars(), runs);
+            m_lines =
+                BreakLines(m_shape,
+                           effectiveSizing() == TextSizing::autoWidth ? -1.0f : effectiveWidth(),
+                           (TextAlign)alignValue());
+            if (!precomputeModifierCoverage && haveModifiers())
+            {
+                m_glyphLookup.compute(m_styledText.unichars(), m_shape);
+                uint32_t textSize = (uint32_t)m_styledText.unichars().size();
+                for (TextModifierGroup* group : m_modifierGroups)
+                {
+                    group->computeRangeMap(m_styledText.unichars(),
+                                           m_shape,
+                                           m_lines,
+                                           m_glyphLookup);
+                    group->computeCoverage(textSize);
+                }
+            }
+        }
+        else
+        {
+            m_shape = SimpleArray<Paragraph>();
+            m_lines = SimpleArray<SimpleArray<GlyphLine>>();
+            m_glyphLookup.clear();
+        }
+        m_orderedLines.clear();
+        m_ellipsisRun = {};
+
+        // Immediately build render styles so dimensions get computed.
+        buildRenderStyles();
+    }
+    else if (hasDirt(value, ComponentDirt::Paint))
+    {
+        buildRenderStyles();
+    }
+    else if (hasDirt(value, ComponentDirt::RenderOpacity))
+    {
+        // Note that buildRenderStyles does this too, which is why we can get
+        // away doing this in the else.
+        for (TextStyle* style : m_renderStyles)
+        {
+            style->propagateOpacity(renderOpacity());
+        }
+    }
+}
+
+Vec2D Text::measure(Vec2D maxSize)
+{
+    if (makeStyled(m_styledText))
+    {
+        const float paragraphSpace = paragraphSpacing();
+        auto runs = m_styledText.runs();
+        auto shape = runs[0].font->shapeText(m_styledText.unichars(), runs);
+        auto lines = BreakLines(shape,
+                                std::min(maxSize.x,
+                                         sizing() == TextSizing::autoWidth
+                                             ? std::numeric_limits<float>::max()
+                                             : width()),
+                                (TextAlign)alignValue());
+        float y = 0;
+        float computedHeight = 0.0f;
+        float minY = 0;
+        int paragraphIndex = 0;
+        float maxWidth = 0;
+
+        if (textOrigin() == TextOrigin::baseline && !lines.empty() && !lines[0].empty())
+        {
+            y -= m_lines[0][0].baseline;
+            minY = y;
+        }
+        int ellipsisLine = -1;
+        bool wantEllipsis = overflow() == TextOverflow::ellipsis;
+
+        for (const SimpleArray<GlyphLine>& paragraphLines : lines)
+        {
+            const Paragraph& paragraph = shape[paragraphIndex++];
+            for (const GlyphLine& line : paragraphLines)
+            {
+                const GlyphRun& endRun = paragraph.runs[line.endRunIndex];
+                const GlyphRun& startRun = paragraph.runs[line.startRunIndex];
+                float width = endRun.xpos[line.endGlyphIndex] -
+                              startRun.xpos[line.startGlyphIndex] - endRun.letterSpacing;
+                if (width > maxWidth)
+                {
+                    maxWidth = width;
+                }
+                if (wantEllipsis && y + line.bottom > maxSize.y)
+                {
+                    if (ellipsisLine == -1)
+                    {
+                        // Nothing fits, just show the first line and ellipse it.
+                        computedHeight = y + line.bottom;
+                    }
+                    goto doneMeasuring;
+                }
+                ellipsisLine++;
+                computedHeight = y + line.bottom;
+            }
+            if (!paragraphLines.empty())
+            {
+                y += paragraphLines.back().bottom;
+            }
+            y += paragraphSpace;
+        }
+    doneMeasuring:
+
+        switch (sizing())
+        {
+            case TextSizing::autoWidth:
+                return Vec2D(maxWidth, std::max(minY, computedHeight));
+                break;
+            case TextSizing::autoHeight:
+                return Vec2D(width(), std::max(minY, computedHeight));
+                break;
+            case TextSizing::fixed:
+                return Vec2D(width(), minY + height());
+                break;
+        }
+    }
+    return Vec2D();
+}
+
+AABB Text::localBounds() const
+{
+    float width = m_bounds.width();
+    float height = m_bounds.height();
+    return AABB::fromLTWH(m_bounds.minX - width * originX(),
+                          m_bounds.minY - height * originY(),
+                          width,
+                          height);
+}
+
+Core* Text::hitTest(HitInfo*, const Mat2D&)
+{
+    if (renderOpacity() == 0.0f)
+    {
+        return nullptr;
+    }
+
+    return nullptr;
+}
+
+void Text::originValueChanged()
+{
+    markPaintDirty();
+    markWorldTransformDirty();
+}
+
+void Text::originXChanged()
+{
+    markPaintDirty();
+    markWorldTransformDirty();
+}
+void Text::originYChanged()
+{
+    markPaintDirty();
+    markWorldTransformDirty();
+}
+
+#else
+// Text disabled.
+void Text::draw(Renderer* renderer) {}
+Core* Text::hitTest(HitInfo*, const Mat2D&) { return nullptr; }
+void Text::addRun(TextValueRun* run) {}
+void Text::addModifierGroup(TextModifierGroup* group) {}
+void Text::markShapeDirty(bool sendToLayout) {}
+void Text::update(ComponentDirt value) {}
+void Text::alignValueChanged() {}
+void Text::sizingValueChanged() {}
+void Text::overflowValueChanged() {}
+void Text::widthChanged() {}
+void Text::heightChanged() {}
+void Text::markPaintDirty() {}
+void Text::modifierShapeDirty() {}
+bool Text::modifierRangesNeedShape() const { return false; }
+const TextStyle* Text::styleFromShaperId(uint16_t id) const { return nullptr; }
+void Text::paragraphSpacingChanged() {}
+AABB Text::localBounds() const { return AABB(); }
+void Text::originValueChanged() {}
+void Text::originXChanged() {}
+void Text::originYChanged() {}
+Vec2D Text::measureLayout(float width,
+                          LayoutMeasureMode widthMode,
+                          float height,
+                          LayoutMeasureMode heightMode)
+{
+    return Vec2D();
+}
+void Text::controlSize(Vec2D size) {}
+#endif
\ No newline at end of file
diff --git a/src/text/text_modifier.cpp b/src/text/text_modifier.cpp
new file mode 100644
index 0000000..3583c8e
--- /dev/null
+++ b/src/text/text_modifier.cpp
@@ -0,0 +1,22 @@
+#include "rive/text/text.hpp"
+#include "rive/text/text_modifier.hpp"
+#include "rive/text/text_modifier_group.hpp"
+
+using namespace rive;
+
+StatusCode TextModifier::onAddedDirty(CoreContext* context)
+{
+    StatusCode code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+
+    if (parent() != nullptr && parent()->is<TextModifierGroup>())
+    {
+        parent()->as<TextModifierGroup>()->addModifier(this);
+        return StatusCode::Ok;
+    }
+
+    return StatusCode::MissingObject;
+}
\ No newline at end of file
diff --git a/src/text/text_modifier_group.cpp b/src/text/text_modifier_group.cpp
new file mode 100644
index 0000000..14778a5
--- /dev/null
+++ b/src/text/text_modifier_group.cpp
@@ -0,0 +1,304 @@
+#include "rive/text/text.hpp"
+#include "rive/text/text_modifier_group.hpp"
+#include "rive/text/text_shape_modifier.hpp"
+#include "rive/text/text_modifier_range.hpp"
+#include "rive/text/glyph_lookup.hpp"
+#include "rive/text/text_style.hpp"
+#include "rive/artboard.hpp"
+#include <limits>
+
+using namespace rive;
+
+StatusCode TextModifierGroup::onAddedDirty(CoreContext* context)
+{
+    StatusCode code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+
+    if (parent() != nullptr && parent()->is<Text>())
+    {
+        parent()->as<Text>()->addModifierGroup(this);
+        return StatusCode::Ok;
+    }
+
+    return StatusCode::MissingObject;
+}
+
+void TextModifierGroup::addModifierRange(TextModifierRange* range) { m_ranges.push_back(range); }
+
+void TextModifierGroup::addModifier(TextModifier* modifier)
+{
+    m_modifiers.push_back(modifier);
+    if (modifier->is<TextShapeModifier>())
+    {
+        m_shapeModifiers.push_back(modifier->as<TextShapeModifier>());
+    }
+}
+
+void TextModifierGroup::rangeTypeChanged()
+{
+    parent()->as<Text>()->modifierShapeDirty();
+    addDirt(ComponentDirt::TextCoverage);
+}
+
+void TextModifierGroup::shapeModifierChanged() { parent()->as<Text>()->markShapeDirty(); }
+
+void TextModifierGroup::rangeChanged()
+{
+    /// Marking shape dirty should only be done if this modifer group changes
+    /// shaping properties (for now we're just testing and we're hardcoding a
+    /// shaping change).
+    if (!m_shapeModifiers.empty())
+    {
+        parent()->as<Text>()->modifierShapeDirty();
+    }
+    else
+    {
+        parent()->as<Text>()->markPaintDirty();
+    }
+    addDirt(ComponentDirt::TextCoverage);
+}
+
+/// Clear any cached selector range maps so they can be recomputed after next
+/// shaping.
+void TextModifierGroup::clearRangeMaps()
+{
+    for (TextModifierRange* range : m_ranges)
+    {
+        range->clearRangeMap();
+    }
+    addDirt(ComponentDirt::TextCoverage);
+}
+
+void TextModifierGroup::computeRangeMap(Span<const Unichar> text,
+                                        const SimpleArray<Paragraph>& shape,
+                                        const SimpleArray<SimpleArray<GlyphLine>>& lines,
+                                        const GlyphLookup& glyphLookup)
+{
+    for (TextModifierRange* range : m_ranges)
+    {
+        range->computeRange(text, shape, lines, glyphLookup);
+    }
+}
+
+void TextModifierGroup::computeCoverage(uint32_t textSize)
+{
+    if (!hasDirt(ComponentDirt::TextCoverage))
+    {
+        return;
+    }
+
+    // Because we're not dependent on anything we need to reset our dirt
+    // ourselves. We're not in the DAG so we'll never get reset.
+    m_Dirt = ComponentDirt::None;
+
+    // When the text re-shapes, we udpate our coverage values.
+    m_coverage.resize(textSize);
+    std::fill(m_coverage.begin(), m_coverage.end(), 0);
+    for (TextModifierRange* range : m_ranges)
+    {
+        range->computeCoverage(m_coverage);
+    }
+}
+
+float TextModifierGroup::glyphCoverage(uint32_t textIndex, uint32_t codePointCount)
+{
+    assert(codePointCount >= 1);
+    float c = coverage(textIndex);
+
+    for (uint32_t i = 1; i < codePointCount; i++)
+    {
+        c += coverage(textIndex + i);
+    }
+    return c /= (float)codePointCount;
+}
+
+void TextModifierGroup::transform(float amount, Mat2D& ctm)
+{
+    if (amount == 0.0f || !modifiesTransform())
+    {
+        return;
+    }
+
+    float actualRotation = modifiesRotation() ? rotation() * amount : 0.0f;
+    Mat2D transform = actualRotation != 0.0f ? Mat2D::fromRotation(actualRotation) : Mat2D();
+    if (modifiesTranslation())
+    {
+        transform[4] = x() * amount;
+        transform[5] = y() * amount;
+    }
+    if (modifiesScale())
+    {
+        float iamount = 1.0f - amount;
+        transform.scaleByValues(iamount + scaleX() * amount, iamount + scaleY() * amount);
+    }
+    bool doesModifyOrigin = modifiesOrigin();
+    if (doesModifyOrigin)
+    {
+        ctm[4] += originX();
+        ctm[5] += originY();
+    }
+    ctm = transform * ctm;
+    if (doesModifyOrigin)
+    {
+        ctm[4] -= originX();
+        ctm[5] -= originY();
+    }
+}
+
+float TextModifierGroup::computeOpacity(float current, float t) const
+{
+    if ((modifierFlags() & (uint32_t)TextModifierFlags::invertOpacity) != 0)
+    {
+        return current * (1.0f - t) + opacity() * t;
+    }
+    else
+    {
+        return current * opacity() * t;
+    }
+}
+
+void TextModifierGroup::modifierFlagsChanged() { parent()->as<Text>()->markPaintDirty(); }
+void TextModifierGroup::originXChanged() { parent()->as<Text>()->markPaintDirty(); }
+void TextModifierGroup::originYChanged() { parent()->as<Text>()->markPaintDirty(); }
+void TextModifierGroup::opacityChanged() { parent()->as<Text>()->markPaintDirty(); }
+void TextModifierGroup::xChanged() { parent()->as<Text>()->markPaintDirty(); }
+void TextModifierGroup::yChanged() { parent()->as<Text>()->markPaintDirty(); }
+void TextModifierGroup::rotationChanged() { parent()->as<Text>()->markPaintDirty(); }
+void TextModifierGroup::scaleXChanged() { parent()->as<Text>()->markPaintDirty(); }
+void TextModifierGroup::scaleYChanged() { parent()->as<Text>()->markPaintDirty(); }
+
+static TextRun copyRun(const TextRun& source, uint32_t unicharCount)
+{
+    return {
+        source.font,
+        source.size,
+        source.lineHeight,
+        source.letterSpacing,
+        unicharCount,
+        source.script,
+        source.styleId,
+        source.dir,
+    };
+}
+
+TextRun TextModifierGroup::modifyShape(const Text& text, TextRun run, float strength)
+{
+    const TextStyle* style = text.styleFromShaperId(run.styleId);
+    if (style == nullptr || style->font() == nullptr)
+    {
+        return run;
+    }
+
+    Font* font = style->font().get();
+
+    std::unordered_map<uint32_t, float> variations;
+    float fontSize = run.size;
+    for (TextShapeModifier* shapeModifier : m_shapeModifiers)
+    {
+        fontSize = shapeModifier->modify(font, variations, fontSize, strength);
+    }
+
+    if (!variations.empty())
+    {
+        m_variationCoords.clear();
+        for (auto itr = variations.begin(); itr != variations.end(); itr++)
+        {
+            m_variationCoords.push_back({itr->first, itr->second});
+        }
+        m_variableFont = font->makeAtCoords(m_variationCoords);
+        return {
+            m_variableFont,
+            run.size,
+            run.lineHeight,
+            run.letterSpacing,
+            run.unicharCount,
+            run.script,
+            run.styleId,
+            run.dir,
+        };
+    }
+    else
+    {
+        m_variableFont = nullptr;
+    }
+    return run;
+}
+
+void TextModifierGroup::applyShapeModifiers(const Text& text, StyledText& styledText)
+{
+    if (m_shapeModifiers.empty())
+    {
+        return;
+    }
+    m_nextTextRuns.clear();
+    m_nextTextRuns.reserve(styledText.runs().size());
+
+    uint32_t index = 0;
+    float lastCoverage = std::numeric_limits<float>::max();
+    uint32_t extractRunIndex = 0;
+
+    for (const TextRun& run : styledText.runs())
+    {
+        // Split the run into sub-runs as necessary based on equal coverage
+        // values.
+        uint32_t end = index + run.unicharCount;
+
+        while (index < end)
+        {
+            float coverage = m_coverage[index];
+            if (coverage != lastCoverage)
+            {
+                if (index - extractRunIndex != 0)
+                {
+                    // Add new run from extractRunStart to index (exclusive)
+                    if (lastCoverage == 0.0f)
+                    {
+                        m_nextTextRuns.push_back(copyRun(run, index - extractRunIndex));
+                    }
+                    else
+                    {
+                        m_nextTextRuns.push_back(
+                            modifyShape(text, copyRun(run, index - extractRunIndex), lastCoverage));
+                    }
+                }
+                lastCoverage = coverage;
+                extractRunIndex = index;
+            }
+            index++;
+        }
+
+        assert(extractRunIndex != end);
+        // Add new run from extractRunStart to index (exclusive)
+        if (lastCoverage == 0.0f)
+        {
+            m_nextTextRuns.push_back(copyRun(run, end - extractRunIndex));
+        }
+        else
+        {
+            m_nextTextRuns.push_back(
+                modifyShape(text, copyRun(run, end - extractRunIndex), lastCoverage));
+        }
+        extractRunIndex = end;
+    }
+    styledText.swapRuns(m_nextTextRuns);
+    // return nextTextRuns;
+}
+
+bool TextModifierGroup::needsShape() const
+{
+    if (!m_shapeModifiers.empty())
+    {
+        return true;
+    }
+    for (const TextModifierRange* range : m_ranges)
+    {
+        if (range->needsShape())
+        {
+            return true;
+        }
+    }
+    return false;
+}
\ No newline at end of file
diff --git a/src/text/text_modifier_range.cpp b/src/text/text_modifier_range.cpp
new file mode 100644
index 0000000..a2c0a46
--- /dev/null
+++ b/src/text/text_modifier_range.cpp
@@ -0,0 +1,345 @@
+#include "rive/text/text.hpp"
+#include "rive/text/text_modifier_range.hpp"
+#include "rive/text/text_modifier_group.hpp"
+#include "rive/text/text_value_run.hpp"
+#include "rive/text/glyph_lookup.hpp"
+#include "rive/animation/cubic_interpolator_component.hpp"
+#include "rive/core_context.hpp"
+
+using namespace rive;
+
+StatusCode TextModifierRange::onAddedDirty(CoreContext* context)
+{
+    StatusCode code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+
+    if (runId() != Core::emptyId)
+    {
+        auto coreObject = context->resolve(runId());
+        if (coreObject == nullptr || !coreObject->is<TextValueRun>())
+        {
+            return StatusCode::MissingObject;
+        }
+        m_run = static_cast<TextValueRun*>(coreObject);
+    }
+
+    if (parent() != nullptr && parent()->is<TextModifierGroup>())
+    {
+        parent()->as<TextModifierGroup>()->addModifierRange(this);
+        return StatusCode::Ok;
+    }
+
+    return StatusCode::MissingObject;
+}
+
+void TextModifierRange::addChild(Component* component)
+{
+    Super::addChild(component);
+    if (component->is<CubicInterpolatorComponent>())
+    {
+        // mark this as the cubic interpolator.
+        m_interpolator = component->as<CubicInterpolatorComponent>();
+    }
+}
+
+void TextModifierRange::clearRangeMap() { m_rangeMapper.clear(); }
+
+void TextModifierRange::computeRange(Span<const Unichar> text,
+                                     const SimpleArray<Paragraph>& shape,
+                                     const SimpleArray<SimpleArray<GlyphLine>>& lines,
+                                     const GlyphLookup& glyphLookup)
+{
+    // Check if the range mapper is still valid.
+    if (!m_rangeMapper.empty())
+    {
+        return;
+    }
+    uint32_t start = 0;
+    uint32_t end = (uint32_t)text.size();
+    if (m_run != nullptr)
+    {
+        start = m_run->offset();
+        end = start + m_run->length();
+    }
+    switch (units())
+    {
+        case TextRangeUnits::charactersExcludingSpaces:
+            m_rangeMapper.fromCharacters(text, start, end, glyphLookup, true);
+            break;
+        case TextRangeUnits::words:
+            m_rangeMapper.fromWords(text, start, end);
+            break;
+        case TextRangeUnits::lines:
+            m_rangeMapper.fromLines(text, start, end, shape, lines, glyphLookup);
+            break;
+        default:
+            m_rangeMapper.fromCharacters(text, start, end, glyphLookup);
+            break;
+    }
+}
+
+float TextModifierRange::coverageAt(float t)
+{
+    float c = 0.0f;
+    if (m_indexTo >= m_indexFrom)
+    {
+        if (t < m_indexFrom || t > m_indexTo)
+        {
+            c = 0.0f;
+        }
+        else if (t < m_indexFalloffFrom)
+        {
+            float range = std::max(0.0f, m_indexFalloffFrom - m_indexFrom);
+            c = range == 0.0f ? 1.0f : std::max(0.0f, t - m_indexFrom) / range;
+            if (m_interpolator != nullptr)
+            {
+                c = m_interpolator->transform(c);
+            }
+        }
+        else if (t > m_indexFalloffTo)
+        {
+            float range = std::max(0.0f, m_indexTo - m_indexFalloffTo);
+            c = range == 0.0f ? 1.0f : 1.0f - std::min(1.0f, (t - m_indexFalloffTo) / range);
+            if (m_interpolator != nullptr)
+            {
+                c = m_interpolator->transform(c);
+            }
+        }
+        else
+        {
+            c = 1.0f;
+        }
+    }
+    return c;
+}
+
+void TextModifierRange::computeCoverage(Span<float> coverage)
+{
+    if (m_rangeMapper.empty())
+    {
+        return;
+    }
+
+    // Compute range mapped coverage.
+    uint32_t count = m_rangeMapper.unitCount();
+    switch (type())
+    {
+        case TextRangeType::unitIndex:
+            m_indexFrom = offsetModifyFrom();
+            m_indexTo = offsetModifyTo();
+            m_indexFalloffFrom = offsetFalloffFrom();
+            m_indexFalloffTo = offsetFalloffTo();
+            break;
+        case TextRangeType::percentage:
+            m_indexFrom = count * offsetModifyFrom();
+            m_indexTo = count * offsetModifyTo();
+            m_indexFalloffFrom = count * offsetFalloffFrom();
+            m_indexFalloffTo = count * offsetFalloffTo();
+            break;
+    }
+
+    TextRangeMode rangeMode = mode();
+    for (uint32_t unitIndex = 0; unitIndex < count; unitIndex++)
+    {
+        uint32_t unitLength = (uint32_t)m_rangeMapper.unitLength(unitIndex);
+        uint32_t characterIndex = m_rangeMapper.unitCharacterIndex(unitIndex);
+        float c = strength() * coverageAt(unitIndex + 0.5f);
+        for (uint32_t i = 0; i < unitLength; i++)
+        {
+            assert((characterIndex + i) < (uint32_t)coverage.size());
+            float current = coverage[characterIndex + i];
+            switch (rangeMode)
+            {
+                case TextRangeMode::add:
+                    current += c;
+                    break;
+                case TextRangeMode::subtract:
+                    current -= c;
+                    break;
+                case TextRangeMode::max:
+                    current = std::max(current, c);
+                    break;
+                case TextRangeMode::min:
+                    current = std::min(current, c);
+                    break;
+                case TextRangeMode::multiply:
+                    current *= c;
+                    break;
+                case TextRangeMode::difference:
+                    current = std::abs(current - c);
+                    break;
+            }
+            coverage[characterIndex + i] =
+                clamp() ? std::max(0.0f, std::min(1.0f, current)) : current;
+        }
+
+        // Set space between units coverage to 0.
+        if (unitIndex + 1 < m_rangeMapper.unitCharacterIndexCount())
+        {
+            uint32_t nextCharacterIndex = m_rangeMapper.unitCharacterIndex(unitIndex + 1);
+            for (uint32_t i = characterIndex + unitLength; i < nextCharacterIndex; i++)
+            {
+                coverage[i] = 0;
+            }
+        }
+    }
+}
+
+void TextModifierRange::modifyFromChanged() { parent()->as<TextModifierGroup>()->rangeChanged(); }
+void TextModifierRange::modifyToChanged() { parent()->as<TextModifierGroup>()->rangeChanged(); }
+void TextModifierRange::strengthChanged() { parent()->as<TextModifierGroup>()->rangeChanged(); }
+void TextModifierRange::unitsValueChanged()
+{
+    parent()->as<TextModifierGroup>()->rangeTypeChanged();
+}
+void TextModifierRange::typeValueChanged() { parent()->as<TextModifierGroup>()->rangeChanged(); }
+void TextModifierRange::modeValueChanged() { parent()->as<TextModifierGroup>()->rangeChanged(); }
+void TextModifierRange::clampChanged() { parent()->as<TextModifierGroup>()->rangeChanged(); }
+void TextModifierRange::falloffFromChanged() { parent()->as<TextModifierGroup>()->rangeChanged(); }
+void TextModifierRange::falloffToChanged() { parent()->as<TextModifierGroup>()->rangeChanged(); }
+void TextModifierRange::offsetChanged() { parent()->as<TextModifierGroup>()->rangeChanged(); }
+
+bool TextModifierRange::needsShape() const { return units() == TextRangeUnits::lines; }
+
+void RangeMapper::clear()
+{
+    m_unitCharacterIndices.clear();
+    m_unitLengths.clear();
+}
+
+float RangeMapper::unitToCharacterRange(float word) const
+{
+    if (m_unitCharacterIndices.empty())
+    {
+        return 0.0f;
+    }
+    float clampedUnit = std::min(std::max(word, 0.0f), (float)(m_unitCharacterIndices.size() - 1));
+    size_t intUnit = (size_t)clampedUnit;
+    float characters = (float)m_unitCharacterIndices[intUnit];
+    if (intUnit < m_unitLengths.size())
+    {
+        float fraction = clampedUnit - intUnit;
+        characters += m_unitLengths[intUnit] * fraction;
+    }
+    return characters;
+}
+
+void RangeMapper::addRange(uint32_t indexFrom,
+                           uint32_t indexTo,
+                           uint32_t startOffset,
+                           uint32_t endOffset)
+{
+    if (indexTo > startOffset && endOffset > indexFrom)
+    {
+        uint32_t actualStart = std::max(startOffset, indexFrom);
+        uint32_t actualEnd = std::min(endOffset, indexTo);
+        if (actualEnd > actualStart)
+        {
+            m_unitCharacterIndices.push_back(actualStart);
+            m_unitLengths.push_back(actualEnd - actualStart);
+        }
+    }
+}
+
+void RangeMapper::fromWords(Span<const Unichar> text, uint32_t start, uint32_t end)
+{
+    if (text.empty())
+    {
+        return;
+    }
+
+    bool wantWhiteSpace = false;
+    uint32_t characterCount = 0;
+    uint32_t index = 0;
+    uint32_t indexFrom = 0;
+    for (Unichar unit : text)
+    {
+        if (wantWhiteSpace == isWhiteSpace(unit))
+        {
+            if (!wantWhiteSpace)
+            {
+                indexFrom = index;
+            }
+            else
+            {
+                addRange(indexFrom, indexFrom + characterCount, start, end);
+
+                characterCount = 0;
+            }
+            wantWhiteSpace = !wantWhiteSpace;
+        }
+        if (wantWhiteSpace)
+        {
+            characterCount++;
+        }
+        index++;
+    }
+    if (characterCount > 0)
+    {
+        addRange(indexFrom, indexFrom + characterCount, start, end);
+    }
+    m_unitCharacterIndices.push_back(end);
+}
+
+void RangeMapper::fromCharacters(Span<const Unichar> text,
+                                 uint32_t start,
+                                 uint32_t end,
+                                 const GlyphLookup& glyphLookup,
+                                 bool withoutSpaces)
+{
+    if (text.empty())
+    {
+        return;
+    }
+    for (uint32_t i = start; i < end;)
+    {
+        Unichar unit = text[i];
+        if (withoutSpaces && isWhiteSpace(unit))
+        {
+            i++;
+            continue;
+        }
+        // Don't break ligated glyphs.
+        uint32_t codePoints = glyphLookup.count(i);
+        m_unitCharacterIndices.push_back(i);
+        m_unitLengths.push_back(codePoints);
+        i += codePoints;
+    }
+
+    m_unitCharacterIndices.push_back(end);
+}
+
+void RangeMapper::fromLines(Span<const Unichar> text,
+                            uint32_t start,
+                            uint32_t end,
+                            const SimpleArray<Paragraph>& shape,
+                            const SimpleArray<SimpleArray<GlyphLine>>& lines,
+                            const GlyphLookup& glyphLookup)
+{
+    if (text.empty())
+    {
+        return;
+    }
+    uint32_t paragraphIndex = 0;
+    for (const SimpleArray<GlyphLine>& paragraphLines : lines)
+    {
+        const Paragraph& paragraph = shape[paragraphIndex++];
+        const SimpleArray<GlyphRun>& glyphRuns = paragraph.runs;
+        for (const GlyphLine& line : paragraphLines)
+        {
+            const GlyphRun& rf = glyphRuns[line.startRunIndex];
+            uint32_t indexFrom = rf.textIndices[line.startGlyphIndex];
+
+            const GlyphRun& rt = glyphRuns[line.endRunIndex];
+            uint32_t endGlyphIndex = line.endGlyphIndex == 0 ? 0 : line.endGlyphIndex - 1;
+            uint32_t indexTo = rt.textIndices[endGlyphIndex];
+            indexTo += glyphLookup.count(indexTo);
+
+            addRange(indexFrom, indexTo, start, end);
+        }
+    }
+    m_unitCharacterIndices.push_back(end);
+}
diff --git a/src/text/text_style.cpp b/src/text/text_style.cpp
new file mode 100644
index 0000000..a2a81ea
--- /dev/null
+++ b/src/text/text_style.cpp
@@ -0,0 +1,234 @@
+#include "rive/text/text_style.hpp"
+#include "rive/text/text_style_axis.hpp"
+#include "rive/text/text_style_feature.hpp"
+#include "rive/renderer.hpp"
+#include "rive/shapes/paint/shape_paint.hpp"
+#include "rive/backboard.hpp"
+#include "rive/importers/backboard_importer.hpp"
+#include "rive/text/text.hpp"
+#include "rive/artboard.hpp"
+#include "rive/factory.hpp"
+
+using namespace rive;
+
+namespace rive
+{
+class TextVariationHelper : public Component
+{
+public:
+    TextVariationHelper(TextStyle* style) : m_textStyle(style) {}
+    TextStyle* style() const { return m_textStyle; }
+    void buildDependencies() override
+    {
+        auto text = m_textStyle->parent();
+        text->artboard()->addDependent(this);
+        addDependent(text);
+    }
+
+    void update(ComponentDirt value) override { m_textStyle->updateVariableFont(); }
+
+private:
+    TextStyle* m_textStyle;
+};
+} // namespace rive
+
+// satisfy unique_ptr
+TextStyle::TextStyle() {}
+
+void TextStyle::addVariation(TextStyleAxis* axis) { m_variations.push_back(axis); }
+
+void TextStyle::addFeature(TextStyleFeature* feature) { m_styleFeatures.push_back(feature); }
+
+void TextStyle::onDirty(ComponentDirt dirt)
+{
+    if ((dirt & ComponentDirt::TextShape) == ComponentDirt::TextShape)
+    {
+        parent()->as<Text>()->markShapeDirty();
+        if (m_variationHelper != nullptr)
+        {
+            m_variationHelper->addDirt(ComponentDirt::TextShape);
+        }
+    }
+}
+
+StatusCode TextStyle::onAddedClean(CoreContext* context)
+{
+    auto code = Super::onAddedClean(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+    // This ensures context propagates to variation helper too.
+    if (!m_variations.empty())
+    {
+        m_variationHelper = rivestd::make_unique<TextVariationHelper>(this);
+    }
+    if (m_variationHelper != nullptr)
+    {
+        if ((code = m_variationHelper->onAddedDirty(context)) != StatusCode::Ok)
+        {
+            return code;
+        }
+        if ((code = m_variationHelper->onAddedClean(context)) != StatusCode::Ok)
+        {
+            return code;
+        }
+    }
+    return StatusCode::Ok;
+}
+
+const rcp<Font> TextStyle::font() const
+{
+    if (m_variableFont != nullptr)
+    {
+        return m_variableFont;
+    }
+
+    auto asset = fontAsset();
+    return asset == nullptr ? nullptr : asset->font();
+}
+
+void TextStyle::updateVariableFont()
+{
+    auto asset = fontAsset();
+    rcp<Font> baseFont = asset == nullptr ? nullptr : asset->font();
+    if (baseFont == nullptr)
+    {
+        // Not ready yet.
+        return;
+    }
+    if (!m_variations.empty() || !m_styleFeatures.empty())
+    {
+        m_coords.clear();
+        for (TextStyleAxis* axis : m_variations)
+        {
+            m_coords.push_back({axis->tag(), axis->axisValue()});
+        }
+        m_features.clear();
+        for (TextStyleFeature* styleFeature : m_styleFeatures)
+        {
+            m_features.push_back({styleFeature->tag(), styleFeature->featureValue()});
+        }
+        m_variableFont = baseFont->withOptions(m_coords, m_features);
+    }
+    else
+    {
+        m_variableFont = nullptr;
+    }
+}
+
+void TextStyle::buildDependencies()
+{
+    if (m_variationHelper != nullptr)
+    {
+        m_variationHelper->buildDependencies();
+    }
+    parent()->addDependent(this);
+    Super::buildDependencies();
+    auto factory = getArtboard()->factory();
+    m_path = factory->makeEmptyRenderPath();
+}
+
+void TextStyle::rewindPath()
+{
+    m_path->rewind();
+    m_hasContents = false;
+    m_opacityPaths.clear();
+}
+
+bool TextStyle::addPath(const RawPath& rawPath, float opacity)
+{
+    bool hadContents = m_hasContents;
+    m_hasContents = true;
+    if (opacity == 1.0f)
+    {
+        rawPath.addTo(m_path.get());
+    }
+    else if (opacity > 0.0f)
+    {
+        auto itr = m_opacityPaths.find(opacity);
+        RenderPath* renderPath = nullptr;
+        if (itr != m_opacityPaths.end())
+        {
+            renderPath = itr->second.get();
+        }
+        else
+        {
+            auto factory = getArtboard()->factory();
+            auto erp = factory->makeEmptyRenderPath();
+            renderPath = erp.get();
+            m_opacityPaths[opacity] = std::move(erp);
+        }
+        rawPath.addTo(renderPath);
+    }
+
+    return !hadContents;
+}
+
+void TextStyle::draw(Renderer* renderer)
+{
+    auto path = m_path.get();
+    for (auto shapePaint : m_ShapePaints)
+    {
+        if (!shapePaint->isVisible())
+        {
+            continue;
+        }
+        shapePaint->draw(renderer, path);
+
+        if (m_paintPool.size() < m_opacityPaths.size())
+        {
+            m_paintPool.reserve(m_opacityPaths.size());
+            Factory* factory = artboard()->factory();
+            while (m_paintPool.size() < m_opacityPaths.size())
+            {
+                m_paintPool.emplace_back(factory->makeRenderPaint());
+            }
+        }
+
+        uint32_t paintIndex = 0;
+        for (auto itr = m_opacityPaths.begin(); itr != m_opacityPaths.end(); itr++)
+        {
+            RenderPaint* renderPaint = m_paintPool[paintIndex++].get();
+            shapePaint->applyTo(renderPaint, itr->first);
+            shapePaint->draw(renderer, itr->second.get(), nullptr, renderPaint);
+        }
+    }
+}
+
+uint32_t TextStyle::assetId() { return this->fontAssetId(); }
+
+void TextStyle::setAsset(FileAsset* asset)
+{
+    if (asset->is<FontAsset>())
+    {
+        FileAssetReferencer::setAsset(asset);
+    }
+}
+
+StatusCode TextStyle::import(ImportStack& importStack)
+{
+    auto result = registerReferencer(importStack);
+    if (result != StatusCode::Ok)
+    {
+        return result;
+    }
+    return Super::import(importStack);
+}
+
+void TextStyle::fontSizeChanged() { parent()->as<Text>()->markShapeDirty(); }
+
+void TextStyle::lineHeightChanged() { parent()->as<Text>()->markShapeDirty(); }
+
+void TextStyle::letterSpacingChanged() { parent()->as<Text>()->markShapeDirty(); }
+
+Core* TextStyle::clone() const
+{
+    TextStyle* twin = TextStyleBase::clone()->as<TextStyle>();
+    if (m_fileAsset != nullptr)
+    {
+        twin->setAsset(m_fileAsset);
+    }
+
+    return twin;
+}
\ No newline at end of file
diff --git a/src/text/text_style_axis.cpp b/src/text/text_style_axis.cpp
new file mode 100644
index 0000000..4064578
--- /dev/null
+++ b/src/text/text_style_axis.cpp
@@ -0,0 +1,24 @@
+#include "rive/text/text_style_axis.hpp"
+#include "rive/text/text_style.hpp"
+#include "rive/container_component.hpp"
+
+using namespace rive;
+
+StatusCode TextStyleAxis::onAddedDirty(CoreContext* context)
+{
+    StatusCode code = Super::onAddedDirty(context);
+    if (code == StatusCode::Ok)
+    {
+        if (!parent()->is<TextStyle>())
+        {
+            return StatusCode::InvalidObject;
+        }
+        auto style = parent()->as<TextStyle>();
+        style->addVariation(this);
+    }
+    return code;
+}
+
+void TextStyleAxis::tagChanged() { parent()->addDirt(ComponentDirt::TextShape); }
+
+void TextStyleAxis::axisValueChanged() { parent()->addDirt(ComponentDirt::TextShape); }
\ No newline at end of file
diff --git a/src/text/text_style_feature.cpp b/src/text/text_style_feature.cpp
new file mode 100644
index 0000000..31410a5
--- /dev/null
+++ b/src/text/text_style_feature.cpp
@@ -0,0 +1,20 @@
+#include "rive/text/text_style_feature.hpp"
+#include "rive/text/text_style.hpp"
+#include "rive/container_component.hpp"
+
+using namespace rive;
+
+StatusCode TextStyleFeature::onAddedDirty(CoreContext* context)
+{
+    StatusCode code = Super::onAddedDirty(context);
+    if (code == StatusCode::Ok)
+    {
+        if (!parent()->is<TextStyle>())
+        {
+            return StatusCode::InvalidObject;
+        }
+        auto style = parent()->as<TextStyle>();
+        style->addFeature(this);
+    }
+    return code;
+}
\ No newline at end of file
diff --git a/src/text/text_value_run.cpp b/src/text/text_value_run.cpp
new file mode 100644
index 0000000..9db0396
--- /dev/null
+++ b/src/text/text_value_run.cpp
@@ -0,0 +1,79 @@
+#include "rive/core_context.hpp"
+#include "rive/text/text.hpp"
+#include "rive/text/text_style.hpp"
+#include "rive/text/text_value_run.hpp"
+#include "rive/artboard.hpp"
+#include "rive/importers/artboard_importer.hpp"
+
+using namespace rive;
+
+void TextValueRun::textChanged()
+{
+    m_length = -1;
+    parent()->as<Text>()->markShapeDirty();
+}
+
+StatusCode TextValueRun::onAddedClean(CoreContext* context)
+{
+    StatusCode code = Super::onAddedClean(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+
+    if (parent() != nullptr && parent()->is<Text>())
+    {
+        parent()->as<Text>()->addRun(this);
+        return StatusCode::Ok;
+    }
+
+    return StatusCode::MissingObject;
+}
+
+StatusCode TextValueRun::onAddedDirty(CoreContext* context)
+{
+    StatusCode code = Super::onAddedDirty(context);
+    if (code != StatusCode::Ok)
+    {
+        return code;
+    }
+    auto coreObject = context->resolve(styleId());
+    if (coreObject == nullptr || !coreObject->is<TextStyle>())
+    {
+        return StatusCode::MissingObject;
+    }
+
+    m_style = static_cast<TextStyle*>(coreObject);
+
+    return StatusCode::Ok;
+}
+
+void TextValueRun::styleIdChanged()
+{
+    auto coreObject = artboard()->resolve(styleId());
+    if (coreObject != nullptr && coreObject->is<TextStyle>())
+    {
+        m_style = static_cast<TextStyle*>(coreObject);
+        parent()->as<Text>()->markShapeDirty();
+    }
+}
+
+uint32_t TextValueRun::offset() const
+{
+#ifdef WITH_RIVE_TEXT
+    Text* text = parent()->as<Text>();
+    uint32_t offset = 0;
+
+    for (TextValueRun* run : text->runs())
+    {
+        if (run == this)
+        {
+            break;
+        }
+        offset += run->length();
+    }
+    return offset;
+#else
+    return 0;
+#endif
+}
\ No newline at end of file
diff --git a/src/text/text_variation_modifier.cpp b/src/text/text_variation_modifier.cpp
new file mode 100644
index 0000000..a0918c9
--- /dev/null
+++ b/src/text/text_variation_modifier.cpp
@@ -0,0 +1,22 @@
+#include "rive/text/text_variation_modifier.hpp"
+#include "rive/text/text_modifier_group.hpp"
+#include "rive/text_engine.hpp"
+
+using namespace rive;
+
+float TextVariationModifier::modify(Font* font,
+                                    std::unordered_map<uint32_t, float>& variations,
+                                    float fontSize,
+                                    float strength) const
+{
+    auto itr = variations.find(axisTag());
+
+    float fromValue = itr != variations.end() ? itr->second : font->getAxisValue(axisTag());
+    variations[axisTag()] = fromValue * (1 - strength) + axisValue() * strength;
+    return fontSize;
+}
+
+void TextVariationModifier::axisValueChanged()
+{
+    parent()->as<TextModifierGroup>()->shapeModifierChanged();
+}
\ No newline at end of file
diff --git a/src/text/utf.cpp b/src/text/utf.cpp
new file mode 100644
index 0000000..1d69243
--- /dev/null
+++ b/src/text/utf.cpp
@@ -0,0 +1,64 @@
+#include "rive/text/utf.hpp"
+#include "rive/core/type_conversions.hpp"
+
+using namespace rive;
+
+int UTF::CountUTF8Length(const uint8_t utf8[])
+{
+    unsigned lead = *utf8;
+    assert(lead != 0xFF);
+    assert((lead & 0xC0) != 0x80); // 10xxxxxx is not a legal lead byte
+    if ((lead & 0x80) == 0)
+    {
+        return 1;
+    }
+    int n = 1;
+    lead <<= 1;
+    while (lead & 0x80)
+    {
+        n += 1;
+        lead <<= 1;
+    }
+    assert(n >= 1 && n <= 4);
+    return n;
+}
+
+// Return the unichar pointed to by the utf8 pointer, and then
+// update the pointer to point to the next sequence.
+Unichar UTF::NextUTF8(const uint8_t** utf8Ptr)
+{
+    const uint8_t* text = *utf8Ptr;
+
+    uint32_t c = 0;
+    int n = CountUTF8Length(text);
+    assert(n >= 1 && n <= 4);
+
+    unsigned first = *text++;
+    if (n == 1)
+    {
+        c = first;
+    }
+    else
+    {
+        c = first & ((unsigned)0xFF >> n);
+        --n;
+        do
+        {
+            c = (c << 6) | (*text++ & 0x3F);
+        } while (--n);
+    }
+    *utf8Ptr = text; // update the pointer
+    return c;
+}
+
+int UTF::ToUTF16(Unichar uni, uint16_t utf16[])
+{
+    if (uni > 0xFFFF)
+    {
+        utf16[0] = castTo<uint16_t>((0xD800 - 64) | (uni >> 10));
+        utf16[1] = castTo<uint16_t>(0xDC00 | (uni & 0x3FF));
+        return 2;
+    }
+    utf16[0] = castTo<uint16_t>(uni);
+    return 1;
+}
diff --git a/src/transform_component.cpp b/src/transform_component.cpp
new file mode 100644
index 0000000..73bc031
--- /dev/null
+++ b/src/transform_component.cpp
@@ -0,0 +1,124 @@
+#include "rive/transform_component.hpp"
+#include "rive/world_transform_component.hpp"
+#include "rive/shapes/clipping_shape.hpp"
+#include "rive/math/vec2d.hpp"
+#include "rive/constraints/constraint.hpp"
+
+using namespace rive;
+
+StatusCode TransformComponent::onAddedClean(CoreContext* context)
+{
+    m_ParentTransformComponent = parent() != nullptr && parent()->is<WorldTransformComponent>()
+                                     ? parent()->as<WorldTransformComponent>()
+                                     : nullptr;
+    return StatusCode::Ok;
+}
+
+bool TransformComponent::collapse(bool value)
+{
+    if (!Super::collapse(value))
+    {
+        return false;
+    }
+    for (auto d : dependents())
+    {
+        if (d->is<TransformComponent>())
+        {
+            auto dependent = d->as<TransformComponent>();
+            dependent->markDirtyIfConstrained();
+        }
+    }
+    return true;
+}
+
+// If the component has any constraints applied we mark it as dirty
+// because one of its constraining targets has changed its collapse
+// status.
+void TransformComponent::markDirtyIfConstrained()
+{
+    if (m_Constraints.size() > 0)
+    {
+        addDirt(ComponentDirt::WorldTransform, true);
+    }
+}
+
+void TransformComponent::buildDependencies()
+{
+    if (parent() != nullptr)
+    {
+        parent()->addDependent(this);
+    }
+}
+
+void TransformComponent::markTransformDirty()
+{
+    if (!addDirt(ComponentDirt::Transform))
+    {
+        return;
+    }
+    markWorldTransformDirty();
+}
+
+void TransformComponent::updateTransform()
+{
+    m_Transform = Mat2D::fromRotation(rotation());
+    m_Transform[4] = x();
+    m_Transform[5] = y();
+    m_Transform.scaleByValues(scaleX(), scaleY());
+}
+
+AABB TransformComponent::localBounds() const { return AABB(); }
+
+void TransformComponent::updateWorldTransform()
+{
+    if (m_ParentTransformComponent != nullptr)
+    {
+        m_WorldTransform = m_ParentTransformComponent->m_WorldTransform * m_Transform;
+    }
+    else
+    {
+        m_WorldTransform = m_Transform;
+    }
+    updateConstraints();
+}
+
+void TransformComponent::updateConstraints()
+{
+    for (auto constraint : m_Constraints)
+    {
+        constraint->constrain(this);
+    }
+}
+
+void TransformComponent::update(ComponentDirt value)
+{
+    if (hasDirt(value, ComponentDirt::Transform))
+    {
+        updateTransform();
+    }
+    if (hasDirt(value, ComponentDirt::WorldTransform))
+    {
+        updateWorldTransform();
+    }
+    if (hasDirt(value, ComponentDirt::RenderOpacity))
+    {
+        m_RenderOpacity = opacity();
+        if (m_ParentTransformComponent != nullptr)
+        {
+            m_RenderOpacity *= m_ParentTransformComponent->childOpacity();
+        }
+    }
+}
+
+const Mat2D& TransformComponent::transform() const { return m_Transform; }
+
+Mat2D& TransformComponent::mutableTransform() { return m_Transform; }
+
+void TransformComponent::rotationChanged() { markTransformDirty(); }
+void TransformComponent::scaleXChanged() { markTransformDirty(); }
+void TransformComponent::scaleYChanged() { markTransformDirty(); }
+
+void TransformComponent::addConstraint(Constraint* constraint)
+{
+    m_Constraints.push_back(constraint);
+}
diff --git a/src/viewmodel/data_enum.cpp b/src/viewmodel/data_enum.cpp
new file mode 100644
index 0000000..a1780cb
--- /dev/null
+++ b/src/viewmodel/data_enum.cpp
@@ -0,0 +1,80 @@
+#include <sstream>
+#include <iomanip>
+#include <array>
+
+#include "rive/viewmodel/data_enum.hpp"
+#include "rive/viewmodel/data_enum_value.hpp"
+#include "rive/viewmodel/viewmodel_property.hpp"
+#include "rive/backboard.hpp"
+#include "rive/importers/backboard_importer.hpp"
+
+using namespace rive;
+
+void DataEnum::addValue(DataEnumValue* value) { m_Values.push_back(value); }
+
+std::string DataEnum::value(std::string key)
+{
+    for (auto enumValue : m_Values)
+    {
+        if (enumValue->key() == key)
+        {
+            return enumValue->value();
+        };
+    }
+    return "";
+}
+
+std::string DataEnum::value(uint32_t index)
+{
+    if (index < m_Values.size())
+    {
+        return m_Values[index]->value();
+    }
+    return "";
+}
+
+bool DataEnum::value(std::string key, std::string value)
+{
+    for (auto enumValue : m_Values)
+    {
+        if (enumValue->key() == key)
+        {
+            enumValue->value(value);
+            return true;
+        };
+    }
+    return false;
+}
+
+bool DataEnum::value(uint32_t index, std::string value)
+{
+    if (index < m_Values.size())
+    {
+        m_Values[index]->value(value);
+        return true;
+    }
+    return false;
+}
+
+int DataEnum::valueIndex(std::string key)
+{
+    int index = 0;
+    for (auto enumValue : m_Values)
+    {
+        if (enumValue->key() == key)
+        {
+            return index;
+        };
+        index++;
+    }
+    return -1;
+}
+
+int DataEnum::valueIndex(uint32_t index)
+{
+    if (index < m_Values.size())
+    {
+        return index;
+    }
+    return -1;
+}
\ No newline at end of file
diff --git a/src/viewmodel/data_enum_value.cpp b/src/viewmodel/data_enum_value.cpp
new file mode 100644
index 0000000..e5a2e90
--- /dev/null
+++ b/src/viewmodel/data_enum_value.cpp
@@ -0,0 +1,21 @@
+#include <sstream>
+#include <iomanip>
+#include <array>
+
+#include "rive/viewmodel/data_enum.hpp"
+#include "rive/viewmodel/data_enum_value.hpp"
+#include "rive/importers/enum_importer.hpp"
+
+using namespace rive;
+
+StatusCode DataEnumValue::import(ImportStack& importStack)
+{
+    auto enumImporter = importStack.latest<EnumImporter>(DataEnum::typeKey);
+    if (enumImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+
+    enumImporter->addValue(this);
+    return Super::import(importStack);
+}
\ No newline at end of file
diff --git a/src/viewmodel/viewmodel.cpp b/src/viewmodel/viewmodel.cpp
new file mode 100644
index 0000000..3d9ecfa
--- /dev/null
+++ b/src/viewmodel/viewmodel.cpp
@@ -0,0 +1,62 @@
+#include <sstream>
+#include <iomanip>
+#include <array>
+
+#include "rive/viewmodel/viewmodel.hpp"
+#include "rive/viewmodel/viewmodel_property.hpp"
+#include "rive/backboard.hpp"
+#include "rive/importers/backboard_importer.hpp"
+
+using namespace rive;
+
+void ViewModel::addProperty(ViewModelProperty* property) { m_Properties.push_back(property); }
+
+ViewModelProperty* ViewModel::property(size_t index)
+{
+    if (index < m_Properties.size())
+    {
+        return m_Properties[index];
+    }
+    return nullptr;
+}
+
+ViewModelProperty* ViewModel::property(const std::string& name)
+{
+    for (auto property : m_Properties)
+    {
+        if (property->name() == name)
+        {
+            return property;
+        }
+    }
+    return nullptr;
+}
+
+void ViewModel::addInstance(ViewModelInstance* value)
+{
+    m_Instances.push_back(value);
+    value->viewModel(this);
+}
+
+ViewModelInstance* ViewModel::defaultInstance() { return m_Instances[defaultInstanceId()]; }
+
+ViewModelInstance* ViewModel::instance(size_t index)
+{
+    if (index < m_Instances.size())
+    {
+        return m_Instances[index];
+    }
+    return nullptr;
+}
+
+ViewModelInstance* ViewModel::instance(const std::string& name)
+{
+    for (auto instance : m_Instances)
+    {
+        if (instance->name() == name)
+        {
+            return instance;
+        }
+    }
+    return nullptr;
+}
\ No newline at end of file
diff --git a/src/viewmodel/viewmodel_instance.cpp b/src/viewmodel/viewmodel_instance.cpp
new file mode 100644
index 0000000..b2656b5
--- /dev/null
+++ b/src/viewmodel/viewmodel_instance.cpp
@@ -0,0 +1,115 @@
+#include <sstream>
+#include <iomanip>
+#include <array>
+
+#include "rive/viewmodel/viewmodel_instance.hpp"
+#include "rive/viewmodel/viewmodel.hpp"
+#include "rive/viewmodel/viewmodel_instance_value.hpp"
+#include "rive/viewmodel/viewmodel_instance_viewmodel.hpp"
+#include "rive/importers/viewmodel_importer.hpp"
+#include "rive/core_context.hpp"
+
+using namespace rive;
+
+void ViewModelInstance::addValue(ViewModelInstanceValue* value)
+{
+    m_PropertyValues.push_back(value);
+}
+
+ViewModelInstanceValue* ViewModelInstance::propertyValue(const uint32_t id)
+{
+    for (auto value : m_PropertyValues)
+    {
+        if (value->viewModelPropertyId() == id)
+        {
+            return value;
+        }
+    }
+    return nullptr;
+}
+
+ViewModelInstanceValue* ViewModelInstance::propertyValue(const std::string& name)
+{
+    auto viewModelProperty = viewModel()->property(name);
+    if (viewModelProperty != nullptr)
+    {
+        for (auto value : m_PropertyValues)
+        {
+            if (value->viewModelProperty() == viewModelProperty)
+            {
+                return value;
+            }
+        }
+    }
+    return nullptr;
+}
+
+void ViewModelInstance::viewModel(ViewModel* value) { m_ViewModel = value; }
+
+ViewModel* ViewModelInstance::viewModel() const { return m_ViewModel; }
+
+void ViewModelInstance::onComponentDirty(Component* component) {}
+
+void ViewModelInstance::setAsRoot() { setRoot(this); }
+
+void ViewModelInstance::setRoot(ViewModelInstance* value)
+{
+    for (auto propertyValue : m_PropertyValues)
+    {
+        propertyValue->setRoot(value);
+    }
+}
+
+std::vector<ViewModelInstanceValue*> ViewModelInstance::propertyValues()
+{
+    return m_PropertyValues;
+}
+
+Core* ViewModelInstance::clone() const
+{
+    auto cloned = new ViewModelInstance();
+    cloned->copy(*this);
+    for (auto propertyValue : m_PropertyValues)
+    {
+        auto clonedValue = propertyValue->clone()->as<ViewModelInstanceValue>();
+        cloned->addValue(clonedValue);
+    }
+    cloned->viewModel(viewModel());
+    return cloned;
+}
+
+StatusCode ViewModelInstance::import(ImportStack& importStack)
+{
+    auto viewModelImporter = importStack.latest<ViewModelImporter>(ViewModel::typeKey);
+    if (viewModelImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+
+    viewModelImporter->addInstance(this);
+    return StatusCode::Ok;
+}
+
+ViewModelInstanceValue* ViewModelInstance::propertyFromPath(std::vector<uint32_t>* path,
+                                                            size_t index)
+{
+    if (index < path->size())
+    {
+        auto propertyId = (*path)[index];
+        auto property = propertyValue(propertyId);
+        if (property != nullptr)
+        {
+            if (index == path->size() - 1)
+            {
+                return property;
+            }
+            if (property->is<ViewModelInstanceViewModel>())
+            {
+                auto propertyViewModel = property->as<ViewModelInstanceViewModel>();
+                auto viewModelInstance = propertyViewModel->referenceViewModelInstance();
+                return viewModelInstance->propertyFromPath(path, index + 1);
+            }
+        }
+    }
+    return nullptr;
+}
\ No newline at end of file
diff --git a/src/viewmodel/viewmodel_instance_boolean.cpp b/src/viewmodel/viewmodel_instance_boolean.cpp
new file mode 100644
index 0000000..c4e5599
--- /dev/null
+++ b/src/viewmodel/viewmodel_instance_boolean.cpp
@@ -0,0 +1,10 @@
+#include <sstream>
+#include <iomanip>
+#include <array>
+
+#include "rive/viewmodel/viewmodel_instance_boolean.hpp"
+#include "rive/component_dirt.hpp"
+
+using namespace rive;
+
+void ViewModelInstanceBoolean::propertyValueChanged() { addDirt(ComponentDirt::Bindings); }
\ No newline at end of file
diff --git a/src/viewmodel/viewmodel_instance_color.cpp b/src/viewmodel/viewmodel_instance_color.cpp
new file mode 100644
index 0000000..9cd82bd
--- /dev/null
+++ b/src/viewmodel/viewmodel_instance_color.cpp
@@ -0,0 +1,10 @@
+#include <sstream>
+#include <iomanip>
+#include <array>
+
+#include "rive/viewmodel/viewmodel_instance_color.hpp"
+#include "rive/component_dirt.hpp"
+
+using namespace rive;
+
+void ViewModelInstanceColor::propertyValueChanged() { addDirt(ComponentDirt::Bindings); }
\ No newline at end of file
diff --git a/src/viewmodel/viewmodel_instance_enum.cpp b/src/viewmodel/viewmodel_instance_enum.cpp
new file mode 100644
index 0000000..3b7a755
--- /dev/null
+++ b/src/viewmodel/viewmodel_instance_enum.cpp
@@ -0,0 +1,34 @@
+#include <sstream>
+#include <iomanip>
+#include <array>
+
+#include "rive/viewmodel/viewmodel_instance_enum.hpp"
+#include "rive/viewmodel/viewmodel_property_enum.hpp"
+#include "rive/component_dirt.hpp"
+
+using namespace rive;
+
+void ViewModelInstanceEnum::propertyValueChanged() { addDirt(ComponentDirt::Bindings); }
+
+bool ViewModelInstanceEnum::value(std::string name)
+{
+    auto enumProperty = viewModelProperty()->as<ViewModelPropertyEnum>();
+    int index = enumProperty->valueIndex(name);
+    if (index != -1)
+    {
+        propertyValue(index);
+        return true;
+    }
+    return false;
+}
+
+bool ViewModelInstanceEnum::value(uint32_t index)
+{
+    auto enumProperty = viewModelProperty()->as<ViewModelPropertyEnum>();
+    if (enumProperty->valueIndex(index) != -1)
+    {
+        propertyValue(index);
+        return true;
+    }
+    return false;
+}
\ No newline at end of file
diff --git a/src/viewmodel/viewmodel_instance_list.cpp b/src/viewmodel/viewmodel_instance_list.cpp
new file mode 100644
index 0000000..0ac9f5a
--- /dev/null
+++ b/src/viewmodel/viewmodel_instance_list.cpp
@@ -0,0 +1,74 @@
+#include <sstream>
+#include <iomanip>
+#include <array>
+
+#include "rive/viewmodel/viewmodel_instance_list.hpp"
+#include "rive/viewmodel/viewmodel_instance_list_item.hpp"
+#include "rive/component_dirt.hpp"
+
+using namespace rive;
+
+void ViewModelInstanceList::propertyValueChanged() { addDirt(ComponentDirt::Components); }
+
+void ViewModelInstanceList::addItem(ViewModelInstanceListItem* item)
+{
+    m_ListItems.push_back(item);
+    propertyValueChanged();
+}
+
+void ViewModelInstanceList::insertItem(int index, ViewModelInstanceListItem* item)
+{
+    // TODO: @hernan decide if we want to return a boolean
+    if (index < m_ListItems.size())
+    {
+        m_ListItems.insert(m_ListItems.begin() + index, item);
+        propertyValueChanged();
+    }
+}
+
+void ViewModelInstanceList::removeItem(int index)
+{
+    // TODO: @hernan decide if we want to return a boolean
+    if (index < m_ListItems.size())
+    {
+        m_ListItems.erase(m_ListItems.begin() + index);
+        propertyValueChanged();
+    }
+}
+
+void ViewModelInstanceList::removeItem(ViewModelInstanceListItem* listItem)
+{
+    auto noSpaceEnd = std::remove(m_ListItems.begin(), m_ListItems.end(), listItem);
+    m_ListItems.erase(noSpaceEnd, m_ListItems.end());
+    propertyValueChanged();
+}
+
+ViewModelInstanceListItem* ViewModelInstanceList::item(uint32_t index)
+{
+    if (index < m_ListItems.size())
+    {
+        return m_ListItems[index];
+    }
+    return nullptr;
+}
+
+void ViewModelInstanceList::swap(uint32_t index1, uint32_t index2)
+{
+    if (index1 < m_ListItems.size() && index2 < m_ListItems.size())
+    {
+        std::iter_swap(m_ListItems.begin() + index1, m_ListItems.begin() + index2);
+        propertyValueChanged();
+    }
+}
+
+Core* ViewModelInstanceList::clone() const
+{
+    auto cloned = new ViewModelInstanceList();
+    cloned->copy(*this);
+    for (auto property : m_ListItems)
+    {
+        auto clonedValue = property->clone()->as<ViewModelInstanceListItem>();
+        cloned->addItem(clonedValue);
+    }
+    return cloned;
+}
\ No newline at end of file
diff --git a/src/viewmodel/viewmodel_instance_list_item.cpp b/src/viewmodel/viewmodel_instance_list_item.cpp
new file mode 100644
index 0000000..a723c6c
--- /dev/null
+++ b/src/viewmodel/viewmodel_instance_list_item.cpp
@@ -0,0 +1,22 @@
+#include <sstream>
+#include <iomanip>
+#include <array>
+
+#include "rive/viewmodel/viewmodel_instance_list.hpp"
+#include "rive/viewmodel/viewmodel_instance_list_item.hpp"
+#include "rive/importers/viewmodel_instance_list_importer.hpp"
+
+using namespace rive;
+
+StatusCode ViewModelInstanceListItem::import(ImportStack& importStack)
+{
+    auto viewModelInstanceList =
+        importStack.latest<ViewModelInstanceListImporter>(ViewModelInstanceList::typeKey);
+    if (viewModelInstanceList == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    viewModelInstanceList->addItem(this);
+
+    return Super::import(importStack);
+}
\ No newline at end of file
diff --git a/src/viewmodel/viewmodel_instance_number.cpp b/src/viewmodel/viewmodel_instance_number.cpp
new file mode 100644
index 0000000..6e2620a
--- /dev/null
+++ b/src/viewmodel/viewmodel_instance_number.cpp
@@ -0,0 +1,10 @@
+#include <sstream>
+#include <iomanip>
+#include <array>
+
+#include "rive/viewmodel/viewmodel_instance_number.hpp"
+#include "rive/component_dirt.hpp"
+
+using namespace rive;
+
+void ViewModelInstanceNumber::propertyValueChanged() { addDirt(ComponentDirt::Bindings); }
\ No newline at end of file
diff --git a/src/viewmodel/viewmodel_instance_string.cpp b/src/viewmodel/viewmodel_instance_string.cpp
new file mode 100644
index 0000000..6c60d4b
--- /dev/null
+++ b/src/viewmodel/viewmodel_instance_string.cpp
@@ -0,0 +1,10 @@
+#include <sstream>
+#include <iomanip>
+#include <array>
+
+#include "rive/viewmodel/viewmodel_instance_string.hpp"
+#include "rive/component_dirt.hpp"
+
+using namespace rive;
+
+void ViewModelInstanceString::propertyValueChanged() { addDirt(ComponentDirt::Bindings); }
\ No newline at end of file
diff --git a/src/viewmodel/viewmodel_instance_value.cpp b/src/viewmodel/viewmodel_instance_value.cpp
new file mode 100644
index 0000000..de7bdba
--- /dev/null
+++ b/src/viewmodel/viewmodel_instance_value.cpp
@@ -0,0 +1,41 @@
+#include <sstream>
+#include <iomanip>
+#include <array>
+
+#include "rive/viewmodel/viewmodel_instance.hpp"
+#include "rive/viewmodel/viewmodel_instance_value.hpp"
+#include "rive/importers/viewmodel_instance_importer.hpp"
+#include "rive/data_bind/data_bind.hpp"
+
+using namespace rive;
+
+StatusCode ViewModelInstanceValue::import(ImportStack& importStack)
+{
+    auto viewModelInstanceImporter =
+        importStack.latest<ViewModelInstanceImporter>(ViewModelInstance::typeKey);
+    if (viewModelInstanceImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+    viewModelInstanceImporter->addValue(this);
+
+    return Super::import(importStack);
+}
+
+void ViewModelInstanceValue::viewModelProperty(ViewModelProperty* value)
+{
+    m_ViewModelProperty = value;
+}
+ViewModelProperty* ViewModelInstanceValue::viewModelProperty() { return m_ViewModelProperty; }
+
+void ViewModelInstanceValue::addDependent(DataBind* value)
+{
+    m_DependencyHelper.addDependent(value);
+}
+
+void ViewModelInstanceValue::addDirt(ComponentDirt value) { m_DependencyHelper.addDirt(value); }
+
+void ViewModelInstanceValue::setRoot(ViewModelInstance* viewModelInstance)
+{
+    m_DependencyHelper.dependecyRoot(viewModelInstance);
+}
\ No newline at end of file
diff --git a/src/viewmodel/viewmodel_instance_viewmodel.cpp b/src/viewmodel/viewmodel_instance_viewmodel.cpp
new file mode 100644
index 0000000..7630112
--- /dev/null
+++ b/src/viewmodel/viewmodel_instance_viewmodel.cpp
@@ -0,0 +1,13 @@
+#include <sstream>
+#include <iomanip>
+#include <array>
+
+#include "rive/viewmodel/viewmodel_instance_viewmodel.hpp"
+
+using namespace rive;
+
+void ViewModelInstanceViewModel::setRoot(ViewModelInstance* value)
+{
+    Super::setRoot(value);
+    referenceViewModelInstance()->setRoot(value);
+}
\ No newline at end of file
diff --git a/src/viewmodel/viewmodel_property.cpp b/src/viewmodel/viewmodel_property.cpp
new file mode 100644
index 0000000..4450bc1
--- /dev/null
+++ b/src/viewmodel/viewmodel_property.cpp
@@ -0,0 +1,17 @@
+#include "rive/viewmodel/viewmodel_property.hpp"
+#include "rive/importers/viewmodel_importer.hpp"
+#include "rive/viewmodel/viewmodel.hpp"
+
+using namespace rive;
+
+StatusCode ViewModelProperty::import(ImportStack& importStack)
+{
+    auto viewModelImporter = importStack.latest<ViewModelImporter>(ViewModel::typeKey);
+    if (viewModelImporter == nullptr)
+    {
+        return StatusCode::MissingObject;
+    }
+
+    viewModelImporter->addProperty(this);
+    return Super::import(importStack);
+}
diff --git a/src/viewmodel/viewmodel_property_enum.cpp b/src/viewmodel/viewmodel_property_enum.cpp
new file mode 100644
index 0000000..ef5e12f
--- /dev/null
+++ b/src/viewmodel/viewmodel_property_enum.cpp
@@ -0,0 +1,61 @@
+#include "rive/viewmodel/viewmodel_property_enum.hpp"
+
+using namespace rive;
+
+void ViewModelPropertyEnum::dataEnum(DataEnum* value) { m_DataEnum = value; }
+
+DataEnum* ViewModelPropertyEnum::dataEnum() { return m_DataEnum; }
+
+std::string ViewModelPropertyEnum::value(std::string name)
+{
+    if (dataEnum() != nullptr)
+    {
+        return dataEnum()->value(name);
+    }
+    return "";
+}
+
+std::string ViewModelPropertyEnum::value(uint32_t index)
+{
+    if (dataEnum() != nullptr)
+    {
+        return dataEnum()->value(index);
+    }
+    return "";
+}
+
+bool ViewModelPropertyEnum::value(std::string name, std::string value)
+{
+    if (dataEnum() != nullptr)
+    {
+        return dataEnum()->value(name, value);
+    }
+    return false;
+}
+
+bool ViewModelPropertyEnum::value(uint32_t index, std::string value)
+{
+    if (dataEnum() != nullptr)
+    {
+        return dataEnum()->value(index, value);
+    }
+    return false;
+}
+
+int ViewModelPropertyEnum::valueIndex(std::string name)
+{
+    if (dataEnum() != nullptr)
+    {
+        return dataEnum()->valueIndex(name);
+    }
+    return -1;
+}
+
+int ViewModelPropertyEnum::valueIndex(uint32_t index)
+{
+    if (dataEnum() != nullptr)
+    {
+        return dataEnum()->valueIndex(index);
+    }
+    return -1;
+}
diff --git a/src/world_transform_component.cpp b/src/world_transform_component.cpp
new file mode 100644
index 0000000..a132577
--- /dev/null
+++ b/src/world_transform_component.cpp
@@ -0,0 +1,19 @@
+#include "rive/transform_component.hpp"
+#include "rive/shapes/clipping_shape.hpp"
+#include "rive/math/vec2d.hpp"
+#include "rive/constraints/constraint.hpp"
+
+using namespace rive;
+
+float WorldTransformComponent::childOpacity() { return opacity(); }
+
+void WorldTransformComponent::markWorldTransformDirty()
+{
+    addDirt(ComponentDirt::WorldTransform, true);
+}
+
+const Mat2D& WorldTransformComponent::worldTransform() const { return m_WorldTransform; }
+
+Mat2D& WorldTransformComponent::mutableWorldTransform() { return m_WorldTransform; }
+
+void WorldTransformComponent::opacityChanged() { addDirt(ComponentDirt::RenderOpacity, true); }
diff --git a/tess/build/macosx/build_tess.sh b/tess/build/macosx/build_tess.sh
new file mode 100755
index 0000000..6e8884f
--- /dev/null
+++ b/tess/build/macosx/build_tess.sh
@@ -0,0 +1,71 @@
+#!/bin/sh
+set -e
+
+source ../../../dependencies/macosx/config_directories.sh
+
+if [[ ! -f "$DEPENDENCIES/bin/premake5" ]]; then
+    pushd $DEPENDENCIES_SCRIPTS
+    ./get_premake5.sh
+    popd
+fi
+
+if [[ ! -d "$DEPENDENCIES/sokol" ]]; then
+    pushd $DEPENDENCIES_SCRIPTS
+    ./get_sokol.sh
+    popd
+fi
+
+if [[ ! -d "$DEPENDENCIES/earcut.hpp" ]]; then
+    pushd $DEPENDENCIES_SCRIPTS
+    ./get_earcut.sh
+    popd
+fi
+
+if [[ ! -d "$DEPENDENCIES/libtess2" ]]; then
+    pushd $DEPENDENCIES_SCRIPTS
+    ./get_libtess2.sh
+    popd
+fi
+
+export PREMAKE=$DEPENDENCIES/bin/premake5
+pushd ..
+
+CONFIG=debug
+GRAPHICS=gl
+TEST=false
+for var in "$@"; do
+    if [[ $var = "release" ]]; then
+        CONFIG=release
+    fi
+    if [[ $var = "gl" ]]; then
+        GRAPHICS=gl
+    fi
+    if [[ $var = "d3d" ]]; then
+        GRAPHICS=d3d
+    fi
+    if [[ $var = "metal" ]]; then
+        GRAPHICS=metal
+    fi
+    if [[ $var = "test" ]]; then
+        TEST=true
+    fi
+done
+
+$PREMAKE --scripts=../../build --file=./premake5_tess.lua gmake2 --graphics=$GRAPHICS --with_rive_tools
+
+for var in "$@"; do
+    if [[ $var = "clean" ]]; then
+        make clean
+        make config=release clean
+    fi
+done
+
+# compile shaders
+$DEPENDENCIES/bin/sokol-shdc --input ../src/sokol/shader.glsl --output ../src/sokol/generated/shader.h --slang glsl330:hlsl5:metal_macos:metal_ios:metal_sim:glsl300es:glsl100
+
+make config=$CONFIG -j$(($(sysctl -n hw.physicalcpu) + 1))
+
+if [[ $TEST = "true" ]]; then
+    macosx/bin/$CONFIG/rive_tess_tests
+fi
+popd
diff --git a/tess/build/premake5_tess.lua b/tess/build/premake5_tess.lua
new file mode 100644
index 0000000..7f93b18
--- /dev/null
+++ b/tess/build/premake5_tess.lua
@@ -0,0 +1,103 @@
+workspace('rive')
+configurations({ 'debug', 'release' })
+
+dependencies = os.getenv('DEPENDENCIES')
+
+rive = '../../'
+
+dofile(path.join(path.getabsolute(rive) .. '/build', 'premake5.lua'))
+
+project('rive_tess_renderer')
+do
+    kind('StaticLib')
+    language('C++')
+    cppdialect('C++11')
+    toolset('clang')
+    targetdir('%{cfg.system}/bin/%{cfg.buildcfg}')
+    objdir('%{cfg.system}/obj/%{cfg.buildcfg}')
+    includedirs({
+        '../include',
+        rive .. '/include',
+        dependencies .. '/sokol',
+        dependencies .. '/earcut.hpp/include/mapbox',
+        dependencies .. '/libtess2/Include',
+        yoga,
+    })
+    files({ '../src/**.cpp', dependencies .. '/libtess2/Source/**.c' })
+    buildoptions({ '-Wall', '-fno-exceptions', '-fno-rtti', '-Werror=format' })
+
+    filter('configurations:debug')
+    do
+        buildoptions({ '-g' })
+        defines({ 'DEBUG' })
+        symbols('On')
+    end
+
+    filter('configurations:release')
+    do
+        buildoptions({ '-flto=full' })
+        defines({ 'RELEASE', 'NDEBUG' })
+        optimize('On')
+    end
+
+    filter({ 'options:graphics=gl' })
+    do
+        defines({ 'SOKOL_GLCORE33' })
+    end
+
+    filter({ 'options:graphics=metal' })
+    do
+        defines({ 'SOKOL_METAL' })
+    end
+
+    filter({ 'options:graphics=d3d' })
+    do
+        defines({ 'SOKOL_D3D11' })
+    end
+
+    newoption({
+        trigger = 'graphics',
+        value = 'gl',
+        description = 'The graphics api to use.',
+        allowed = { { 'gl' }, { 'metal' }, { 'd3d' } },
+    })
+end
+
+project('rive_tess_tests')
+do
+    dependson('rive_tess_renderer')
+    dependson('rive')
+    kind('ConsoleApp')
+    language('C++')
+    cppdialect('C++17')
+    toolset('clang')
+    targetdir('%{cfg.system}/bin/%{cfg.buildcfg}')
+    objdir('%{cfg.system}/obj/%{cfg.buildcfg}')
+    includedirs({
+        rive .. 'dev/test/include', -- for catch.hpp
+        rive .. 'test', -- for things like rive_file_reader.hpp
+        '../include',
+        rive .. '/include',
+        dependencies .. '/sokol',
+        dependencies .. '/earcut.hpp/include/mapbox',
+        yoga,
+    })
+    files({ '../test/**.cpp', rive .. 'utils/no_op_factory.cpp' })
+    links({ 'rive_tess_renderer', 'rive', 'rive_harfbuzz', 'rive_sheenbidi', 'rive_yoga' })
+    buildoptions({ '-Wall', '-fno-exceptions', '-fno-rtti', '-Werror=format' })
+    defines({ 'TESTING', 'YOGA_EXPORT=' })
+
+    filter('configurations:debug')
+    do
+        buildoptions({ '-g' })
+        defines({ 'DEBUG' })
+        symbols('On')
+    end
+
+    filter('configurations:release')
+    do
+        buildoptions({ '-flto=full' })
+        defines({ 'RELEASE', 'NDEBUG' })
+        optimize('On')
+    end
+end
diff --git a/tess/include/rive/math/mat4.hpp b/tess/include/rive/math/mat4.hpp
new file mode 100644
index 0000000..e8b7066
--- /dev/null
+++ b/tess/include/rive/math/mat4.hpp
@@ -0,0 +1,72 @@
+#ifndef _RIVE_MAT4_HPP_
+#define _RIVE_MAT4_HPP_
+
+#include <cstddef>
+
+namespace rive
+{
+class Mat2D;
+class Mat4
+{
+private:
+    float m_Buffer[16];
+
+public:
+    Mat4() :
+        m_Buffer{
+            // clang-format off
+                1.0f, 0.0f, 0.0f, 0.0f,
+                0.0f, 1.0f, 0.0f, 0.0f,
+                0.0f, 0.0f, 1.0f, 0.0f, 
+                0.0f, 0.0f, 0.0f, 1.0f,
+            // clang-format on
+        }
+    {}
+    Mat4(const Mat4& copy) = default;
+
+    // Construct a 4x4 Matrix with the provided elements stored in row-major
+    // order.
+    Mat4(
+        // clang-format off
+            float x1, float y1, float z1, float w1,
+            float x2, float y2, float z2, float w2,
+            float x3, float y3, float z3, float w3,
+            float tx, float ty, float tz, float tw
+        // clang-format on
+        ) :
+        m_Buffer{
+            // clang-format off
+                x1, y1, z1, w1,
+                x2, y2, z2, w2,
+                x3, y3, z3, w3,
+                tx, ty, tz, tw,
+            // clang-format on
+        }
+    {}
+
+    Mat4(const Mat2D& mat2d);
+
+    inline const float* values() const { return m_Buffer; }
+
+    float& operator[](std::size_t idx) { return m_Buffer[idx]; }
+    const float& operator[](std::size_t idx) const { return m_Buffer[idx]; }
+
+    Mat4& operator*=(const Mat4& rhs)
+    {
+        *this = Mat4::multiply(*this, rhs);
+        return *this;
+    }
+
+    Mat4& operator*=(const Mat2D& rhs)
+    {
+        *this = Mat4::multiply(*this, rhs);
+        return *this;
+    }
+
+    static Mat4 multiply(const Mat4& a, const Mat4& b);
+    static Mat4 multiply(const Mat4& a, const Mat2D& b);
+};
+inline Mat4 operator*(const Mat4& a, const Mat4& b) { return Mat4::multiply(a, b); }
+inline Mat4 operator*(const Mat4& a, const Mat2D& b) { return Mat4::multiply(a, b); }
+} // namespace rive
+#endif
diff --git a/tess/include/rive/tess/contour_stroke.hpp b/tess/include/rive/tess/contour_stroke.hpp
new file mode 100644
index 0000000..3f70413
--- /dev/null
+++ b/tess/include/rive/tess/contour_stroke.hpp
@@ -0,0 +1,38 @@
+#ifndef _RIVE_CONTOUR_STROKE_HPP_
+#define _RIVE_CONTOUR_STROKE_HPP_
+
+#include "rive/renderer.hpp"
+#include "rive/math/aabb.hpp"
+#include "rive/math/mat2d.hpp"
+#include <vector>
+#include <cstdint>
+
+namespace rive
+{
+class SegmentedContour;
+
+///
+/// Builds a triangle strip vertex buffer from a SegmentedContour.
+///
+class ContourStroke
+{
+protected:
+    std::vector<Vec2D> m_TriangleStrip;
+    std::vector<std::size_t> m_Offsets;
+    uint32_t m_RenderOffset = 0;
+
+public:
+    const std::vector<Vec2D>& triangleStrip() const { return m_TriangleStrip; }
+
+    void reset();
+    void resetRenderOffset();
+    bool nextRenderOffset(std::size_t& start, std::size_t& end);
+
+    void extrude(const SegmentedContour* contour,
+                 bool isClosed,
+                 StrokeJoin join,
+                 StrokeCap cap,
+                 float strokeWidth);
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/tess/include/rive/tess/segmented_contour.hpp b/tess/include/rive/tess/segmented_contour.hpp
new file mode 100644
index 0000000..4195cb1
--- /dev/null
+++ b/tess/include/rive/tess/segmented_contour.hpp
@@ -0,0 +1,44 @@
+#ifndef _RIVE_SEGMENTED_CONTOUR_HPP_
+#define _RIVE_SEGMENTED_CONTOUR_HPP_
+
+#include "rive/math/vec2d.hpp"
+#include "rive/math/mat2d.hpp"
+#include "rive/math/aabb.hpp"
+#include <vector>
+
+namespace rive
+{
+class RawPath;
+
+/// Utilty for converting a RawPath into a contour segments.
+class SegmentedContour
+{
+private:
+    std::vector<Vec2D> m_contourPoints;
+
+    AABB m_bounds;
+    float m_threshold;
+    float m_thresholdSquared;
+
+    void addVertex(Vec2D vertex);
+    void segmentCubic(const Vec2D& from,
+                      const Vec2D& fromOut,
+                      const Vec2D& toIn,
+                      const Vec2D& to,
+                      float t1,
+                      float t2);
+
+public:
+    const Span<const Vec2D> contourPoints(uint32_t endOffset = 0) const;
+    const std::size_t contourSize() const;
+
+    SegmentedContour(float threshold);
+
+    float threshold() const;
+    void threshold(float value);
+    const AABB& bounds() const;
+
+    void contour(const RawPath& rawPath, const Mat2D& transform);
+};
+} // namespace rive
+#endif
diff --git a/tess/include/rive/tess/sokol/sokol_factory.hpp b/tess/include/rive/tess/sokol/sokol_factory.hpp
new file mode 100644
index 0000000..6d66d4d
--- /dev/null
+++ b/tess/include/rive/tess/sokol/sokol_factory.hpp
@@ -0,0 +1,44 @@
+//
+// Copyright 2022 Rive
+//
+
+#ifndef _RIVE_SOKOL_FACTORY_HPP_
+#define _RIVE_SOKOL_FACTORY_HPP_
+
+#include "rive/factory.hpp"
+
+namespace rive
+{
+
+class SokolFactory : public Factory
+{
+
+public:
+    SokolFactory();
+
+    rcp<RenderBuffer> makeRenderBuffer(RenderBufferType, RenderBufferFlags, size_t) override;
+
+    rcp<RenderShader> makeLinearGradient(float sx,
+                                         float sy,
+                                         float ex,
+                                         float ey,
+                                         const ColorInt colors[], // [count]
+                                         const float stops[],     // [count]
+                                         size_t count) override;
+
+    rcp<RenderShader> makeRadialGradient(float cx,
+                                         float cy,
+                                         float radius,
+                                         const ColorInt colors[], // [count]
+                                         const float stops[],     // [count]
+                                         size_t count) override;
+
+    // Returns a full-formed RenderPath -- can be treated as immutable
+    rcp<RenderPath> makeRenderPath(RawPath&, FillRule) override;
+
+    rcp<RenderPath> makeEmptyRenderPath() override;
+
+    rcp<RenderPaint> makeRenderPaint() override;
+};
+} // namespace rive
+#endif
diff --git a/tess/include/rive/tess/sokol/sokol_tess_renderer.hpp b/tess/include/rive/tess/sokol/sokol_tess_renderer.hpp
new file mode 100644
index 0000000..67454fa
--- /dev/null
+++ b/tess/include/rive/tess/sokol/sokol_tess_renderer.hpp
@@ -0,0 +1,105 @@
+//
+// Copyright 2022 Rive
+//
+
+#ifndef _RIVE_SOKOL_TESS_RENDERER_HPP_
+#define _RIVE_SOKOL_TESS_RENDERER_HPP_
+
+#include "rive/tess/tess_renderer.hpp"
+#include "sokol_gfx.h"
+
+namespace rive
+{
+
+// The actual graphics device image.
+class SokolRenderImageResource : public RefCnt<SokolRenderImageResource>
+{
+private:
+    sg_image m_gpuResource;
+
+public:
+    // bytes is expected to be tightly packed RGBA*width*height.
+    SokolRenderImageResource(const uint8_t* bytes, uint32_t width, uint32_t height);
+    ~SokolRenderImageResource();
+
+    sg_image image() const { return m_gpuResource; }
+};
+
+// 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 lite_rtti_override<RenderImage, SokolRenderImage>
+{
+private:
+    rcp<SokolRenderImageResource> m_gpuImage;
+    sg_buffer m_vertexBuffer;
+    sg_buffer m_uvBuffer;
+
+public:
+    // Needed by std::unique_ptr
+    // SokolRenderImage() {}
+
+    SokolRenderImage(rcp<SokolRenderImageResource> image,
+                     uint32_t width,
+                     uint32_t height,
+                     const Mat2D& uvTransform);
+
+    ~SokolRenderImage() override;
+
+    sg_image image() const { return m_gpuImage->image(); }
+    sg_buffer vertexBuffer() const { return m_vertexBuffer; }
+    sg_buffer uvBuffer() const { return m_uvBuffer; }
+};
+
+class SokolTessRenderer : public TessRenderer
+{
+private:
+    static const std::size_t maxClippingPaths = 16;
+    sg_pipeline m_meshPipeline;
+    sg_pipeline m_currentPipeline = {0};
+    int m_clipCount = 0;
+
+    // Src Over Pipelines
+    sg_pipeline m_pathPipeline[maxClippingPaths + 1];
+
+    // Screen Pipelines
+    sg_pipeline m_pathScreenPipeline[maxClippingPaths + 1];
+
+    // Additive
+    sg_pipeline m_pathAdditivePipeline[maxClippingPaths + 1];
+
+    // Multiply
+    sg_pipeline m_pathMultiplyPipeline[maxClippingPaths + 1];
+
+    sg_pipeline m_incClipPipeline;
+    sg_pipeline m_decClipPipeline;
+    sg_buffer m_boundsIndices;
+
+    std::vector<SubPath> m_ClipPaths;
+
+    void applyClipping();
+    void setPipeline(sg_pipeline pipeline);
+
+public:
+    SokolTessRenderer();
+    ~SokolTessRenderer();
+    void orthographicProjection(float left,
+                                float right,
+                                float bottom,
+                                float top,
+                                float near,
+                                float far) 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,
+                       uint32_t vertexCount,
+                       uint32_t indexCount,
+                       BlendMode,
+                       float opacity) override;
+    void restore() override;
+    void reset();
+};
+} // namespace rive
+#endif
diff --git a/tess/include/rive/tess/sub_path.hpp b/tess/include/rive/tess/sub_path.hpp
new file mode 100644
index 0000000..7c9259a
--- /dev/null
+++ b/tess/include/rive/tess/sub_path.hpp
@@ -0,0 +1,26 @@
+#ifndef _RIVE_SUB_PATH_HPP_
+#define _RIVE_SUB_PATH_HPP_
+
+#include "rive/renderer.hpp"
+#include "rive/math/mat2d.hpp"
+
+namespace rive
+{
+///
+/// A reference to a sub-path added to a TessRenderPath with its relative
+/// transform.
+///
+class SubPath
+{
+private:
+    RenderPath* m_Path;
+    Mat2D m_Transform;
+
+public:
+    SubPath(RenderPath* path, const Mat2D& transform);
+
+    RenderPath* path() const;
+    const Mat2D& transform() const;
+};
+} // namespace rive
+#endif
\ No newline at end of file
diff --git a/tess/include/rive/tess/tess_render_path.hpp b/tess/include/rive/tess/tess_render_path.hpp
new file mode 100644
index 0000000..b374c75
--- /dev/null
+++ b/tess/include/rive/tess/tess_render_path.hpp
@@ -0,0 +1,70 @@
+#ifndef _RIVE_TESS_RENDER_PATH_HPP_
+#define _RIVE_TESS_RENDER_PATH_HPP_
+
+#include "rive/math/raw_path.hpp"
+#include "rive/renderer.hpp"
+#include "rive/span.hpp"
+#include "rive/tess/segmented_contour.hpp"
+#include "rive/tess/sub_path.hpp"
+#include "earcut.hpp"
+
+namespace rive
+{
+
+class ContourStroke;
+class TessRenderPath : public lite_rtti_override<RenderPath, TessRenderPath>
+{
+private:
+    // TessRenderPath stores a RawPath so that it can use utility classes
+    // that will work off of RawPath (like segmenting the contour and then
+    // triangulating the segmented contour).
+    RawPath m_rawPath;
+    FillRule m_fillRule;
+
+    // We hold a reference to the segmented contour so it can reserve and
+    // reuse storage when re-contouring.
+    SegmentedContour m_segmentedContour;
+
+    mapbox::detail::Earcut<uint16_t> m_earcut;
+
+    bool m_isContourDirty = true;
+    bool m_isTriangulationDirty = true;
+    Mat2D m_contourTransform;
+    bool m_isClosed = false;
+
+protected:
+    std::vector<SubPath> m_subPaths;
+    virtual void addTriangles(Span<const Vec2D> vertices, Span<const uint16_t> indices) = 0;
+    virtual void setTriangulatedBounds(const AABB& value) = 0;
+    void contour(const Mat2D& transform);
+    void triangulate(TessRenderPath* containerPath);
+
+public:
+    TessRenderPath();
+    TessRenderPath(RawPath&, FillRule);
+    ~TessRenderPath();
+    void rewind() override;
+    void fillRule(FillRule value) override;
+    bool empty() const;
+
+    // In Rive a Path is used as a simple container (with no commands) when
+    // it aggregates multiple paths.
+    bool isContainer() const { return !m_subPaths.empty(); }
+
+    void moveTo(float x, float y) override;
+    void lineTo(float x, float y) override;
+    void cubicTo(float ox, float oy, float ix, float iy, float x, float y) override;
+    void close() override;
+    void addRenderPath(RenderPath* path, const Mat2D& transform) override;
+
+    const SegmentedContour& segmentedContour() const;
+    bool triangulate();
+    void extrudeStroke(ContourStroke* stroke,
+                       StrokeJoin join,
+                       StrokeCap cap,
+                       float strokeWidth,
+                       const Mat2D& transform);
+    const RawPath& rawPath() const;
+};
+} // namespace rive
+#endif
diff --git a/tess/include/rive/tess/tess_renderer.hpp b/tess/include/rive/tess/tess_renderer.hpp
new file mode 100644
index 0000000..3387ffb
--- /dev/null
+++ b/tess/include/rive/tess/tess_renderer.hpp
@@ -0,0 +1,58 @@
+//
+// Copyright 2022 Rive
+//
+
+#ifndef _RIVE_TESS_RENDERER_HPP_
+#define _RIVE_TESS_RENDERER_HPP_
+
+#include "rive/renderer.hpp"
+#include "rive/tess/sub_path.hpp"
+#include "rive/math/mat2d.hpp"
+#include "rive/math/mat4.hpp"
+#include <vector>
+#include <list>
+
+namespace rive
+{
+
+struct RenderState
+{
+    Mat2D transform;
+    std::vector<SubPath> clipPaths;
+};
+
+class TessRenderer : public Renderer
+{
+protected:
+    Mat4 m_Projection;
+    std::list<RenderState> m_Stack;
+    bool m_IsClippingDirty = false;
+
+public:
+    TessRenderer();
+
+    void projection(const Mat4& value);
+    virtual void orthographicProjection(float left,
+                                        float right,
+                                        float bottom,
+                                        float top,
+                                        float near,
+                                        float far) = 0;
+
+    void save() override;
+    void restore() override;
+    void transform(const Mat2D& transform) override;
+    const Mat2D& transform() { return m_Stack.back().transform; }
+    void clipPath(RenderPath* path) 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,
+                       uint32_t vertexCount,
+                       uint32_t indexCount,
+                       BlendMode,
+                       float opacity) override;
+};
+} // namespace rive
+#endif
diff --git a/tess/src/contour_stroke.cpp b/tess/src/contour_stroke.cpp
new file mode 100644
index 0000000..0a782b3
--- /dev/null
+++ b/tess/src/contour_stroke.cpp
@@ -0,0 +1,351 @@
+#include "rive/math/math_types.hpp"
+#include "rive/tess/contour_stroke.hpp"
+#include "rive/tess/segmented_contour.hpp"
+#include "rive/math/vec2d.hpp"
+#include <assert.h>
+#include <algorithm>
+
+using namespace rive;
+
+static const int subdivisionArcLength = 4.0f;
+
+void ContourStroke::reset()
+{
+    m_TriangleStrip.clear();
+    m_Offsets.clear();
+}
+
+void ContourStroke::resetRenderOffset() { m_RenderOffset = 0; }
+
+bool ContourStroke::nextRenderOffset(std::size_t& start, std::size_t& end)
+{
+    if (m_RenderOffset == m_Offsets.size())
+    {
+        return false;
+    }
+    start = m_RenderOffset == 0 ? 0 : m_Offsets[m_RenderOffset - 1];
+    end = m_Offsets[m_RenderOffset++];
+    return true;
+}
+
+void ContourStroke::extrude(const SegmentedContour* contour,
+                            bool isClosed,
+                            StrokeJoin join,
+                            StrokeCap cap,
+                            float strokeWidth)
+{
+    auto contourPoints = contour->contourPoints();
+    std::vector<Vec2D> points(contourPoints.begin(), contourPoints.end());
+
+    auto pointCount = points.size();
+    if (pointCount < 2)
+    {
+        return;
+    }
+    auto startOffset = m_TriangleStrip.size();
+    Vec2D lastPoint = points[0];
+    Vec2D lastDiff = points[1] - lastPoint;
+
+    float lastLength = lastDiff.length();
+    Vec2D lastDiffNormalized = lastDiff / lastLength;
+
+    Vec2D perpendicularStrokeDiff =
+        Vec2D(lastDiffNormalized.y * -strokeWidth, lastDiffNormalized.x * strokeWidth);
+    Vec2D lastA = lastPoint + perpendicularStrokeDiff;
+    Vec2D lastB = lastPoint - perpendicularStrokeDiff;
+
+    if (!isClosed)
+    {
+        switch (cap)
+        {
+            case StrokeCap::square:
+            {
+                Vec2D strokeDiff = lastDiffNormalized * strokeWidth;
+                Vec2D squareA = lastA - strokeDiff;
+                Vec2D squareB = lastB - strokeDiff;
+                m_TriangleStrip.push_back(squareA);
+                m_TriangleStrip.push_back(squareB);
+                break;
+            }
+            case StrokeCap::round:
+            {
+                Vec2D capDirection = Vec2D(-lastDiffNormalized.y, lastDiffNormalized.x);
+                float arcLength = std::abs(math::PI * strokeWidth);
+                int steps = (int)std::ceil(arcLength / subdivisionArcLength);
+                float angleTo = std::atan2(capDirection.y, capDirection.x);
+                float inc = math::PI / steps;
+                float angle = angleTo;
+                // make sure to draw the full cap due triangle strip
+                for (int j = 0; j <= steps; j++)
+                {
+                    m_TriangleStrip.push_back(lastPoint);
+                    m_TriangleStrip.push_back(Vec2D(lastPoint.x + std::cos(angle) * strokeWidth,
+                                                    lastPoint.y + std::sin(angle) * strokeWidth));
+                    angle += inc;
+                }
+                break;
+            }
+            default:
+                break;
+        }
+    }
+    m_TriangleStrip.push_back(lastA);
+    m_TriangleStrip.push_back(lastB);
+
+    pointCount -= isClosed ? 1 : 0;
+    std::size_t adjustedPointCount = isClosed ? pointCount + 1 : pointCount;
+
+    for (std::size_t i = 1; i < adjustedPointCount; i++)
+    {
+        const Vec2D& point = points[i % pointCount];
+        Vec2D diff, diffNormalized, next;
+        float length;
+        if (i < adjustedPointCount - 1 || isClosed)
+        {
+            diff = (next = points[(i + 1) % pointCount]) - point;
+            length = diff.length();
+            diffNormalized = diff / length;
+        }
+        else
+        {
+            diff = lastDiff;
+            next = point;
+            length = lastLength;
+            diffNormalized = lastDiffNormalized;
+        }
+
+        // perpendicular dx
+        float pdx0 = -lastDiffNormalized.y;
+        float pdy0 = lastDiffNormalized.x;
+        float pdx1 = -diffNormalized.y;
+        float pdy1 = diffNormalized.x;
+
+        // Compute bisector without a normalization by averaging perpendicular
+        // diffs.
+        Vec2D bisector((pdx0 + pdx1) * 0.5f, (pdy0 + pdy1) * 0.5f);
+        float cross = Vec2D::cross(diff, lastDiff);
+        float dot = Vec2D::dot(bisector, bisector);
+
+        float lengthLimit = std::min(length, lastLength);
+        bool bevelInner = false;
+        bool bevel = join == StrokeJoin::miter ? dot < 0.1f : dot < 0.999f;
+
+        // Scale bisector to match stroke size.
+        if (dot > 0.000001f)
+        {
+            float scale = 1.0f / dot * strokeWidth;
+            float limit = lengthLimit / strokeWidth;
+            if (dot * limit * limit < 1.0f)
+            {
+                bevelInner = true;
+            }
+            bisector *= scale;
+        }
+        else
+        {
+            bisector *= strokeWidth;
+        }
+
+        if (!bevel)
+        {
+            Vec2D c = point + bisector;
+            Vec2D d = point - bisector;
+
+            if (!bevelInner)
+            {
+                // Normal mitered edge.
+                m_TriangleStrip.push_back(c);
+                m_TriangleStrip.push_back(d);
+            }
+            else if (cross <= 0)
+            {
+                // Overlap the inner (in this case right) edge (sometimes called
+                // miter inner).
+                Vec2D c1 = point + Vec2D(lastDiffNormalized.y * -strokeWidth,
+                                         lastDiffNormalized.x * strokeWidth);
+                Vec2D c2 =
+                    point + Vec2D(diffNormalized.y * -strokeWidth, diffNormalized.x * strokeWidth);
+
+                m_TriangleStrip.push_back(c1);
+                m_TriangleStrip.push_back(d);
+                m_TriangleStrip.push_back(c2);
+                m_TriangleStrip.push_back(d);
+            }
+            else
+            {
+                // Overlap the inner (in this case left) edge (sometimes called
+                // miter inner).
+                Vec2D d1 = point - Vec2D(lastDiffNormalized.y * -strokeWidth,
+                                         lastDiffNormalized.x * strokeWidth);
+                Vec2D d2 =
+                    point - Vec2D(diffNormalized.y * -strokeWidth, diffNormalized.x * strokeWidth);
+
+                m_TriangleStrip.push_back(c);
+                m_TriangleStrip.push_back(d1);
+                m_TriangleStrip.push_back(c);
+                m_TriangleStrip.push_back(d2);
+            }
+        }
+        else
+        {
+            Vec2D ldPStroke =
+                Vec2D(lastDiffNormalized.y * -strokeWidth, lastDiffNormalized.x * strokeWidth);
+            Vec2D dPStroke = Vec2D(diffNormalized.y * -strokeWidth, diffNormalized.x * strokeWidth);
+            if (cross <= 0)
+            {
+                // Bevel the outer (left in this case) edge.
+                Vec2D a1;
+                Vec2D a2;
+
+                if (bevelInner)
+                {
+                    a1 = point + ldPStroke;
+                    a2 = point + dPStroke;
+                }
+                else
+                {
+                    a1 = point + bisector;
+                    a2 = a1;
+                }
+
+                Vec2D b = point - ldPStroke;
+                Vec2D bn = point - dPStroke;
+
+                m_TriangleStrip.push_back(a1);
+                m_TriangleStrip.push_back(b);
+                if (join == StrokeJoin::round)
+                {
+                    const Vec2D& pivot = bevelInner ? point : a1;
+                    Vec2D toPrev = bn - point;
+                    Vec2D toNext = b - point;
+                    float angleFrom = std::atan2(toPrev.y, toPrev.x);
+                    float angleTo = std::atan2(toNext.y, toNext.x);
+                    if (angleTo > angleFrom)
+                    {
+                        angleTo -= math::PI * 2.0f;
+                    }
+                    float range = angleTo - angleFrom;
+                    float arcLength = std::abs(range * strokeWidth);
+                    int steps = std::ceil(arcLength / subdivisionArcLength);
+
+                    float inc = range / steps;
+                    float angle = angleTo - inc;
+                    for (int j = 0; j < steps - 1; j++)
+                    {
+                        m_TriangleStrip.push_back(pivot);
+                        m_TriangleStrip.emplace_back(
+                            Vec2D(point.x + std::cos(angle) * strokeWidth,
+                                  point.y + std::sin(angle) * strokeWidth));
+
+                        angle -= inc;
+                    }
+                }
+                m_TriangleStrip.push_back(a2);
+                m_TriangleStrip.push_back(bn);
+            }
+            else
+            {
+                // Bevel the outer (right in this case) edge.
+                Vec2D b1;
+                Vec2D b2;
+                if (bevelInner)
+                {
+                    b1 = point - ldPStroke;
+                    b2 = point - dPStroke;
+                }
+                else
+                {
+                    b1 = point - bisector;
+                    b2 = b1;
+                }
+
+                Vec2D a = point + ldPStroke;
+                Vec2D an = point + dPStroke;
+
+                m_TriangleStrip.push_back(a);
+                m_TriangleStrip.push_back(b1);
+
+                if (join == StrokeJoin::round)
+                {
+                    const Vec2D& pivot = bevelInner ? point : b1;
+                    Vec2D toPrev = a - point;
+                    Vec2D toNext = an - point;
+                    float angleFrom = std::atan2(toPrev.y, toPrev.x);
+                    float angleTo = std::atan2(toNext.y, toNext.x);
+                    if (angleTo > angleFrom)
+                    {
+                        angleTo -= math::PI * 2.0f;
+                    }
+
+                    float range = angleTo - angleFrom;
+                    float arcLength = std::abs(range * strokeWidth);
+                    int steps = std::ceil(arcLength / subdivisionArcLength);
+                    float inc = range / steps;
+
+                    float angle = angleFrom + inc;
+                    for (int j = 0; j < steps - 1; j++)
+                    {
+                        m_TriangleStrip.emplace_back(
+                            Vec2D(point.x + std::cos(angle) * strokeWidth,
+                                  point.y + std::sin(angle) * strokeWidth));
+                        m_TriangleStrip.push_back(pivot);
+                        angle += inc;
+                    }
+                }
+                m_TriangleStrip.push_back(an);
+                m_TriangleStrip.push_back(b2);
+            }
+        }
+
+        lastPoint = point;
+        lastDiff = diff;
+        lastDiffNormalized = diffNormalized;
+    }
+
+    if (isClosed)
+    {
+        auto last = m_TriangleStrip.size() - 1;
+        m_TriangleStrip[startOffset] = m_TriangleStrip[last - 1];
+        m_TriangleStrip[startOffset + 1] = m_TriangleStrip[last];
+    }
+    else
+    {
+        switch (cap)
+        {
+            case StrokeCap::square:
+            {
+                auto l = m_TriangleStrip.size();
+
+                Vec2D strokeDiff = lastDiffNormalized * strokeWidth;
+                Vec2D squareA = m_TriangleStrip[l - 2] + strokeDiff;
+                Vec2D squareB = m_TriangleStrip[l - 1] + strokeDiff;
+
+                m_TriangleStrip.push_back(squareA);
+                m_TriangleStrip.push_back(squareB);
+                break;
+            }
+            case StrokeCap::round:
+            {
+                Vec2D capDirection = Vec2D(-lastDiffNormalized.y, lastDiffNormalized.x);
+                float arcLength = std::abs(math::PI * strokeWidth);
+                int steps = (int)std::ceil(arcLength / subdivisionArcLength);
+                float angleTo = std::atan2(capDirection.y, capDirection.x);
+                float inc = math::PI / steps;
+                float angle = angleTo;
+                // make sure to draw the full cap due triangle strip
+                for (int j = 0; j <= steps; j++)
+                {
+                    m_TriangleStrip.push_back(lastPoint);
+                    m_TriangleStrip.push_back(Vec2D(lastPoint.x + std::cos(angle) * strokeWidth,
+                                                    lastPoint.y + std::sin(angle) * strokeWidth));
+                    angle -= inc;
+                }
+                break;
+            }
+            default:
+                break;
+        }
+    }
+
+    m_Offsets.push_back(m_TriangleStrip.size());
+}
\ No newline at end of file
diff --git a/tess/src/math/mat4.cpp b/tess/src/math/mat4.cpp
new file mode 100644
index 0000000..6e1da14
--- /dev/null
+++ b/tess/src/math/mat4.cpp
@@ -0,0 +1,70 @@
+#include "rive/math/mat4.hpp"
+#include "rive/math/mat2d.hpp"
+#include <cmath>
+
+using namespace rive;
+
+Mat4::Mat4(const Mat2D& mat2d) :
+    m_Buffer{
+        // clang-format off
+        mat2d[0], mat2d[1], 0.0f, 0.0f,
+        mat2d[2], mat2d[3], 0.0f, 0.0f,
+        0.0f, 0.0f, 1.0f, 0.0f, 
+        mat2d[4], mat2d[5], 0.0f, 1.0f,
+        // clang-format on
+    }
+{}
+
+Mat4 Mat4::multiply(const Mat4& a, const Mat4& b)
+{
+    return {
+        // clang-format off
+        b[0] * a[0] + b[1] * a[4] + b[2] * a[8] + b[3] * a[12],
+        b[0] * a[1] + b[1] * a[5] + b[2] * a[9] + b[3] * a[13],
+        b[0] * a[2] + b[1] * a[6] + b[2] * a[10] + b[3] * a[14],
+        b[0] * a[3] + b[1] * a[7] + b[2] * a[11] + b[3] * a[15],
+
+        b[4] * a[0] + b[5] * a[4] + b[6] * a[8] + b[7] * a[12],
+        b[4] * a[1] + b[5] * a[5] + b[6] * a[9] + b[7] * a[13],
+        b[4] * a[2] + b[5] * a[6] + b[6] * a[10] + b[7] * a[14],
+        b[4] * a[3] + b[5] * a[7] + b[6] * a[11] + b[7] * a[15],
+
+        b[8] * a[0] + b[9] * a[4] + b[10] * a[8] + b[11] * a[12],
+        b[8] * a[1] + b[9] * a[5] + b[10] * a[9] + b[11] * a[13],
+        b[8] * a[2] + b[9] * a[6] + b[10] * a[10] + b[11] * a[14],
+        b[8] * a[3] + b[9] * a[7] + b[10] * a[11] + b[11] * a[15],
+
+        b[12] * a[0] + b[13] * a[4] + b[14] * a[8] + b[15] * a[12],
+        b[12] * a[1] + b[13] * a[5] + b[14] * a[9] + b[15] * a[13],
+        b[12] * a[2] + b[13] * a[6] + b[14] * a[10] + b[15] * a[14],
+        b[12] * a[3] + b[13] * a[7] + b[14] * a[11] + b[15] * a[15],
+        // clang-format on
+    };
+}
+
+Mat4 Mat4::multiply(const Mat4& a, const Mat2D& b)
+{
+    return {
+        // clang-format off
+        b[0] * a[0] + b[1] * a[4],
+        b[0] * a[1] + b[1] * a[5],
+        b[0] * a[2] + b[1] * a[6],
+        b[0] * a[3] + b[1] * a[7],
+
+        b[2] * a[0] + b[3] * a[4],
+        b[2] * a[1] + b[3] * a[5],
+        b[2] * a[2] + b[3] * a[6],
+        b[2] * a[3] + b[3] * a[7],
+
+        a[8],
+        a[9],
+        a[10],
+        a[11],
+
+        b[4] * a[0] + b[5] * a[4] + a[12],
+        b[4] * a[1] + b[5] * a[5] + a[13],
+        b[4] * a[2] + b[5] * a[6] + a[14],
+        b[4] * a[3] + b[5] * a[7] + a[15],
+        // clang-format on
+    };
+}
diff --git a/tess/src/segmented_contour.cpp b/tess/src/segmented_contour.cpp
new file mode 100644
index 0000000..92f8c0f
--- /dev/null
+++ b/tess/src/segmented_contour.cpp
@@ -0,0 +1,97 @@
+#include "rive/tess/segmented_contour.hpp"
+#include "rive/math/raw_path.hpp"
+#include "rive/math/cubic_utilities.hpp"
+
+using namespace rive;
+
+SegmentedContour::SegmentedContour(float threshold) :
+    m_bounds(AABB::forExpansion()),
+    m_threshold(threshold),
+    m_thresholdSquared(threshold * threshold)
+{}
+
+float SegmentedContour::threshold() const { return m_threshold; }
+void SegmentedContour::threshold(float value)
+{
+    m_threshold = value;
+    m_thresholdSquared = value * value;
+}
+const AABB& SegmentedContour::bounds() const { return m_bounds; }
+void SegmentedContour::addVertex(Vec2D vertex)
+{
+    m_contourPoints.push_back(vertex);
+    AABB::expandTo(m_bounds, vertex);
+}
+
+const std::size_t SegmentedContour::contourSize() const { return m_contourPoints.size(); }
+
+const Span<const Vec2D> SegmentedContour::contourPoints(uint32_t endOffset) const
+{
+    assert(endOffset <= m_contourPoints.size());
+    return Span<const Vec2D>(m_contourPoints.data(), m_contourPoints.size() - endOffset);
+}
+
+void SegmentedContour::segmentCubic(const Vec2D& from,
+                                    const Vec2D& fromOut,
+                                    const Vec2D& toIn,
+                                    const Vec2D& to,
+                                    float t1,
+                                    float t2)
+{
+    if (CubicUtilities::shouldSplitCubic(from, fromOut, toIn, to, m_threshold))
+    {
+        float halfT = (t1 + t2) / 2.0f;
+
+        Vec2D hull[6];
+        CubicUtilities::computeHull(from, fromOut, toIn, to, 0.5f, hull);
+
+        segmentCubic(from, hull[0], hull[3], hull[5], t1, halfT);
+
+        segmentCubic(hull[5], hull[4], hull[2], to, halfT, t2);
+    }
+    else
+    {
+        if (Vec2D::distanceSquared(from, to) > m_thresholdSquared)
+        {
+            addVertex(Vec2D(CubicUtilities::cubicAt(t2, from.x, fromOut.x, toIn.x, to.x),
+                            CubicUtilities::cubicAt(t2, from.y, fromOut.y, toIn.y, to.y)));
+        }
+    }
+}
+
+void SegmentedContour::contour(const RawPath& rawPath, const Mat2D& transform)
+{
+    m_contourPoints.clear();
+
+    // Possible perf consideration: could add second path that doesn't transform
+    // if transform is the identity.
+    for (auto tuple : rawPath)
+    {
+        PathVerb verb = std::get<0>(tuple);
+        const Vec2D* pts = std::get<1>(tuple);
+
+        switch (verb)
+        {
+            case PathVerb::move:
+                addVertex(transform * pts[0]);
+                break;
+            case PathVerb::line:
+                addVertex(transform * pts[1]);
+                break;
+            case PathVerb::cubic:
+                segmentCubic(transform * pts[0],
+                             transform * pts[1],
+                             transform * pts[2],
+                             transform * pts[3],
+                             0.0f,
+                             1.0f);
+                break;
+            case PathVerb::close:
+                break;
+            case PathVerb::quad:
+                // TODO: not currently used by render paths, however might be
+                // necessary for fonts.
+                break;
+        }
+    }
+}
diff --git a/tess/src/sokol/generated/.clang-format b/tess/src/sokol/generated/.clang-format
new file mode 100644
index 0000000..a43d914
--- /dev/null
+++ b/tess/src/sokol/generated/.clang-format
@@ -0,0 +1,2 @@
+DisableFormat: true
+SortIncludes: false
\ No newline at end of file
diff --git a/tess/src/sokol/generated/shader.h b/tess/src/sokol/generated/shader.h
new file mode 100644
index 0000000..e5ba73a
--- /dev/null
+++ b/tess/src/sokol/generated/shader.h
@@ -0,0 +1,3169 @@
+#pragma once
+/*
+    #version:1# (machine generated, don't edit!)
+
+    Generated by sokol-shdc (https://github.com/floooh/sokol-tools)
+
+    Cmdline: sokol-shdc --input ../src/sokol/shader.glsl --output ../src/sokol/generated/shader.h --slang glsl330:hlsl5:metal_macos:metal_ios:metal_sim:glsl300es:glsl100
+
+    Overview:
+
+        Shader program 'rive_tess':
+            Get shader desc: rive_tess_shader_desc(sg_query_backend());
+            Vertex shader: vs
+                Attribute slots:
+                    ATTR_vs_position = 0
+                    ATTR_vs_texcoord0 = 1
+                Uniform block 'vs_params':
+                    C struct: vs_params_t
+                    Bind slot: SLOT_vs_params = 0
+            Fragment shader: fs
+                Image 'tex':
+                    Type: SG_IMAGETYPE_2D
+                    Component Type: SG_SAMPLERTYPE_FLOAT
+                    Bind slot: SLOT_tex = 0
+
+        Shader program 'rive_tess_path':
+            Get shader desc: rive_tess_path_shader_desc(sg_query_backend());
+            Vertex shader: vs_path
+                Attribute slots:
+                    ATTR_vs_path_position = 0
+                Uniform block 'vs_path_params':
+                    C struct: vs_path_params_t
+                    Bind slot: SLOT_vs_path_params = 0
+            Fragment shader: fs_path
+                Uniform block 'fs_path_uniforms':
+                    C struct: fs_path_uniforms_t
+                    Bind slot: SLOT_fs_path_uniforms = 0
+
+
+    Shader descriptor structs:
+
+        sg_shader rive_tess = sg_make_shader(rive_tess_shader_desc(sg_query_backend()));
+        sg_shader rive_tess_path = sg_make_shader(rive_tess_path_shader_desc(sg_query_backend()));
+
+    Vertex attribute locations for vertex shader 'vs':
+
+        sg_pipeline pip = sg_make_pipeline(&(sg_pipeline_desc){
+            .layout = {
+                .attrs = {
+                    [ATTR_vs_position] = { ... },
+                    [ATTR_vs_texcoord0] = { ... },
+                },
+            },
+            ...});
+
+    Vertex attribute locations for vertex shader 'vs_path':
+
+        sg_pipeline pip = sg_make_pipeline(&(sg_pipeline_desc){
+            .layout = {
+                .attrs = {
+                    [ATTR_vs_path_position] = { ... },
+                },
+            },
+            ...});
+
+    Image bind slots, use as index in sg_bindings.vs_images[] or .fs_images[]
+
+        SLOT_tex = 0;
+
+    Bind slot and C-struct for uniform block 'vs_params':
+
+        vs_params_t vs_params = {
+            .mvp = ...;
+        };
+        sg_apply_uniforms(SG_SHADERSTAGE_[VS|FS], SLOT_vs_params, &SG_RANGE(vs_params));
+
+    Bind slot and C-struct for uniform block 'vs_path_params':
+
+        vs_path_params_t vs_path_params = {
+            .mvp = ...;
+            .fillType = ...;
+            .gradientStart = ...;
+            .gradientEnd = ...;
+        };
+        sg_apply_uniforms(SG_SHADERSTAGE_[VS|FS], SLOT_vs_path_params, &SG_RANGE(vs_path_params));
+
+    Bind slot and C-struct for uniform block 'fs_path_uniforms':
+
+        fs_path_uniforms_t fs_path_uniforms = {
+            .fillType = ...;
+            .colors = ...;
+            .stops = ...;
+            .stopCount = ...;
+        };
+        sg_apply_uniforms(SG_SHADERSTAGE_[VS|FS], SLOT_fs_path_uniforms, &SG_RANGE(fs_path_uniforms));
+
+*/
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+#include <stddef.h>
+#if !defined(SOKOL_SHDC_ALIGN)
+  #if defined(_MSC_VER)
+    #define SOKOL_SHDC_ALIGN(a) __declspec(align(a))
+  #else
+    #define SOKOL_SHDC_ALIGN(a) __attribute__((aligned(a)))
+  #endif
+#endif
+#define ATTR_vs_position (0)
+#define ATTR_vs_texcoord0 (1)
+#define ATTR_vs_path_position (0)
+#define SLOT_tex (0)
+#define SLOT_vs_params (0)
+#pragma pack(push,1)
+SOKOL_SHDC_ALIGN(16) typedef struct vs_params_t {
+    rive::Mat4 mvp;
+} vs_params_t;
+#pragma pack(pop)
+#define SLOT_vs_path_params (0)
+#pragma pack(push,1)
+SOKOL_SHDC_ALIGN(16) typedef struct vs_path_params_t {
+    rive::Mat4 mvp;
+    int fillType;
+    uint8_t _pad_68[4];
+    rive::Vec2D gradientStart;
+    rive::Vec2D gradientEnd;
+    uint8_t _pad_88[8];
+} vs_path_params_t;
+#pragma pack(pop)
+#define SLOT_fs_path_uniforms (0)
+#pragma pack(push,1)
+SOKOL_SHDC_ALIGN(16) typedef struct fs_path_uniforms_t {
+    int fillType;
+    uint8_t _pad_4[12];
+    float colors[16][4];
+    float stops[4][4];
+    int stopCount;
+    uint8_t _pad_340[12];
+} fs_path_uniforms_t;
+#pragma pack(pop)
+/*
+    #version 330
+    
+    uniform vec4 vs_params[4];
+    layout(location = 0) in vec2 position;
+    out vec2 uv;
+    layout(location = 1) in vec2 texcoord0;
+    
+    void main()
+    {
+        gl_Position = mat4(vs_params[0], vs_params[1], vs_params[2], vs_params[3]) * vec4(position.x, position.y, 0.0, 1.0);
+        uv = texcoord0;
+    }
+    
+*/
+static const char vs_source_glsl330[293] = {
+    0x23,0x76,0x65,0x72,0x73,0x69,0x6f,0x6e,0x20,0x33,0x33,0x30,0x0a,0x0a,0x75,0x6e,
+    0x69,0x66,0x6f,0x72,0x6d,0x20,0x76,0x65,0x63,0x34,0x20,0x76,0x73,0x5f,0x70,0x61,
+    0x72,0x61,0x6d,0x73,0x5b,0x34,0x5d,0x3b,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,
+    0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x30,0x29,0x20,0x69,0x6e,
+    0x20,0x76,0x65,0x63,0x32,0x20,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3b,0x0a,
+    0x6f,0x75,0x74,0x20,0x76,0x65,0x63,0x32,0x20,0x75,0x76,0x3b,0x0a,0x6c,0x61,0x79,
+    0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x31,
+    0x29,0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x32,0x20,0x74,0x65,0x78,0x63,0x6f,0x6f,
+    0x72,0x64,0x30,0x3b,0x0a,0x0a,0x76,0x6f,0x69,0x64,0x20,0x6d,0x61,0x69,0x6e,0x28,
+    0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,
+    0x69,0x6f,0x6e,0x20,0x3d,0x20,0x6d,0x61,0x74,0x34,0x28,0x76,0x73,0x5f,0x70,0x61,
+    0x72,0x61,0x6d,0x73,0x5b,0x30,0x5d,0x2c,0x20,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,
+    0x6d,0x73,0x5b,0x31,0x5d,0x2c,0x20,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,
+    0x5b,0x32,0x5d,0x2c,0x20,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x33,
+    0x5d,0x29,0x20,0x2a,0x20,0x76,0x65,0x63,0x34,0x28,0x70,0x6f,0x73,0x69,0x74,0x69,
+    0x6f,0x6e,0x2e,0x78,0x2c,0x20,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x2e,0x79,
+    0x2c,0x20,0x30,0x2e,0x30,0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,
+    0x20,0x75,0x76,0x20,0x3d,0x20,0x74,0x65,0x78,0x63,0x6f,0x6f,0x72,0x64,0x30,0x3b,
+    0x0a,0x7d,0x0a,0x0a,0x00,
+};
+/*
+    #version 330
+    
+    uniform sampler2D tex;
+    
+    layout(location = 0) out vec4 frag_color;
+    in vec2 uv;
+    
+    void main()
+    {
+        frag_color = texture(tex, uv);
+    }
+    
+*/
+static const char fs_source_glsl330[146] = {
+    0x23,0x76,0x65,0x72,0x73,0x69,0x6f,0x6e,0x20,0x33,0x33,0x30,0x0a,0x0a,0x75,0x6e,
+    0x69,0x66,0x6f,0x72,0x6d,0x20,0x73,0x61,0x6d,0x70,0x6c,0x65,0x72,0x32,0x44,0x20,
+    0x74,0x65,0x78,0x3b,0x0a,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,
+    0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x30,0x29,0x20,0x6f,0x75,0x74,0x20,0x76,
+    0x65,0x63,0x34,0x20,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x3b,0x0a,
+    0x69,0x6e,0x20,0x76,0x65,0x63,0x32,0x20,0x75,0x76,0x3b,0x0a,0x0a,0x76,0x6f,0x69,
+    0x64,0x20,0x6d,0x61,0x69,0x6e,0x28,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,
+    0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,0x20,0x74,0x65,0x78,0x74,
+    0x75,0x72,0x65,0x28,0x74,0x65,0x78,0x2c,0x20,0x75,0x76,0x29,0x3b,0x0a,0x7d,0x0a,
+    0x0a,0x00,
+};
+/*
+    #version 330
+    
+    struct vs_path_params
+    {
+        mat4 mvp;
+        int fillType;
+        vec2 gradientStart;
+        vec2 gradientEnd;
+    };
+    
+    uniform vs_path_params _22;
+    
+    layout(location = 0) in vec2 position;
+    out vec2 gradient_uv;
+    
+    void main()
+    {
+        gl_Position = _22.mvp * vec4(position, 0.0, 1.0);
+        if (_22.fillType == 1)
+        {
+            vec2 _54 = _22.gradientEnd - _22.gradientStart;
+            float _59 = _54.x;
+            float _64 = _54.y;
+            gradient_uv.x = dot(position - _22.gradientStart, _54) / ((_59 * _59) + (_64 * _64));
+        }
+        else
+        {
+            if (_22.fillType == 2)
+            {
+                gradient_uv = (position - _22.gradientStart) / vec2(distance(_22.gradientStart, _22.gradientEnd));
+            }
+        }
+    }
+    
+*/
+static const char vs_path_source_glsl330[709] = {
+    0x23,0x76,0x65,0x72,0x73,0x69,0x6f,0x6e,0x20,0x33,0x33,0x30,0x0a,0x0a,0x73,0x74,
+    0x72,0x75,0x63,0x74,0x20,0x76,0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,0x70,0x61,0x72,
+    0x61,0x6d,0x73,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x6d,0x61,0x74,0x34,0x20,0x6d,
+    0x76,0x70,0x3b,0x0a,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x66,0x69,0x6c,0x6c,
+    0x54,0x79,0x70,0x65,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76,0x65,0x63,0x32,0x20,0x67,
+    0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x3b,0x0a,0x20,0x20,
+    0x20,0x20,0x76,0x65,0x63,0x32,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x45,
+    0x6e,0x64,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x75,0x6e,0x69,0x66,0x6f,0x72,0x6d,0x20,
+    0x76,0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x20,0x5f,
+    0x32,0x32,0x3b,0x0a,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,
+    0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x30,0x29,0x20,0x69,0x6e,0x20,0x76,0x65,0x63,
+    0x32,0x20,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3b,0x0a,0x6f,0x75,0x74,0x20,
+    0x76,0x65,0x63,0x32,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,
+    0x3b,0x0a,0x0a,0x76,0x6f,0x69,0x64,0x20,0x6d,0x61,0x69,0x6e,0x28,0x29,0x0a,0x7b,
+    0x0a,0x20,0x20,0x20,0x20,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,
+    0x20,0x3d,0x20,0x5f,0x32,0x32,0x2e,0x6d,0x76,0x70,0x20,0x2a,0x20,0x76,0x65,0x63,
+    0x34,0x28,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x2c,0x20,0x30,0x2e,0x30,0x2c,
+    0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,0x5f,
+    0x32,0x32,0x2e,0x66,0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,0x20,0x3d,0x3d,0x20,0x31,
+    0x29,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x76,0x65,0x63,0x32,0x20,0x5f,0x35,0x34,0x20,0x3d,0x20,0x5f,0x32,0x32,0x2e,0x67,
+    0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x45,0x6e,0x64,0x20,0x2d,0x20,0x5f,0x32,0x32,
+    0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x3b,0x0a,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x35,
+    0x39,0x20,0x3d,0x20,0x5f,0x35,0x34,0x2e,0x78,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x36,0x34,0x20,0x3d,0x20,0x5f,
+    0x35,0x34,0x2e,0x79,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x67,0x72,
+    0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x2e,0x78,0x20,0x3d,0x20,0x64,0x6f,
+    0x74,0x28,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x2d,0x20,0x5f,0x32,0x32,
+    0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x2c,0x20,
+    0x5f,0x35,0x34,0x29,0x20,0x2f,0x20,0x28,0x28,0x5f,0x35,0x39,0x20,0x2a,0x20,0x5f,
+    0x35,0x39,0x29,0x20,0x2b,0x20,0x28,0x5f,0x36,0x34,0x20,0x2a,0x20,0x5f,0x36,0x34,
+    0x29,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x65,0x6c,
+    0x73,0x65,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x69,0x66,0x20,0x28,0x5f,0x32,0x32,0x2e,0x66,0x69,0x6c,0x6c,0x54,0x79,0x70,
+    0x65,0x20,0x3d,0x3d,0x20,0x32,0x29,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x67,0x72,
+    0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x20,0x3d,0x20,0x28,0x70,0x6f,0x73,
+    0x69,0x74,0x69,0x6f,0x6e,0x20,0x2d,0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,
+    0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x29,0x20,0x2f,0x20,0x76,0x65,0x63,
+    0x32,0x28,0x64,0x69,0x73,0x74,0x61,0x6e,0x63,0x65,0x28,0x5f,0x32,0x32,0x2e,0x67,
+    0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x2c,0x20,0x5f,0x32,
+    0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x45,0x6e,0x64,0x29,0x29,0x3b,
+    0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x7d,
+    0x0a,0x7d,0x0a,0x0a,0x00,
+};
+/*
+    #version 330
+    
+    struct fs_path_uniforms
+    {
+        int fillType;
+        vec4 colors[16];
+        vec4 stops[4];
+        int stopCount;
+    };
+    
+    uniform fs_path_uniforms _18;
+    
+    layout(location = 0) out vec4 frag_color;
+    in vec2 gradient_uv;
+    
+    void main()
+    {
+        if (_18.fillType == 0)
+        {
+            frag_color = _18.colors[0];
+        }
+        else
+        {
+            float _39;
+            if (_18.fillType == 1)
+            {
+                _39 = gradient_uv.x;
+            }
+            else
+            {
+                _39 = length(gradient_uv);
+            }
+            vec4 color = mix(_18.colors[0], _18.colors[1], vec4(smoothstep(_18.stops[0].x, _18.stops[0].y, _39)));
+            for (int i = 1; i < 15; i++)
+            {
+                if (i >= (_18.stopCount - 1))
+                {
+                    break;
+                }
+                int _91 = i + 1;
+                color = mix(color, _18.colors[_91], vec4(smoothstep(_18.stops[i / 4][i % 4], _18.stops[_91 / 4][_91 % 4], _39)));
+            }
+            frag_color = color;
+        }
+    }
+    
+*/
+static const char fs_path_source_glsl330[949] = {
+    0x23,0x76,0x65,0x72,0x73,0x69,0x6f,0x6e,0x20,0x33,0x33,0x30,0x0a,0x0a,0x73,0x74,
+    0x72,0x75,0x63,0x74,0x20,0x66,0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,0x75,0x6e,0x69,
+    0x66,0x6f,0x72,0x6d,0x73,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,
+    0x66,0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76,0x65,
+    0x63,0x34,0x20,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x31,0x36,0x5d,0x3b,0x0a,0x20,
+    0x20,0x20,0x20,0x76,0x65,0x63,0x34,0x20,0x73,0x74,0x6f,0x70,0x73,0x5b,0x34,0x5d,
+    0x3b,0x0a,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x73,0x74,0x6f,0x70,0x43,0x6f,
+    0x75,0x6e,0x74,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x75,0x6e,0x69,0x66,0x6f,0x72,0x6d,
+    0x20,0x66,0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,0x75,0x6e,0x69,0x66,0x6f,0x72,0x6d,
+    0x73,0x20,0x5f,0x31,0x38,0x3b,0x0a,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,
+    0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x30,0x29,0x20,0x6f,0x75,0x74,
+    0x20,0x76,0x65,0x63,0x34,0x20,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,
+    0x3b,0x0a,0x69,0x6e,0x20,0x76,0x65,0x63,0x32,0x20,0x67,0x72,0x61,0x64,0x69,0x65,
+    0x6e,0x74,0x5f,0x75,0x76,0x3b,0x0a,0x0a,0x76,0x6f,0x69,0x64,0x20,0x6d,0x61,0x69,
+    0x6e,0x28,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,0x5f,0x31,
+    0x38,0x2e,0x66,0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,0x20,0x3d,0x3d,0x20,0x30,0x29,
+    0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,
+    0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,0x20,0x5f,0x31,0x38,0x2e,
+    0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x30,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,
+    0x0a,0x20,0x20,0x20,0x20,0x65,0x6c,0x73,0x65,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x33,
+    0x39,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,0x5f,
+    0x31,0x38,0x2e,0x66,0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,0x20,0x3d,0x3d,0x20,0x31,
+    0x29,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x5f,0x33,0x39,0x20,0x3d,0x20,0x67,0x72,
+    0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x2e,0x78,0x3b,0x0a,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x65,
+    0x6c,0x73,0x65,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x5f,0x33,0x39,0x20,0x3d,0x20,
+    0x6c,0x65,0x6e,0x67,0x74,0x68,0x28,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,
+    0x75,0x76,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x76,0x65,0x63,0x34,0x20,0x63,0x6f,0x6c,0x6f,
+    0x72,0x20,0x3d,0x20,0x6d,0x69,0x78,0x28,0x5f,0x31,0x38,0x2e,0x63,0x6f,0x6c,0x6f,
+    0x72,0x73,0x5b,0x30,0x5d,0x2c,0x20,0x5f,0x31,0x38,0x2e,0x63,0x6f,0x6c,0x6f,0x72,
+    0x73,0x5b,0x31,0x5d,0x2c,0x20,0x76,0x65,0x63,0x34,0x28,0x73,0x6d,0x6f,0x6f,0x74,
+    0x68,0x73,0x74,0x65,0x70,0x28,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x73,0x5b,
+    0x30,0x5d,0x2e,0x78,0x2c,0x20,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x73,0x5b,
+    0x30,0x5d,0x2e,0x79,0x2c,0x20,0x5f,0x33,0x39,0x29,0x29,0x29,0x3b,0x0a,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x6f,0x72,0x20,0x28,0x69,0x6e,0x74,0x20,0x69,
+    0x20,0x3d,0x20,0x31,0x3b,0x20,0x69,0x20,0x3c,0x20,0x31,0x35,0x3b,0x20,0x69,0x2b,
+    0x2b,0x29,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,0x69,0x20,0x3e,
+    0x3d,0x20,0x28,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x43,0x6f,0x75,0x6e,0x74,
+    0x20,0x2d,0x20,0x31,0x29,0x29,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x62,0x72,0x65,0x61,0x6b,0x3b,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x5f,0x39,0x31,0x20,0x3d,0x20,
+    0x69,0x20,0x2b,0x20,0x31,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,0x20,0x6d,0x69,0x78,0x28,0x63,
+    0x6f,0x6c,0x6f,0x72,0x2c,0x20,0x5f,0x31,0x38,0x2e,0x63,0x6f,0x6c,0x6f,0x72,0x73,
+    0x5b,0x5f,0x39,0x31,0x5d,0x2c,0x20,0x76,0x65,0x63,0x34,0x28,0x73,0x6d,0x6f,0x6f,
+    0x74,0x68,0x73,0x74,0x65,0x70,0x28,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x73,
+    0x5b,0x69,0x20,0x2f,0x20,0x34,0x5d,0x5b,0x69,0x20,0x25,0x20,0x34,0x5d,0x2c,0x20,
+    0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x73,0x5b,0x5f,0x39,0x31,0x20,0x2f,0x20,
+    0x34,0x5d,0x5b,0x5f,0x39,0x31,0x20,0x25,0x20,0x34,0x5d,0x2c,0x20,0x5f,0x33,0x39,
+    0x29,0x29,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,
+    0x72,0x20,0x3d,0x20,0x63,0x6f,0x6c,0x6f,0x72,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,
+    0x0a,0x7d,0x0a,0x0a,0x00,
+};
+/*
+    #version 100
+    
+    uniform vec4 vs_params[4];
+    attribute vec2 position;
+    varying vec2 uv;
+    attribute vec2 texcoord0;
+    
+    void main()
+    {
+        gl_Position = mat4(vs_params[0], vs_params[1], vs_params[2], vs_params[3]) * vec4(position.x, position.y, 0.0, 1.0);
+        uv = texcoord0;
+    }
+    
+*/
+static const char vs_source_glsl100[269] = {
+    0x23,0x76,0x65,0x72,0x73,0x69,0x6f,0x6e,0x20,0x31,0x30,0x30,0x0a,0x0a,0x75,0x6e,
+    0x69,0x66,0x6f,0x72,0x6d,0x20,0x76,0x65,0x63,0x34,0x20,0x76,0x73,0x5f,0x70,0x61,
+    0x72,0x61,0x6d,0x73,0x5b,0x34,0x5d,0x3b,0x0a,0x61,0x74,0x74,0x72,0x69,0x62,0x75,
+    0x74,0x65,0x20,0x76,0x65,0x63,0x32,0x20,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,
+    0x3b,0x0a,0x76,0x61,0x72,0x79,0x69,0x6e,0x67,0x20,0x76,0x65,0x63,0x32,0x20,0x75,
+    0x76,0x3b,0x0a,0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x20,0x76,0x65,0x63,
+    0x32,0x20,0x74,0x65,0x78,0x63,0x6f,0x6f,0x72,0x64,0x30,0x3b,0x0a,0x0a,0x76,0x6f,
+    0x69,0x64,0x20,0x6d,0x61,0x69,0x6e,0x28,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,
+    0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x6d,0x61,
+    0x74,0x34,0x28,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x30,0x5d,0x2c,
+    0x20,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x31,0x5d,0x2c,0x20,0x76,
+    0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x32,0x5d,0x2c,0x20,0x76,0x73,0x5f,
+    0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x33,0x5d,0x29,0x20,0x2a,0x20,0x76,0x65,0x63,
+    0x34,0x28,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x2e,0x78,0x2c,0x20,0x70,0x6f,
+    0x73,0x69,0x74,0x69,0x6f,0x6e,0x2e,0x79,0x2c,0x20,0x30,0x2e,0x30,0x2c,0x20,0x31,
+    0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x75,0x76,0x20,0x3d,0x20,0x74,0x65,
+    0x78,0x63,0x6f,0x6f,0x72,0x64,0x30,0x3b,0x0a,0x7d,0x0a,0x0a,0x00,
+};
+/*
+    #version 100
+    precision mediump float;
+    precision highp int;
+    
+    uniform highp sampler2D tex;
+    
+    varying highp vec2 uv;
+    
+    void main()
+    {
+        gl_FragData[0] = texture2D(tex, uv);
+    }
+    
+*/
+static const char fs_source_glsl100[173] = {
+    0x23,0x76,0x65,0x72,0x73,0x69,0x6f,0x6e,0x20,0x31,0x30,0x30,0x0a,0x70,0x72,0x65,
+    0x63,0x69,0x73,0x69,0x6f,0x6e,0x20,0x6d,0x65,0x64,0x69,0x75,0x6d,0x70,0x20,0x66,
+    0x6c,0x6f,0x61,0x74,0x3b,0x0a,0x70,0x72,0x65,0x63,0x69,0x73,0x69,0x6f,0x6e,0x20,
+    0x68,0x69,0x67,0x68,0x70,0x20,0x69,0x6e,0x74,0x3b,0x0a,0x0a,0x75,0x6e,0x69,0x66,
+    0x6f,0x72,0x6d,0x20,0x68,0x69,0x67,0x68,0x70,0x20,0x73,0x61,0x6d,0x70,0x6c,0x65,
+    0x72,0x32,0x44,0x20,0x74,0x65,0x78,0x3b,0x0a,0x0a,0x76,0x61,0x72,0x79,0x69,0x6e,
+    0x67,0x20,0x68,0x69,0x67,0x68,0x70,0x20,0x76,0x65,0x63,0x32,0x20,0x75,0x76,0x3b,
+    0x0a,0x0a,0x76,0x6f,0x69,0x64,0x20,0x6d,0x61,0x69,0x6e,0x28,0x29,0x0a,0x7b,0x0a,
+    0x20,0x20,0x20,0x20,0x67,0x6c,0x5f,0x46,0x72,0x61,0x67,0x44,0x61,0x74,0x61,0x5b,
+    0x30,0x5d,0x20,0x3d,0x20,0x74,0x65,0x78,0x74,0x75,0x72,0x65,0x32,0x44,0x28,0x74,
+    0x65,0x78,0x2c,0x20,0x75,0x76,0x29,0x3b,0x0a,0x7d,0x0a,0x0a,0x00,
+};
+/*
+    #version 100
+    
+    struct vs_path_params
+    {
+        mat4 mvp;
+        int fillType;
+        vec2 gradientStart;
+        vec2 gradientEnd;
+    };
+    
+    uniform vs_path_params _22;
+    
+    attribute vec2 position;
+    varying vec2 gradient_uv;
+    
+    void main()
+    {
+        gl_Position = _22.mvp * vec4(position, 0.0, 1.0);
+        if (_22.fillType == 1)
+        {
+            vec2 _54 = _22.gradientEnd - _22.gradientStart;
+            float _59 = _54.x;
+            float _64 = _54.y;
+            gradient_uv.x = dot(position - _22.gradientStart, _54) / ((_59 * _59) + (_64 * _64));
+        }
+        else
+        {
+            if (_22.fillType == 2)
+            {
+                gradient_uv = (position - _22.gradientStart) / vec2(distance(_22.gradientStart, _22.gradientEnd));
+            }
+        }
+    }
+    
+*/
+static const char vs_path_source_glsl100[699] = {
+    0x23,0x76,0x65,0x72,0x73,0x69,0x6f,0x6e,0x20,0x31,0x30,0x30,0x0a,0x0a,0x73,0x74,
+    0x72,0x75,0x63,0x74,0x20,0x76,0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,0x70,0x61,0x72,
+    0x61,0x6d,0x73,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x6d,0x61,0x74,0x34,0x20,0x6d,
+    0x76,0x70,0x3b,0x0a,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x66,0x69,0x6c,0x6c,
+    0x54,0x79,0x70,0x65,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76,0x65,0x63,0x32,0x20,0x67,
+    0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x3b,0x0a,0x20,0x20,
+    0x20,0x20,0x76,0x65,0x63,0x32,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x45,
+    0x6e,0x64,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x75,0x6e,0x69,0x66,0x6f,0x72,0x6d,0x20,
+    0x76,0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x20,0x5f,
+    0x32,0x32,0x3b,0x0a,0x0a,0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x20,0x76,
+    0x65,0x63,0x32,0x20,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3b,0x0a,0x76,0x61,
+    0x72,0x79,0x69,0x6e,0x67,0x20,0x76,0x65,0x63,0x32,0x20,0x67,0x72,0x61,0x64,0x69,
+    0x65,0x6e,0x74,0x5f,0x75,0x76,0x3b,0x0a,0x0a,0x76,0x6f,0x69,0x64,0x20,0x6d,0x61,
+    0x69,0x6e,0x28,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x67,0x6c,0x5f,0x50,0x6f,
+    0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x5f,0x32,0x32,0x2e,0x6d,0x76,0x70,
+    0x20,0x2a,0x20,0x76,0x65,0x63,0x34,0x28,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,
+    0x2c,0x20,0x30,0x2e,0x30,0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,
+    0x20,0x69,0x66,0x20,0x28,0x5f,0x32,0x32,0x2e,0x66,0x69,0x6c,0x6c,0x54,0x79,0x70,
+    0x65,0x20,0x3d,0x3d,0x20,0x31,0x29,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x76,0x65,0x63,0x32,0x20,0x5f,0x35,0x34,0x20,0x3d,
+    0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x45,0x6e,0x64,
+    0x20,0x2d,0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,
+    0x74,0x61,0x72,0x74,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x6c,
+    0x6f,0x61,0x74,0x20,0x5f,0x35,0x39,0x20,0x3d,0x20,0x5f,0x35,0x34,0x2e,0x78,0x3b,
+    0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,
+    0x36,0x34,0x20,0x3d,0x20,0x5f,0x35,0x34,0x2e,0x79,0x3b,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x2e,
+    0x78,0x20,0x3d,0x20,0x64,0x6f,0x74,0x28,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,
+    0x20,0x2d,0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,
+    0x74,0x61,0x72,0x74,0x2c,0x20,0x5f,0x35,0x34,0x29,0x20,0x2f,0x20,0x28,0x28,0x5f,
+    0x35,0x39,0x20,0x2a,0x20,0x5f,0x35,0x39,0x29,0x20,0x2b,0x20,0x28,0x5f,0x36,0x34,
+    0x20,0x2a,0x20,0x5f,0x36,0x34,0x29,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,
+    0x20,0x20,0x20,0x20,0x65,0x6c,0x73,0x65,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,0x5f,0x32,0x32,0x2e,0x66,
+    0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,0x20,0x3d,0x3d,0x20,0x32,0x29,0x0a,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x20,
+    0x3d,0x20,0x28,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x2d,0x20,0x5f,0x32,
+    0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x29,
+    0x20,0x2f,0x20,0x76,0x65,0x63,0x32,0x28,0x64,0x69,0x73,0x74,0x61,0x6e,0x63,0x65,
+    0x28,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,
+    0x72,0x74,0x2c,0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,
+    0x45,0x6e,0x64,0x29,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7d,
+    0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,0x7d,0x0a,0x0a,0x00,
+};
+/*
+    #version 100
+    precision mediump float;
+    precision highp int;
+    
+    struct fs_path_uniforms
+    {
+        int fillType;
+        highp vec4 colors[16];
+        highp vec4 stops[4];
+        int stopCount;
+    };
+    
+    uniform fs_path_uniforms _18;
+    
+    varying highp vec2 gradient_uv;
+    
+    void main()
+    {
+        if (_18.fillType == 0)
+        {
+            gl_FragData[0] = _18.colors[0];
+        }
+        else
+        {
+            highp float _39;
+            if (_18.fillType == 1)
+            {
+                _39 = gradient_uv.x;
+            }
+            else
+            {
+                _39 = length(gradient_uv);
+            }
+            highp vec4 color = mix(_18.colors[0], _18.colors[1], vec4(smoothstep(_18.stops[0].x, _18.stops[0].y, _39)));
+            for (int i = 1; i < 15; i++)
+            {
+                if (i >= (_18.stopCount - 1))
+                {
+                    break;
+                }
+                int _91 = i + 1;
+                color = mix(color, _18.colors[_91], vec4(smoothstep(_18.stops[i / 4][i % 4], _18.stops[_91 / 4][_91 % 4], _39)));
+            }
+            gl_FragData[0] = color;
+        }
+    }
+    
+*/
+static const char fs_path_source_glsl100[996] = {
+    0x23,0x76,0x65,0x72,0x73,0x69,0x6f,0x6e,0x20,0x31,0x30,0x30,0x0a,0x70,0x72,0x65,
+    0x63,0x69,0x73,0x69,0x6f,0x6e,0x20,0x6d,0x65,0x64,0x69,0x75,0x6d,0x70,0x20,0x66,
+    0x6c,0x6f,0x61,0x74,0x3b,0x0a,0x70,0x72,0x65,0x63,0x69,0x73,0x69,0x6f,0x6e,0x20,
+    0x68,0x69,0x67,0x68,0x70,0x20,0x69,0x6e,0x74,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,
+    0x63,0x74,0x20,0x66,0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,0x75,0x6e,0x69,0x66,0x6f,
+    0x72,0x6d,0x73,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x66,0x69,
+    0x6c,0x6c,0x54,0x79,0x70,0x65,0x3b,0x0a,0x20,0x20,0x20,0x20,0x68,0x69,0x67,0x68,
+    0x70,0x20,0x76,0x65,0x63,0x34,0x20,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x31,0x36,
+    0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x68,0x69,0x67,0x68,0x70,0x20,0x76,0x65,0x63,
+    0x34,0x20,0x73,0x74,0x6f,0x70,0x73,0x5b,0x34,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,
+    0x69,0x6e,0x74,0x20,0x73,0x74,0x6f,0x70,0x43,0x6f,0x75,0x6e,0x74,0x3b,0x0a,0x7d,
+    0x3b,0x0a,0x0a,0x75,0x6e,0x69,0x66,0x6f,0x72,0x6d,0x20,0x66,0x73,0x5f,0x70,0x61,
+    0x74,0x68,0x5f,0x75,0x6e,0x69,0x66,0x6f,0x72,0x6d,0x73,0x20,0x5f,0x31,0x38,0x3b,
+    0x0a,0x0a,0x76,0x61,0x72,0x79,0x69,0x6e,0x67,0x20,0x68,0x69,0x67,0x68,0x70,0x20,
+    0x76,0x65,0x63,0x32,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,
+    0x3b,0x0a,0x0a,0x76,0x6f,0x69,0x64,0x20,0x6d,0x61,0x69,0x6e,0x28,0x29,0x0a,0x7b,
+    0x0a,0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,0x5f,0x31,0x38,0x2e,0x66,0x69,0x6c,
+    0x6c,0x54,0x79,0x70,0x65,0x20,0x3d,0x3d,0x20,0x30,0x29,0x0a,0x20,0x20,0x20,0x20,
+    0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x67,0x6c,0x5f,0x46,0x72,0x61,
+    0x67,0x44,0x61,0x74,0x61,0x5b,0x30,0x5d,0x20,0x3d,0x20,0x5f,0x31,0x38,0x2e,0x63,
+    0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x30,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,
+    0x20,0x20,0x20,0x20,0x65,0x6c,0x73,0x65,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x68,0x69,0x67,0x68,0x70,0x20,0x66,0x6c,0x6f,
+    0x61,0x74,0x20,0x5f,0x33,0x39,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x69,0x66,0x20,0x28,0x5f,0x31,0x38,0x2e,0x66,0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,
+    0x20,0x3d,0x3d,0x20,0x31,0x29,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7b,
+    0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x5f,0x33,0x39,
+    0x20,0x3d,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x2e,0x78,
+    0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x65,0x6c,0x73,0x65,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x5f,
+    0x33,0x39,0x20,0x3d,0x20,0x6c,0x65,0x6e,0x67,0x74,0x68,0x28,0x67,0x72,0x61,0x64,
+    0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x68,0x69,0x67,0x68,
+    0x70,0x20,0x76,0x65,0x63,0x34,0x20,0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,0x20,0x6d,
+    0x69,0x78,0x28,0x5f,0x31,0x38,0x2e,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x30,0x5d,
+    0x2c,0x20,0x5f,0x31,0x38,0x2e,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x31,0x5d,0x2c,
+    0x20,0x76,0x65,0x63,0x34,0x28,0x73,0x6d,0x6f,0x6f,0x74,0x68,0x73,0x74,0x65,0x70,
+    0x28,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x73,0x5b,0x30,0x5d,0x2e,0x78,0x2c,
+    0x20,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x73,0x5b,0x30,0x5d,0x2e,0x79,0x2c,
+    0x20,0x5f,0x33,0x39,0x29,0x29,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x66,0x6f,0x72,0x20,0x28,0x69,0x6e,0x74,0x20,0x69,0x20,0x3d,0x20,0x31,0x3b,
+    0x20,0x69,0x20,0x3c,0x20,0x31,0x35,0x3b,0x20,0x69,0x2b,0x2b,0x29,0x0a,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,0x69,0x20,0x3e,0x3d,0x20,0x28,0x5f,0x31,
+    0x38,0x2e,0x73,0x74,0x6f,0x70,0x43,0x6f,0x75,0x6e,0x74,0x20,0x2d,0x20,0x31,0x29,
+    0x29,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x62,0x72,0x65,0x61,0x6b,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x69,0x6e,0x74,0x20,0x5f,0x39,0x31,0x20,0x3d,0x20,0x69,0x20,0x2b,0x20,0x31,
+    0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x63,0x6f,
+    0x6c,0x6f,0x72,0x20,0x3d,0x20,0x6d,0x69,0x78,0x28,0x63,0x6f,0x6c,0x6f,0x72,0x2c,
+    0x20,0x5f,0x31,0x38,0x2e,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x5f,0x39,0x31,0x5d,
+    0x2c,0x20,0x76,0x65,0x63,0x34,0x28,0x73,0x6d,0x6f,0x6f,0x74,0x68,0x73,0x74,0x65,
+    0x70,0x28,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x73,0x5b,0x69,0x20,0x2f,0x20,
+    0x34,0x5d,0x5b,0x69,0x20,0x25,0x20,0x34,0x5d,0x2c,0x20,0x5f,0x31,0x38,0x2e,0x73,
+    0x74,0x6f,0x70,0x73,0x5b,0x5f,0x39,0x31,0x20,0x2f,0x20,0x34,0x5d,0x5b,0x5f,0x39,
+    0x31,0x20,0x25,0x20,0x34,0x5d,0x2c,0x20,0x5f,0x33,0x39,0x29,0x29,0x29,0x3b,0x0a,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x67,0x6c,0x5f,0x46,0x72,0x61,0x67,0x44,0x61,0x74,0x61,0x5b,0x30,0x5d,
+    0x20,0x3d,0x20,0x63,0x6f,0x6c,0x6f,0x72,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,
+    0x7d,0x0a,0x0a,0x00,
+};
+/*
+    #version 300 es
+    
+    uniform vec4 vs_params[4];
+    layout(location = 0) in vec2 position;
+    out vec2 uv;
+    layout(location = 1) in vec2 texcoord0;
+    
+    void main()
+    {
+        gl_Position = mat4(vs_params[0], vs_params[1], vs_params[2], vs_params[3]) * vec4(position.x, position.y, 0.0, 1.0);
+        uv = texcoord0;
+    }
+    
+*/
+static const char vs_source_glsl300es[296] = {
+    0x23,0x76,0x65,0x72,0x73,0x69,0x6f,0x6e,0x20,0x33,0x30,0x30,0x20,0x65,0x73,0x0a,
+    0x0a,0x75,0x6e,0x69,0x66,0x6f,0x72,0x6d,0x20,0x76,0x65,0x63,0x34,0x20,0x76,0x73,
+    0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x34,0x5d,0x3b,0x0a,0x6c,0x61,0x79,0x6f,
+    0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x30,0x29,
+    0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x32,0x20,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,
+    0x6e,0x3b,0x0a,0x6f,0x75,0x74,0x20,0x76,0x65,0x63,0x32,0x20,0x75,0x76,0x3b,0x0a,
+    0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,
+    0x3d,0x20,0x31,0x29,0x20,0x69,0x6e,0x20,0x76,0x65,0x63,0x32,0x20,0x74,0x65,0x78,
+    0x63,0x6f,0x6f,0x72,0x64,0x30,0x3b,0x0a,0x0a,0x76,0x6f,0x69,0x64,0x20,0x6d,0x61,
+    0x69,0x6e,0x28,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x67,0x6c,0x5f,0x50,0x6f,
+    0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x6d,0x61,0x74,0x34,0x28,0x76,0x73,
+    0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x5b,0x30,0x5d,0x2c,0x20,0x76,0x73,0x5f,0x70,
+    0x61,0x72,0x61,0x6d,0x73,0x5b,0x31,0x5d,0x2c,0x20,0x76,0x73,0x5f,0x70,0x61,0x72,
+    0x61,0x6d,0x73,0x5b,0x32,0x5d,0x2c,0x20,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,
+    0x73,0x5b,0x33,0x5d,0x29,0x20,0x2a,0x20,0x76,0x65,0x63,0x34,0x28,0x70,0x6f,0x73,
+    0x69,0x74,0x69,0x6f,0x6e,0x2e,0x78,0x2c,0x20,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,
+    0x6e,0x2e,0x79,0x2c,0x20,0x30,0x2e,0x30,0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,
+    0x20,0x20,0x20,0x20,0x75,0x76,0x20,0x3d,0x20,0x74,0x65,0x78,0x63,0x6f,0x6f,0x72,
+    0x64,0x30,0x3b,0x0a,0x7d,0x0a,0x0a,0x00,
+};
+/*
+    #version 300 es
+    precision mediump float;
+    precision highp int;
+    
+    uniform highp sampler2D tex;
+    
+    layout(location = 0) out highp vec4 frag_color;
+    in highp vec2 uv;
+    
+    void main()
+    {
+        frag_color = texture(tex, uv);
+    }
+    
+*/
+static const char fs_source_glsl300es[213] = {
+    0x23,0x76,0x65,0x72,0x73,0x69,0x6f,0x6e,0x20,0x33,0x30,0x30,0x20,0x65,0x73,0x0a,
+    0x70,0x72,0x65,0x63,0x69,0x73,0x69,0x6f,0x6e,0x20,0x6d,0x65,0x64,0x69,0x75,0x6d,
+    0x70,0x20,0x66,0x6c,0x6f,0x61,0x74,0x3b,0x0a,0x70,0x72,0x65,0x63,0x69,0x73,0x69,
+    0x6f,0x6e,0x20,0x68,0x69,0x67,0x68,0x70,0x20,0x69,0x6e,0x74,0x3b,0x0a,0x0a,0x75,
+    0x6e,0x69,0x66,0x6f,0x72,0x6d,0x20,0x68,0x69,0x67,0x68,0x70,0x20,0x73,0x61,0x6d,
+    0x70,0x6c,0x65,0x72,0x32,0x44,0x20,0x74,0x65,0x78,0x3b,0x0a,0x0a,0x6c,0x61,0x79,
+    0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x30,
+    0x29,0x20,0x6f,0x75,0x74,0x20,0x68,0x69,0x67,0x68,0x70,0x20,0x76,0x65,0x63,0x34,
+    0x20,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x3b,0x0a,0x69,0x6e,0x20,
+    0x68,0x69,0x67,0x68,0x70,0x20,0x76,0x65,0x63,0x32,0x20,0x75,0x76,0x3b,0x0a,0x0a,
+    0x76,0x6f,0x69,0x64,0x20,0x6d,0x61,0x69,0x6e,0x28,0x29,0x0a,0x7b,0x0a,0x20,0x20,
+    0x20,0x20,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,0x20,0x74,
+    0x65,0x78,0x74,0x75,0x72,0x65,0x28,0x74,0x65,0x78,0x2c,0x20,0x75,0x76,0x29,0x3b,
+    0x0a,0x7d,0x0a,0x0a,0x00,
+};
+/*
+    #version 300 es
+    
+    struct vs_path_params
+    {
+        mat4 mvp;
+        int fillType;
+        vec2 gradientStart;
+        vec2 gradientEnd;
+    };
+    
+    uniform vs_path_params _22;
+    
+    layout(location = 0) in vec2 position;
+    out vec2 gradient_uv;
+    
+    void main()
+    {
+        gl_Position = _22.mvp * vec4(position, 0.0, 1.0);
+        if (_22.fillType == 1)
+        {
+            vec2 _54 = _22.gradientEnd - _22.gradientStart;
+            float _59 = _54.x;
+            float _64 = _54.y;
+            gradient_uv.x = dot(position - _22.gradientStart, _54) / ((_59 * _59) + (_64 * _64));
+        }
+        else
+        {
+            if (_22.fillType == 2)
+            {
+                gradient_uv = (position - _22.gradientStart) / vec2(distance(_22.gradientStart, _22.gradientEnd));
+            }
+        }
+    }
+    
+*/
+static const char vs_path_source_glsl300es[712] = {
+    0x23,0x76,0x65,0x72,0x73,0x69,0x6f,0x6e,0x20,0x33,0x30,0x30,0x20,0x65,0x73,0x0a,
+    0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x76,0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,
+    0x70,0x61,0x72,0x61,0x6d,0x73,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x6d,0x61,0x74,
+    0x34,0x20,0x6d,0x76,0x70,0x3b,0x0a,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x66,
+    0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76,0x65,0x63,
+    0x32,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x3b,
+    0x0a,0x20,0x20,0x20,0x20,0x76,0x65,0x63,0x32,0x20,0x67,0x72,0x61,0x64,0x69,0x65,
+    0x6e,0x74,0x45,0x6e,0x64,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x75,0x6e,0x69,0x66,0x6f,
+    0x72,0x6d,0x20,0x76,0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,0x70,0x61,0x72,0x61,0x6d,
+    0x73,0x20,0x5f,0x32,0x32,0x3b,0x0a,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,
+    0x6f,0x63,0x61,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x30,0x29,0x20,0x69,0x6e,0x20,
+    0x76,0x65,0x63,0x32,0x20,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3b,0x0a,0x6f,
+    0x75,0x74,0x20,0x76,0x65,0x63,0x32,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,
+    0x5f,0x75,0x76,0x3b,0x0a,0x0a,0x76,0x6f,0x69,0x64,0x20,0x6d,0x61,0x69,0x6e,0x28,
+    0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,
+    0x69,0x6f,0x6e,0x20,0x3d,0x20,0x5f,0x32,0x32,0x2e,0x6d,0x76,0x70,0x20,0x2a,0x20,
+    0x76,0x65,0x63,0x34,0x28,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x2c,0x20,0x30,
+    0x2e,0x30,0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x69,0x66,
+    0x20,0x28,0x5f,0x32,0x32,0x2e,0x66,0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,0x20,0x3d,
+    0x3d,0x20,0x31,0x29,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x76,0x65,0x63,0x32,0x20,0x5f,0x35,0x34,0x20,0x3d,0x20,0x5f,0x32,
+    0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x45,0x6e,0x64,0x20,0x2d,0x20,
+    0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,
+    0x74,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,
+    0x20,0x5f,0x35,0x39,0x20,0x3d,0x20,0x5f,0x35,0x34,0x2e,0x78,0x3b,0x0a,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x36,0x34,0x20,
+    0x3d,0x20,0x5f,0x35,0x34,0x2e,0x79,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x2e,0x78,0x20,0x3d,
+    0x20,0x64,0x6f,0x74,0x28,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x2d,0x20,
+    0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,
+    0x74,0x2c,0x20,0x5f,0x35,0x34,0x29,0x20,0x2f,0x20,0x28,0x28,0x5f,0x35,0x39,0x20,
+    0x2a,0x20,0x5f,0x35,0x39,0x29,0x20,0x2b,0x20,0x28,0x5f,0x36,0x34,0x20,0x2a,0x20,
+    0x5f,0x36,0x34,0x29,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,
+    0x20,0x65,0x6c,0x73,0x65,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,0x5f,0x32,0x32,0x2e,0x66,0x69,0x6c,0x6c,
+    0x54,0x79,0x70,0x65,0x20,0x3d,0x3d,0x20,0x32,0x29,0x0a,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x20,0x3d,0x20,0x28,
+    0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x2d,0x20,0x5f,0x32,0x32,0x2e,0x67,
+    0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x29,0x20,0x2f,0x20,
+    0x76,0x65,0x63,0x32,0x28,0x64,0x69,0x73,0x74,0x61,0x6e,0x63,0x65,0x28,0x5f,0x32,
+    0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x2c,
+    0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x45,0x6e,0x64,
+    0x29,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,
+    0x20,0x20,0x7d,0x0a,0x7d,0x0a,0x0a,0x00,
+};
+/*
+    #version 300 es
+    precision mediump float;
+    precision highp int;
+    
+    struct fs_path_uniforms
+    {
+        int fillType;
+        highp vec4 colors[16];
+        highp vec4 stops[4];
+        int stopCount;
+    };
+    
+    uniform fs_path_uniforms _18;
+    
+    layout(location = 0) out highp vec4 frag_color;
+    in highp vec2 gradient_uv;
+    
+    void main()
+    {
+        if (_18.fillType == 0)
+        {
+            frag_color = _18.colors[0];
+        }
+        else
+        {
+            highp float _39;
+            if (_18.fillType == 1)
+            {
+                _39 = gradient_uv.x;
+            }
+            else
+            {
+                _39 = length(gradient_uv);
+            }
+            highp vec4 color = mix(_18.colors[0], _18.colors[1], vec4(smoothstep(_18.stops[0].x, _18.stops[0].y, _39)));
+            for (int i = 1; i < 15; i++)
+            {
+                if (i >= (_18.stopCount - 1))
+                {
+                    break;
+                }
+                int _91 = i + 1;
+                color = mix(color, _18.colors[_91], vec4(smoothstep(_18.stops[i / 4][i % 4], _18.stops[_91 / 4][_91 % 4], _39)));
+            }
+            frag_color = color;
+        }
+    }
+    
+*/
+static const char fs_path_source_glsl300es[1034] = {
+    0x23,0x76,0x65,0x72,0x73,0x69,0x6f,0x6e,0x20,0x33,0x30,0x30,0x20,0x65,0x73,0x0a,
+    0x70,0x72,0x65,0x63,0x69,0x73,0x69,0x6f,0x6e,0x20,0x6d,0x65,0x64,0x69,0x75,0x6d,
+    0x70,0x20,0x66,0x6c,0x6f,0x61,0x74,0x3b,0x0a,0x70,0x72,0x65,0x63,0x69,0x73,0x69,
+    0x6f,0x6e,0x20,0x68,0x69,0x67,0x68,0x70,0x20,0x69,0x6e,0x74,0x3b,0x0a,0x0a,0x73,
+    0x74,0x72,0x75,0x63,0x74,0x20,0x66,0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,0x75,0x6e,
+    0x69,0x66,0x6f,0x72,0x6d,0x73,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,
+    0x20,0x66,0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,0x3b,0x0a,0x20,0x20,0x20,0x20,0x68,
+    0x69,0x67,0x68,0x70,0x20,0x76,0x65,0x63,0x34,0x20,0x63,0x6f,0x6c,0x6f,0x72,0x73,
+    0x5b,0x31,0x36,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x68,0x69,0x67,0x68,0x70,0x20,
+    0x76,0x65,0x63,0x34,0x20,0x73,0x74,0x6f,0x70,0x73,0x5b,0x34,0x5d,0x3b,0x0a,0x20,
+    0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x73,0x74,0x6f,0x70,0x43,0x6f,0x75,0x6e,0x74,
+    0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x75,0x6e,0x69,0x66,0x6f,0x72,0x6d,0x20,0x66,0x73,
+    0x5f,0x70,0x61,0x74,0x68,0x5f,0x75,0x6e,0x69,0x66,0x6f,0x72,0x6d,0x73,0x20,0x5f,
+    0x31,0x38,0x3b,0x0a,0x0a,0x6c,0x61,0x79,0x6f,0x75,0x74,0x28,0x6c,0x6f,0x63,0x61,
+    0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x30,0x29,0x20,0x6f,0x75,0x74,0x20,0x68,0x69,
+    0x67,0x68,0x70,0x20,0x76,0x65,0x63,0x34,0x20,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,
+    0x6c,0x6f,0x72,0x3b,0x0a,0x69,0x6e,0x20,0x68,0x69,0x67,0x68,0x70,0x20,0x76,0x65,
+    0x63,0x32,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x3b,0x0a,
+    0x0a,0x76,0x6f,0x69,0x64,0x20,0x6d,0x61,0x69,0x6e,0x28,0x29,0x0a,0x7b,0x0a,0x20,
+    0x20,0x20,0x20,0x69,0x66,0x20,0x28,0x5f,0x31,0x38,0x2e,0x66,0x69,0x6c,0x6c,0x54,
+    0x79,0x70,0x65,0x20,0x3d,0x3d,0x20,0x30,0x29,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,
+    0x6f,0x72,0x20,0x3d,0x20,0x5f,0x31,0x38,0x2e,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,
+    0x30,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x65,0x6c,
+    0x73,0x65,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x68,0x69,0x67,0x68,0x70,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x33,0x39,
+    0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,0x5f,0x31,
+    0x38,0x2e,0x66,0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,0x20,0x3d,0x3d,0x20,0x31,0x29,
+    0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x5f,0x33,0x39,0x20,0x3d,0x20,0x67,0x72,0x61,
+    0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x2e,0x78,0x3b,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x65,0x6c,
+    0x73,0x65,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x5f,0x33,0x39,0x20,0x3d,0x20,0x6c,
+    0x65,0x6e,0x67,0x74,0x68,0x28,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,
+    0x76,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x68,0x69,0x67,0x68,0x70,0x20,0x76,0x65,0x63,0x34,
+    0x20,0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,0x20,0x6d,0x69,0x78,0x28,0x5f,0x31,0x38,
+    0x2e,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x30,0x5d,0x2c,0x20,0x5f,0x31,0x38,0x2e,
+    0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x31,0x5d,0x2c,0x20,0x76,0x65,0x63,0x34,0x28,
+    0x73,0x6d,0x6f,0x6f,0x74,0x68,0x73,0x74,0x65,0x70,0x28,0x5f,0x31,0x38,0x2e,0x73,
+    0x74,0x6f,0x70,0x73,0x5b,0x30,0x5d,0x2e,0x78,0x2c,0x20,0x5f,0x31,0x38,0x2e,0x73,
+    0x74,0x6f,0x70,0x73,0x5b,0x30,0x5d,0x2e,0x79,0x2c,0x20,0x5f,0x33,0x39,0x29,0x29,
+    0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x6f,0x72,0x20,0x28,
+    0x69,0x6e,0x74,0x20,0x69,0x20,0x3d,0x20,0x31,0x3b,0x20,0x69,0x20,0x3c,0x20,0x31,
+    0x35,0x3b,0x20,0x69,0x2b,0x2b,0x29,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x69,0x66,
+    0x20,0x28,0x69,0x20,0x3e,0x3d,0x20,0x28,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,
+    0x43,0x6f,0x75,0x6e,0x74,0x20,0x2d,0x20,0x31,0x29,0x29,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x62,0x72,0x65,0x61,0x6b,0x3b,
+    0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x5f,
+    0x39,0x31,0x20,0x3d,0x20,0x69,0x20,0x2b,0x20,0x31,0x3b,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,0x20,
+    0x6d,0x69,0x78,0x28,0x63,0x6f,0x6c,0x6f,0x72,0x2c,0x20,0x5f,0x31,0x38,0x2e,0x63,
+    0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x5f,0x39,0x31,0x5d,0x2c,0x20,0x76,0x65,0x63,0x34,
+    0x28,0x73,0x6d,0x6f,0x6f,0x74,0x68,0x73,0x74,0x65,0x70,0x28,0x5f,0x31,0x38,0x2e,
+    0x73,0x74,0x6f,0x70,0x73,0x5b,0x69,0x20,0x2f,0x20,0x34,0x5d,0x5b,0x69,0x20,0x25,
+    0x20,0x34,0x5d,0x2c,0x20,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x73,0x5b,0x5f,
+    0x39,0x31,0x20,0x2f,0x20,0x34,0x5d,0x5b,0x5f,0x39,0x31,0x20,0x25,0x20,0x34,0x5d,
+    0x2c,0x20,0x5f,0x33,0x39,0x29,0x29,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x72,0x61,0x67,
+    0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,0x20,0x63,0x6f,0x6c,0x6f,0x72,0x3b,0x0a,
+    0x20,0x20,0x20,0x20,0x7d,0x0a,0x7d,0x0a,0x0a,0x00,
+};
+/*
+    cbuffer vs_params : register(b0)
+    {
+        row_major float4x4 _21_mvp : packoffset(c0);
+    };
+    
+    
+    static float4 gl_Position;
+    static float2 position;
+    static float2 uv;
+    static float2 texcoord0;
+    
+    struct SPIRV_Cross_Input
+    {
+        float2 position : TEXCOORD0;
+        float2 texcoord0 : TEXCOORD1;
+    };
+    
+    struct SPIRV_Cross_Output
+    {
+        float2 uv : TEXCOORD0;
+        float4 gl_Position : SV_Position;
+    };
+    
+    #line 16 "../src/sokol/shader.glsl"
+    void vert_main()
+    {
+    #line 16 "../src/sokol/shader.glsl"
+        gl_Position = mul(float4(position.x, position.y, 0.0f, 1.0f), _21_mvp);
+    #line 17 "../src/sokol/shader.glsl"
+        uv = texcoord0;
+    }
+    
+    SPIRV_Cross_Output main(SPIRV_Cross_Input stage_input)
+    {
+        position = stage_input.position;
+        texcoord0 = stage_input.texcoord0;
+        vert_main();
+        SPIRV_Cross_Output stage_output;
+        stage_output.gl_Position = gl_Position;
+        stage_output.uv = uv;
+        return stage_output;
+    }
+*/
+static const char vs_source_hlsl5[890] = {
+    0x63,0x62,0x75,0x66,0x66,0x65,0x72,0x20,0x76,0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,
+    0x73,0x20,0x3a,0x20,0x72,0x65,0x67,0x69,0x73,0x74,0x65,0x72,0x28,0x62,0x30,0x29,
+    0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x72,0x6f,0x77,0x5f,0x6d,0x61,0x6a,0x6f,0x72,
+    0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x78,0x34,0x20,0x5f,0x32,0x31,0x5f,0x6d,0x76,
+    0x70,0x20,0x3a,0x20,0x70,0x61,0x63,0x6b,0x6f,0x66,0x66,0x73,0x65,0x74,0x28,0x63,
+    0x30,0x29,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x0a,0x73,0x74,0x61,0x74,0x69,0x63,0x20,
+    0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,
+    0x6f,0x6e,0x3b,0x0a,0x73,0x74,0x61,0x74,0x69,0x63,0x20,0x66,0x6c,0x6f,0x61,0x74,
+    0x32,0x20,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3b,0x0a,0x73,0x74,0x61,0x74,
+    0x69,0x63,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x75,0x76,0x3b,0x0a,0x73,0x74,
+    0x61,0x74,0x69,0x63,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x74,0x65,0x78,0x63,
+    0x6f,0x6f,0x72,0x64,0x30,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x53,
+    0x50,0x49,0x52,0x56,0x5f,0x43,0x72,0x6f,0x73,0x73,0x5f,0x49,0x6e,0x70,0x75,0x74,
+    0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x70,0x6f,
+    0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3a,0x20,0x54,0x45,0x58,0x43,0x4f,0x4f,0x52,
+    0x44,0x30,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x74,
+    0x65,0x78,0x63,0x6f,0x6f,0x72,0x64,0x30,0x20,0x3a,0x20,0x54,0x45,0x58,0x43,0x4f,
+    0x4f,0x52,0x44,0x31,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,
+    0x20,0x53,0x50,0x49,0x52,0x56,0x5f,0x43,0x72,0x6f,0x73,0x73,0x5f,0x4f,0x75,0x74,
+    0x70,0x75,0x74,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,
+    0x20,0x75,0x76,0x20,0x3a,0x20,0x54,0x45,0x58,0x43,0x4f,0x4f,0x52,0x44,0x30,0x3b,
+    0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x67,0x6c,0x5f,0x50,
+    0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3a,0x20,0x53,0x56,0x5f,0x50,0x6f,0x73,
+    0x69,0x74,0x69,0x6f,0x6e,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x23,0x6c,0x69,0x6e,0x65,
+    0x20,0x31,0x36,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,
+    0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x76,
+    0x6f,0x69,0x64,0x20,0x76,0x65,0x72,0x74,0x5f,0x6d,0x61,0x69,0x6e,0x28,0x29,0x0a,
+    0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x36,0x20,0x22,0x2e,0x2e,0x2f,0x73,
+    0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,
+    0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x67,0x6c,0x5f,0x50,0x6f,0x73,
+    0x69,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x6d,0x75,0x6c,0x28,0x66,0x6c,0x6f,0x61,
+    0x74,0x34,0x28,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x2e,0x78,0x2c,0x20,0x70,
+    0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x2e,0x79,0x2c,0x20,0x30,0x2e,0x30,0x66,0x2c,
+    0x20,0x31,0x2e,0x30,0x66,0x29,0x2c,0x20,0x5f,0x32,0x31,0x5f,0x6d,0x76,0x70,0x29,
+    0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x37,0x20,0x22,0x2e,0x2e,0x2f,0x73,
+    0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,
+    0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x75,0x76,0x20,0x3d,0x20,0x74,
+    0x65,0x78,0x63,0x6f,0x6f,0x72,0x64,0x30,0x3b,0x0a,0x7d,0x0a,0x0a,0x53,0x50,0x49,
+    0x52,0x56,0x5f,0x43,0x72,0x6f,0x73,0x73,0x5f,0x4f,0x75,0x74,0x70,0x75,0x74,0x20,
+    0x6d,0x61,0x69,0x6e,0x28,0x53,0x50,0x49,0x52,0x56,0x5f,0x43,0x72,0x6f,0x73,0x73,
+    0x5f,0x49,0x6e,0x70,0x75,0x74,0x20,0x73,0x74,0x61,0x67,0x65,0x5f,0x69,0x6e,0x70,
+    0x75,0x74,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x70,0x6f,0x73,0x69,0x74,0x69,
+    0x6f,0x6e,0x20,0x3d,0x20,0x73,0x74,0x61,0x67,0x65,0x5f,0x69,0x6e,0x70,0x75,0x74,
+    0x2e,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3b,0x0a,0x20,0x20,0x20,0x20,0x74,
+    0x65,0x78,0x63,0x6f,0x6f,0x72,0x64,0x30,0x20,0x3d,0x20,0x73,0x74,0x61,0x67,0x65,
+    0x5f,0x69,0x6e,0x70,0x75,0x74,0x2e,0x74,0x65,0x78,0x63,0x6f,0x6f,0x72,0x64,0x30,
+    0x3b,0x0a,0x20,0x20,0x20,0x20,0x76,0x65,0x72,0x74,0x5f,0x6d,0x61,0x69,0x6e,0x28,
+    0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x53,0x50,0x49,0x52,0x56,0x5f,0x43,0x72,0x6f,
+    0x73,0x73,0x5f,0x4f,0x75,0x74,0x70,0x75,0x74,0x20,0x73,0x74,0x61,0x67,0x65,0x5f,
+    0x6f,0x75,0x74,0x70,0x75,0x74,0x3b,0x0a,0x20,0x20,0x20,0x20,0x73,0x74,0x61,0x67,
+    0x65,0x5f,0x6f,0x75,0x74,0x70,0x75,0x74,0x2e,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,
+    0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,
+    0x6f,0x6e,0x3b,0x0a,0x20,0x20,0x20,0x20,0x73,0x74,0x61,0x67,0x65,0x5f,0x6f,0x75,
+    0x74,0x70,0x75,0x74,0x2e,0x75,0x76,0x20,0x3d,0x20,0x75,0x76,0x3b,0x0a,0x20,0x20,
+    0x20,0x20,0x72,0x65,0x74,0x75,0x72,0x6e,0x20,0x73,0x74,0x61,0x67,0x65,0x5f,0x6f,
+    0x75,0x74,0x70,0x75,0x74,0x3b,0x0a,0x7d,0x0a,0x00,
+};
+/*
+    Texture2D<float4> tex : register(t0);
+    SamplerState _tex_sampler : register(s0);
+    
+    static float4 frag_color;
+    static float2 uv;
+    
+    struct SPIRV_Cross_Input
+    {
+        float2 uv : TEXCOORD0;
+    };
+    
+    struct SPIRV_Cross_Output
+    {
+        float4 frag_color : SV_Target0;
+    };
+    
+    #line 12 "../src/sokol/shader.glsl"
+    void frag_main()
+    {
+    #line 12 "../src/sokol/shader.glsl"
+        frag_color = tex.Sample(_tex_sampler, uv);
+    }
+    
+    SPIRV_Cross_Output main(SPIRV_Cross_Input stage_input)
+    {
+        uv = stage_input.uv;
+        frag_main();
+        SPIRV_Cross_Output stage_output;
+        stage_output.frag_color = frag_color;
+        return stage_output;
+    }
+*/
+static const char fs_source_hlsl5[599] = {
+    0x54,0x65,0x78,0x74,0x75,0x72,0x65,0x32,0x44,0x3c,0x66,0x6c,0x6f,0x61,0x74,0x34,
+    0x3e,0x20,0x74,0x65,0x78,0x20,0x3a,0x20,0x72,0x65,0x67,0x69,0x73,0x74,0x65,0x72,
+    0x28,0x74,0x30,0x29,0x3b,0x0a,0x53,0x61,0x6d,0x70,0x6c,0x65,0x72,0x53,0x74,0x61,
+    0x74,0x65,0x20,0x5f,0x74,0x65,0x78,0x5f,0x73,0x61,0x6d,0x70,0x6c,0x65,0x72,0x20,
+    0x3a,0x20,0x72,0x65,0x67,0x69,0x73,0x74,0x65,0x72,0x28,0x73,0x30,0x29,0x3b,0x0a,
+    0x0a,0x73,0x74,0x61,0x74,0x69,0x63,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x66,
+    0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x3b,0x0a,0x73,0x74,0x61,0x74,0x69,
+    0x63,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x75,0x76,0x3b,0x0a,0x0a,0x73,0x74,
+    0x72,0x75,0x63,0x74,0x20,0x53,0x50,0x49,0x52,0x56,0x5f,0x43,0x72,0x6f,0x73,0x73,
+    0x5f,0x49,0x6e,0x70,0x75,0x74,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,
+    0x61,0x74,0x32,0x20,0x75,0x76,0x20,0x3a,0x20,0x54,0x45,0x58,0x43,0x4f,0x4f,0x52,
+    0x44,0x30,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x53,
+    0x50,0x49,0x52,0x56,0x5f,0x43,0x72,0x6f,0x73,0x73,0x5f,0x4f,0x75,0x74,0x70,0x75,
+    0x74,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x66,
+    0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3a,0x20,0x53,0x56,0x5f,0x54,
+    0x61,0x72,0x67,0x65,0x74,0x30,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x23,0x6c,0x69,0x6e,
+    0x65,0x20,0x31,0x32,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,
+    0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,
+    0x76,0x6f,0x69,0x64,0x20,0x66,0x72,0x61,0x67,0x5f,0x6d,0x61,0x69,0x6e,0x28,0x29,
+    0x0a,0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x32,0x20,0x22,0x2e,0x2e,0x2f,
+    0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,
+    0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x66,0x72,0x61,0x67,0x5f,
+    0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,0x20,0x74,0x65,0x78,0x2e,0x53,0x61,0x6d,0x70,
+    0x6c,0x65,0x28,0x5f,0x74,0x65,0x78,0x5f,0x73,0x61,0x6d,0x70,0x6c,0x65,0x72,0x2c,
+    0x20,0x75,0x76,0x29,0x3b,0x0a,0x7d,0x0a,0x0a,0x53,0x50,0x49,0x52,0x56,0x5f,0x43,
+    0x72,0x6f,0x73,0x73,0x5f,0x4f,0x75,0x74,0x70,0x75,0x74,0x20,0x6d,0x61,0x69,0x6e,
+    0x28,0x53,0x50,0x49,0x52,0x56,0x5f,0x43,0x72,0x6f,0x73,0x73,0x5f,0x49,0x6e,0x70,
+    0x75,0x74,0x20,0x73,0x74,0x61,0x67,0x65,0x5f,0x69,0x6e,0x70,0x75,0x74,0x29,0x0a,
+    0x7b,0x0a,0x20,0x20,0x20,0x20,0x75,0x76,0x20,0x3d,0x20,0x73,0x74,0x61,0x67,0x65,
+    0x5f,0x69,0x6e,0x70,0x75,0x74,0x2e,0x75,0x76,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,
+    0x72,0x61,0x67,0x5f,0x6d,0x61,0x69,0x6e,0x28,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,
+    0x53,0x50,0x49,0x52,0x56,0x5f,0x43,0x72,0x6f,0x73,0x73,0x5f,0x4f,0x75,0x74,0x70,
+    0x75,0x74,0x20,0x73,0x74,0x61,0x67,0x65,0x5f,0x6f,0x75,0x74,0x70,0x75,0x74,0x3b,
+    0x0a,0x20,0x20,0x20,0x20,0x73,0x74,0x61,0x67,0x65,0x5f,0x6f,0x75,0x74,0x70,0x75,
+    0x74,0x2e,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,0x20,0x66,
+    0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x3b,0x0a,0x20,0x20,0x20,0x20,0x72,
+    0x65,0x74,0x75,0x72,0x6e,0x20,0x73,0x74,0x61,0x67,0x65,0x5f,0x6f,0x75,0x74,0x70,
+    0x75,0x74,0x3b,0x0a,0x7d,0x0a,0x00,
+};
+/*
+    cbuffer vs_path_params : register(b0)
+    {
+        row_major float4x4 _22_mvp : packoffset(c0);
+        int _22_fillType : packoffset(c4);
+        float2 _22_gradientStart : packoffset(c4.z);
+        float2 _22_gradientEnd : packoffset(c5);
+    };
+    
+    
+    static float4 gl_Position;
+    static float2 position;
+    static float2 gradient_uv;
+    
+    struct SPIRV_Cross_Input
+    {
+        float2 position : TEXCOORD0;
+    };
+    
+    struct SPIRV_Cross_Output
+    {
+        float2 gradient_uv : TEXCOORD0;
+        float4 gl_Position : SV_Position;
+    };
+    
+    #line 17 "../src/sokol/shader.glsl"
+    void vert_main()
+    {
+    #line 17 "../src/sokol/shader.glsl"
+        gl_Position = mul(float4(position, 0.0f, 1.0f), _22_mvp);
+    #line 19 "../src/sokol/shader.glsl"
+        if (_22_fillType == 1)
+        {
+    #line 21 "../src/sokol/shader.glsl"
+            float2 _54 = _22_gradientEnd - _22_gradientStart;
+    #line 22 "../src/sokol/shader.glsl"
+            float _59 = _54.x;
+            float _64 = _54.y;
+    #line 23 "../src/sokol/shader.glsl"
+            gradient_uv.x = dot(position - _22_gradientStart, _54) / ((_59 * _59) + (_64 * _64));
+        }
+        else
+        {
+    #line 25 "../src/sokol/shader.glsl"
+            if (_22_fillType == 2)
+            {
+    #line 27 "../src/sokol/shader.glsl"
+                gradient_uv = (position - _22_gradientStart) / distance(_22_gradientStart, _22_gradientEnd).xx;
+            }
+        }
+    }
+    
+    SPIRV_Cross_Output main(SPIRV_Cross_Input stage_input)
+    {
+        position = stage_input.position;
+        vert_main();
+        SPIRV_Cross_Output stage_output;
+        stage_output.gl_Position = gl_Position;
+        stage_output.gradient_uv = gradient_uv;
+        return stage_output;
+    }
+*/
+static const char vs_path_source_hlsl5[1537] = {
+    0x63,0x62,0x75,0x66,0x66,0x65,0x72,0x20,0x76,0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,
+    0x70,0x61,0x72,0x61,0x6d,0x73,0x20,0x3a,0x20,0x72,0x65,0x67,0x69,0x73,0x74,0x65,
+    0x72,0x28,0x62,0x30,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x72,0x6f,0x77,0x5f,
+    0x6d,0x61,0x6a,0x6f,0x72,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x78,0x34,0x20,0x5f,
+    0x32,0x32,0x5f,0x6d,0x76,0x70,0x20,0x3a,0x20,0x70,0x61,0x63,0x6b,0x6f,0x66,0x66,
+    0x73,0x65,0x74,0x28,0x63,0x30,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,
+    0x20,0x5f,0x32,0x32,0x5f,0x66,0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,0x20,0x3a,0x20,
+    0x70,0x61,0x63,0x6b,0x6f,0x66,0x66,0x73,0x65,0x74,0x28,0x63,0x34,0x29,0x3b,0x0a,
+    0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x5f,0x32,0x32,0x5f,0x67,
+    0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x20,0x3a,0x20,0x70,
+    0x61,0x63,0x6b,0x6f,0x66,0x66,0x73,0x65,0x74,0x28,0x63,0x34,0x2e,0x7a,0x29,0x3b,
+    0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x5f,0x32,0x32,0x5f,
+    0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x45,0x6e,0x64,0x20,0x3a,0x20,0x70,0x61,
+    0x63,0x6b,0x6f,0x66,0x66,0x73,0x65,0x74,0x28,0x63,0x35,0x29,0x3b,0x0a,0x7d,0x3b,
+    0x0a,0x0a,0x0a,0x73,0x74,0x61,0x74,0x69,0x63,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,
+    0x20,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3b,0x0a,0x73,0x74,
+    0x61,0x74,0x69,0x63,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x70,0x6f,0x73,0x69,
+    0x74,0x69,0x6f,0x6e,0x3b,0x0a,0x73,0x74,0x61,0x74,0x69,0x63,0x20,0x66,0x6c,0x6f,
+    0x61,0x74,0x32,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x3b,
+    0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x53,0x50,0x49,0x52,0x56,0x5f,0x43,
+    0x72,0x6f,0x73,0x73,0x5f,0x49,0x6e,0x70,0x75,0x74,0x0a,0x7b,0x0a,0x20,0x20,0x20,
+    0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,
+    0x20,0x3a,0x20,0x54,0x45,0x58,0x43,0x4f,0x4f,0x52,0x44,0x30,0x3b,0x0a,0x7d,0x3b,
+    0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x53,0x50,0x49,0x52,0x56,0x5f,0x43,
+    0x72,0x6f,0x73,0x73,0x5f,0x4f,0x75,0x74,0x70,0x75,0x74,0x0a,0x7b,0x0a,0x20,0x20,
+    0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,
+    0x74,0x5f,0x75,0x76,0x20,0x3a,0x20,0x54,0x45,0x58,0x43,0x4f,0x4f,0x52,0x44,0x30,
+    0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x67,0x6c,0x5f,
+    0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3a,0x20,0x53,0x56,0x5f,0x50,0x6f,
+    0x73,0x69,0x74,0x69,0x6f,0x6e,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x23,0x6c,0x69,0x6e,
+    0x65,0x20,0x31,0x37,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,
+    0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,
+    0x76,0x6f,0x69,0x64,0x20,0x76,0x65,0x72,0x74,0x5f,0x6d,0x61,0x69,0x6e,0x28,0x29,
+    0x0a,0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x37,0x20,0x22,0x2e,0x2e,0x2f,
+    0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,
+    0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x67,0x6c,0x5f,0x50,0x6f,
+    0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x6d,0x75,0x6c,0x28,0x66,0x6c,0x6f,
+    0x61,0x74,0x34,0x28,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x2c,0x20,0x30,0x2e,
+    0x30,0x66,0x2c,0x20,0x31,0x2e,0x30,0x66,0x29,0x2c,0x20,0x5f,0x32,0x32,0x5f,0x6d,
+    0x76,0x70,0x29,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x39,0x20,0x22,0x2e,
+    0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,
+    0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x69,0x66,0x20,
+    0x28,0x5f,0x32,0x32,0x5f,0x66,0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,0x20,0x3d,0x3d,
+    0x20,0x31,0x29,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,
+    0x32,0x31,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,
+    0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x5f,0x35,0x34,
+    0x20,0x3d,0x20,0x5f,0x32,0x32,0x5f,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x45,
+    0x6e,0x64,0x20,0x2d,0x20,0x5f,0x32,0x32,0x5f,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,
+    0x74,0x53,0x74,0x61,0x72,0x74,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x32,
+    0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,
+    0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x35,0x39,0x20,0x3d,0x20,
+    0x5f,0x35,0x34,0x2e,0x78,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,
+    0x6c,0x6f,0x61,0x74,0x20,0x5f,0x36,0x34,0x20,0x3d,0x20,0x5f,0x35,0x34,0x2e,0x79,
+    0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x33,0x20,0x22,0x2e,0x2e,0x2f,0x73,
+    0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,
+    0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x67,0x72,
+    0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x2e,0x78,0x20,0x3d,0x20,0x64,0x6f,
+    0x74,0x28,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x2d,0x20,0x5f,0x32,0x32,
+    0x5f,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x2c,0x20,
+    0x5f,0x35,0x34,0x29,0x20,0x2f,0x20,0x28,0x28,0x5f,0x35,0x39,0x20,0x2a,0x20,0x5f,
+    0x35,0x39,0x29,0x20,0x2b,0x20,0x28,0x5f,0x36,0x34,0x20,0x2a,0x20,0x5f,0x36,0x34,
+    0x29,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x65,0x6c,
+    0x73,0x65,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,
+    0x35,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,
+    0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,0x5f,0x32,0x32,0x5f,0x66,0x69,0x6c,
+    0x6c,0x54,0x79,0x70,0x65,0x20,0x3d,0x3d,0x20,0x32,0x29,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x37,0x20,0x22,
+    0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,
+    0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,
+    0x76,0x20,0x3d,0x20,0x28,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x2d,0x20,
+    0x5f,0x32,0x32,0x5f,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,
+    0x74,0x29,0x20,0x2f,0x20,0x64,0x69,0x73,0x74,0x61,0x6e,0x63,0x65,0x28,0x5f,0x32,
+    0x32,0x5f,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x2c,
+    0x20,0x5f,0x32,0x32,0x5f,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x45,0x6e,0x64,
+    0x29,0x2e,0x78,0x78,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7d,0x0a,
+    0x20,0x20,0x20,0x20,0x7d,0x0a,0x7d,0x0a,0x0a,0x53,0x50,0x49,0x52,0x56,0x5f,0x43,
+    0x72,0x6f,0x73,0x73,0x5f,0x4f,0x75,0x74,0x70,0x75,0x74,0x20,0x6d,0x61,0x69,0x6e,
+    0x28,0x53,0x50,0x49,0x52,0x56,0x5f,0x43,0x72,0x6f,0x73,0x73,0x5f,0x49,0x6e,0x70,
+    0x75,0x74,0x20,0x73,0x74,0x61,0x67,0x65,0x5f,0x69,0x6e,0x70,0x75,0x74,0x29,0x0a,
+    0x7b,0x0a,0x20,0x20,0x20,0x20,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3d,
+    0x20,0x73,0x74,0x61,0x67,0x65,0x5f,0x69,0x6e,0x70,0x75,0x74,0x2e,0x70,0x6f,0x73,
+    0x69,0x74,0x69,0x6f,0x6e,0x3b,0x0a,0x20,0x20,0x20,0x20,0x76,0x65,0x72,0x74,0x5f,
+    0x6d,0x61,0x69,0x6e,0x28,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x53,0x50,0x49,0x52,
+    0x56,0x5f,0x43,0x72,0x6f,0x73,0x73,0x5f,0x4f,0x75,0x74,0x70,0x75,0x74,0x20,0x73,
+    0x74,0x61,0x67,0x65,0x5f,0x6f,0x75,0x74,0x70,0x75,0x74,0x3b,0x0a,0x20,0x20,0x20,
+    0x20,0x73,0x74,0x61,0x67,0x65,0x5f,0x6f,0x75,0x74,0x70,0x75,0x74,0x2e,0x67,0x6c,
+    0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x67,0x6c,0x5f,0x50,
+    0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x3b,0x0a,0x20,0x20,0x20,0x20,0x73,0x74,0x61,
+    0x67,0x65,0x5f,0x6f,0x75,0x74,0x70,0x75,0x74,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,
+    0x6e,0x74,0x5f,0x75,0x76,0x20,0x3d,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,
+    0x5f,0x75,0x76,0x3b,0x0a,0x20,0x20,0x20,0x20,0x72,0x65,0x74,0x75,0x72,0x6e,0x20,
+    0x73,0x74,0x61,0x67,0x65,0x5f,0x6f,0x75,0x74,0x70,0x75,0x74,0x3b,0x0a,0x7d,0x0a,
+    0x00,
+};
+/*
+    cbuffer fs_path_uniforms : register(b0)
+    {
+        int _18_fillType : packoffset(c0);
+        float4 _18_colors[16] : packoffset(c1);
+        float4 _18_stops[4] : packoffset(c17);
+        int _18_stopCount : packoffset(c21);
+    };
+    
+    
+    static float4 frag_color;
+    static float2 gradient_uv;
+    
+    struct SPIRV_Cross_Input
+    {
+        float2 gradient_uv : TEXCOORD0;
+    };
+    
+    struct SPIRV_Cross_Output
+    {
+        float4 frag_color : SV_Target0;
+    };
+    
+    #line 18 "../src/sokol/shader.glsl"
+    void frag_main()
+    {
+    #line 18 "../src/sokol/shader.glsl"
+        if (_18_fillType == 0)
+        {
+    #line 20 "../src/sokol/shader.glsl"
+            frag_color = _18_colors[0];
+        }
+        else
+        {
+    #line 23 "../src/sokol/shader.glsl"
+            float _39;
+            if (_18_fillType == 1)
+            {
+                _39 = gradient_uv.x;
+            }
+            else
+            {
+                _39 = length(gradient_uv);
+            }
+    #line 24 "../src/sokol/shader.glsl"
+    #line 25 "../src/sokol/shader.glsl"
+            float4 color = lerp(_18_colors[0], _18_colors[1], smoothstep(_18_stops[0].x, _18_stops[0].y, _39).xxxx);
+    #line 26 "../src/sokol/shader.glsl"
+            for (int i = 1; i < 15; i++)
+            {
+    #line 28 "../src/sokol/shader.glsl"
+                if (i >= (_18_stopCount - 1))
+                {
+    #line 30 "../src/sokol/shader.glsl"
+                    break;
+                }
+    #line 33 "../src/sokol/shader.glsl"
+    #line 35 "../src/sokol/shader.glsl"
+    #line 34 "../src/sokol/shader.glsl"
+                int _91 = i + 1;
+    #line 35 "../src/sokol/shader.glsl"
+                color = lerp(color, _18_colors[_91], smoothstep(_18_stops[i / 4][i % 4], _18_stops[_91 / 4][_91 % 4], _39).xxxx);
+            }
+    #line 37 "../src/sokol/shader.glsl"
+            frag_color = color;
+        }
+    }
+    
+    SPIRV_Cross_Output main(SPIRV_Cross_Input stage_input)
+    {
+        gradient_uv = stage_input.gradient_uv;
+        frag_main();
+        SPIRV_Cross_Output stage_output;
+        stage_output.frag_color = frag_color;
+        return stage_output;
+    }
+*/
+static const char fs_path_source_hlsl5[1870] = {
+    0x63,0x62,0x75,0x66,0x66,0x65,0x72,0x20,0x66,0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,
+    0x75,0x6e,0x69,0x66,0x6f,0x72,0x6d,0x73,0x20,0x3a,0x20,0x72,0x65,0x67,0x69,0x73,
+    0x74,0x65,0x72,0x28,0x62,0x30,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x69,0x6e,
+    0x74,0x20,0x5f,0x31,0x38,0x5f,0x66,0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,0x20,0x3a,
+    0x20,0x70,0x61,0x63,0x6b,0x6f,0x66,0x66,0x73,0x65,0x74,0x28,0x63,0x30,0x29,0x3b,
+    0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x5f,0x31,0x38,0x5f,
+    0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x31,0x36,0x5d,0x20,0x3a,0x20,0x70,0x61,0x63,
+    0x6b,0x6f,0x66,0x66,0x73,0x65,0x74,0x28,0x63,0x31,0x29,0x3b,0x0a,0x20,0x20,0x20,
+    0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x5f,0x31,0x38,0x5f,0x73,0x74,0x6f,0x70,
+    0x73,0x5b,0x34,0x5d,0x20,0x3a,0x20,0x70,0x61,0x63,0x6b,0x6f,0x66,0x66,0x73,0x65,
+    0x74,0x28,0x63,0x31,0x37,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,
+    0x5f,0x31,0x38,0x5f,0x73,0x74,0x6f,0x70,0x43,0x6f,0x75,0x6e,0x74,0x20,0x3a,0x20,
+    0x70,0x61,0x63,0x6b,0x6f,0x66,0x66,0x73,0x65,0x74,0x28,0x63,0x32,0x31,0x29,0x3b,
+    0x0a,0x7d,0x3b,0x0a,0x0a,0x0a,0x73,0x74,0x61,0x74,0x69,0x63,0x20,0x66,0x6c,0x6f,
+    0x61,0x74,0x34,0x20,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x3b,0x0a,
+    0x73,0x74,0x61,0x74,0x69,0x63,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x67,0x72,
+    0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,
+    0x63,0x74,0x20,0x53,0x50,0x49,0x52,0x56,0x5f,0x43,0x72,0x6f,0x73,0x73,0x5f,0x49,
+    0x6e,0x70,0x75,0x74,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,
+    0x32,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x20,0x3a,0x20,
+    0x54,0x45,0x58,0x43,0x4f,0x4f,0x52,0x44,0x30,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,
+    0x74,0x72,0x75,0x63,0x74,0x20,0x53,0x50,0x49,0x52,0x56,0x5f,0x43,0x72,0x6f,0x73,
+    0x73,0x5f,0x4f,0x75,0x74,0x70,0x75,0x74,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,
+    0x6c,0x6f,0x61,0x74,0x34,0x20,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,
+    0x20,0x3a,0x20,0x53,0x56,0x5f,0x54,0x61,0x72,0x67,0x65,0x74,0x30,0x3b,0x0a,0x7d,
+    0x3b,0x0a,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x38,0x20,0x22,0x2e,0x2e,0x2f,
+    0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,
+    0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x76,0x6f,0x69,0x64,0x20,0x66,0x72,0x61,0x67,
+    0x5f,0x6d,0x61,0x69,0x6e,0x28,0x29,0x0a,0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,
+    0x31,0x38,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,
+    0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,
+    0x20,0x20,0x69,0x66,0x20,0x28,0x5f,0x31,0x38,0x5f,0x66,0x69,0x6c,0x6c,0x54,0x79,
+    0x70,0x65,0x20,0x3d,0x3d,0x20,0x30,0x29,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x23,
+    0x6c,0x69,0x6e,0x65,0x20,0x32,0x30,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,
+    0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,
+    0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x72,0x61,0x67,0x5f,
+    0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,0x20,0x5f,0x31,0x38,0x5f,0x63,0x6f,0x6c,0x6f,
+    0x72,0x73,0x5b,0x30,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,
+    0x20,0x65,0x6c,0x73,0x65,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,
+    0x65,0x20,0x32,0x33,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,
+    0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x33,
+    0x39,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,0x5f,
+    0x31,0x38,0x5f,0x66,0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,0x20,0x3d,0x3d,0x20,0x31,
+    0x29,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x5f,0x33,0x39,0x20,0x3d,0x20,0x67,0x72,
+    0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x2e,0x78,0x3b,0x0a,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x65,
+    0x6c,0x73,0x65,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x5f,0x33,0x39,0x20,0x3d,0x20,
+    0x6c,0x65,0x6e,0x67,0x74,0x68,0x28,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,
+    0x75,0x76,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7d,0x0a,0x23,
+    0x6c,0x69,0x6e,0x65,0x20,0x32,0x34,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,
+    0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,
+    0x6c,0x22,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x35,0x20,0x22,0x2e,0x2e,0x2f,
+    0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,
+    0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,
+    0x6c,0x6f,0x61,0x74,0x34,0x20,0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,0x20,0x6c,0x65,
+    0x72,0x70,0x28,0x5f,0x31,0x38,0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x30,0x5d,
+    0x2c,0x20,0x5f,0x31,0x38,0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x31,0x5d,0x2c,
+    0x20,0x73,0x6d,0x6f,0x6f,0x74,0x68,0x73,0x74,0x65,0x70,0x28,0x5f,0x31,0x38,0x5f,
+    0x73,0x74,0x6f,0x70,0x73,0x5b,0x30,0x5d,0x2e,0x78,0x2c,0x20,0x5f,0x31,0x38,0x5f,
+    0x73,0x74,0x6f,0x70,0x73,0x5b,0x30,0x5d,0x2e,0x79,0x2c,0x20,0x5f,0x33,0x39,0x29,
+    0x2e,0x78,0x78,0x78,0x78,0x29,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x36,
+    0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,
+    0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x66,0x6f,0x72,0x20,0x28,0x69,0x6e,0x74,0x20,0x69,0x20,0x3d,
+    0x20,0x31,0x3b,0x20,0x69,0x20,0x3c,0x20,0x31,0x35,0x3b,0x20,0x69,0x2b,0x2b,0x29,
+    0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,
+    0x20,0x32,0x38,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,
+    0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,0x69,
+    0x20,0x3e,0x3d,0x20,0x28,0x5f,0x31,0x38,0x5f,0x73,0x74,0x6f,0x70,0x43,0x6f,0x75,
+    0x6e,0x74,0x20,0x2d,0x20,0x31,0x29,0x29,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,0x30,0x20,
+    0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,
+    0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x62,0x72,0x65,0x61,0x6b,
+    0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7d,0x0a,
+    0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,0x33,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,
+    0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,
+    0x73,0x6c,0x22,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,0x35,0x20,0x22,0x2e,0x2e,
+    0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,
+    0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,0x34,
+    0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,
+    0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x5f,0x39,0x31,0x20,
+    0x3d,0x20,0x69,0x20,0x2b,0x20,0x31,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,
+    0x35,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,
+    0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,
+    0x20,0x6c,0x65,0x72,0x70,0x28,0x63,0x6f,0x6c,0x6f,0x72,0x2c,0x20,0x5f,0x31,0x38,
+    0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x5f,0x39,0x31,0x5d,0x2c,0x20,0x73,0x6d,
+    0x6f,0x6f,0x74,0x68,0x73,0x74,0x65,0x70,0x28,0x5f,0x31,0x38,0x5f,0x73,0x74,0x6f,
+    0x70,0x73,0x5b,0x69,0x20,0x2f,0x20,0x34,0x5d,0x5b,0x69,0x20,0x25,0x20,0x34,0x5d,
+    0x2c,0x20,0x5f,0x31,0x38,0x5f,0x73,0x74,0x6f,0x70,0x73,0x5b,0x5f,0x39,0x31,0x20,
+    0x2f,0x20,0x34,0x5d,0x5b,0x5f,0x39,0x31,0x20,0x25,0x20,0x34,0x5d,0x2c,0x20,0x5f,
+    0x33,0x39,0x29,0x2e,0x78,0x78,0x78,0x78,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x7d,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,0x37,0x20,0x22,0x2e,
+    0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,
+    0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,0x20,0x63,0x6f,
+    0x6c,0x6f,0x72,0x3b,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,0x7d,0x0a,0x0a,0x53,0x50,
+    0x49,0x52,0x56,0x5f,0x43,0x72,0x6f,0x73,0x73,0x5f,0x4f,0x75,0x74,0x70,0x75,0x74,
+    0x20,0x6d,0x61,0x69,0x6e,0x28,0x53,0x50,0x49,0x52,0x56,0x5f,0x43,0x72,0x6f,0x73,
+    0x73,0x5f,0x49,0x6e,0x70,0x75,0x74,0x20,0x73,0x74,0x61,0x67,0x65,0x5f,0x69,0x6e,
+    0x70,0x75,0x74,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x67,0x72,0x61,0x64,0x69,
+    0x65,0x6e,0x74,0x5f,0x75,0x76,0x20,0x3d,0x20,0x73,0x74,0x61,0x67,0x65,0x5f,0x69,
+    0x6e,0x70,0x75,0x74,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,
+    0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x72,0x61,0x67,0x5f,0x6d,0x61,0x69,0x6e,0x28,
+    0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x53,0x50,0x49,0x52,0x56,0x5f,0x43,0x72,0x6f,
+    0x73,0x73,0x5f,0x4f,0x75,0x74,0x70,0x75,0x74,0x20,0x73,0x74,0x61,0x67,0x65,0x5f,
+    0x6f,0x75,0x74,0x70,0x75,0x74,0x3b,0x0a,0x20,0x20,0x20,0x20,0x73,0x74,0x61,0x67,
+    0x65,0x5f,0x6f,0x75,0x74,0x70,0x75,0x74,0x2e,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,
+    0x6c,0x6f,0x72,0x20,0x3d,0x20,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,
+    0x3b,0x0a,0x20,0x20,0x20,0x20,0x72,0x65,0x74,0x75,0x72,0x6e,0x20,0x73,0x74,0x61,
+    0x67,0x65,0x5f,0x6f,0x75,0x74,0x70,0x75,0x74,0x3b,0x0a,0x7d,0x0a,0x00,
+};
+/*
+    #include <metal_stdlib>
+    #include <simd/simd.h>
+    
+    using namespace metal;
+    
+    struct vs_params
+    {
+        float4x4 mvp;
+    };
+    
+    struct main0_out
+    {
+        float2 uv [[user(locn0)]];
+        float4 gl_Position [[position]];
+    };
+    
+    struct main0_in
+    {
+        float2 position [[attribute(0)]];
+        float2 texcoord0 [[attribute(1)]];
+    };
+    
+    #line 16 "../src/sokol/shader.glsl"
+    vertex main0_out main0(main0_in in [[stage_in]], constant vs_params& _21 [[buffer(0)]])
+    {
+        main0_out out = {};
+    #line 16 "../src/sokol/shader.glsl"
+        out.gl_Position = _21.mvp * float4(in.position.x, in.position.y, 0.0, 1.0);
+    #line 17 "../src/sokol/shader.glsl"
+        out.uv = in.texcoord0;
+        return out;
+    }
+    
+*/
+static const char vs_source_metal_macos[652] = {
+    0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,0x20,0x3c,0x6d,0x65,0x74,0x61,0x6c,0x5f,
+    0x73,0x74,0x64,0x6c,0x69,0x62,0x3e,0x0a,0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,
+    0x20,0x3c,0x73,0x69,0x6d,0x64,0x2f,0x73,0x69,0x6d,0x64,0x2e,0x68,0x3e,0x0a,0x0a,
+    0x75,0x73,0x69,0x6e,0x67,0x20,0x6e,0x61,0x6d,0x65,0x73,0x70,0x61,0x63,0x65,0x20,
+    0x6d,0x65,0x74,0x61,0x6c,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x76,
+    0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,
+    0x6c,0x6f,0x61,0x74,0x34,0x78,0x34,0x20,0x6d,0x76,0x70,0x3b,0x0a,0x7d,0x3b,0x0a,
+    0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,
+    0x74,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x75,
+    0x76,0x20,0x5b,0x5b,0x75,0x73,0x65,0x72,0x28,0x6c,0x6f,0x63,0x6e,0x30,0x29,0x5d,
+    0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x67,0x6c,
+    0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x5b,0x5b,0x70,0x6f,0x73,0x69,
+    0x74,0x69,0x6f,0x6e,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,
+    0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x0a,0x7b,0x0a,0x20,0x20,
+    0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,
+    0x6e,0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28,0x30,0x29,
+    0x5d,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x74,
+    0x65,0x78,0x63,0x6f,0x6f,0x72,0x64,0x30,0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69,
+    0x62,0x75,0x74,0x65,0x28,0x31,0x29,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x23,
+    0x6c,0x69,0x6e,0x65,0x20,0x31,0x36,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,
+    0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,
+    0x6c,0x22,0x0a,0x76,0x65,0x72,0x74,0x65,0x78,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,
+    0x6f,0x75,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x28,0x6d,0x61,0x69,0x6e,0x30,0x5f,
+    0x69,0x6e,0x20,0x69,0x6e,0x20,0x5b,0x5b,0x73,0x74,0x61,0x67,0x65,0x5f,0x69,0x6e,
+    0x5d,0x5d,0x2c,0x20,0x63,0x6f,0x6e,0x73,0x74,0x61,0x6e,0x74,0x20,0x76,0x73,0x5f,
+    0x70,0x61,0x72,0x61,0x6d,0x73,0x26,0x20,0x5f,0x32,0x31,0x20,0x5b,0x5b,0x62,0x75,
+    0x66,0x66,0x65,0x72,0x28,0x30,0x29,0x5d,0x5d,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,
+    0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6f,0x75,0x74,0x20,0x3d,
+    0x20,0x7b,0x7d,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x36,0x20,0x22,0x2e,
+    0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,
+    0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x6f,0x75,0x74,
+    0x2e,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x5f,
+    0x32,0x31,0x2e,0x6d,0x76,0x70,0x20,0x2a,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x28,
+    0x69,0x6e,0x2e,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x2e,0x78,0x2c,0x20,0x69,
+    0x6e,0x2e,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x2e,0x79,0x2c,0x20,0x30,0x2e,
+    0x30,0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,
+    0x37,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,
+    0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,
+    0x20,0x6f,0x75,0x74,0x2e,0x75,0x76,0x20,0x3d,0x20,0x69,0x6e,0x2e,0x74,0x65,0x78,
+    0x63,0x6f,0x6f,0x72,0x64,0x30,0x3b,0x0a,0x20,0x20,0x20,0x20,0x72,0x65,0x74,0x75,
+    0x72,0x6e,0x20,0x6f,0x75,0x74,0x3b,0x0a,0x7d,0x0a,0x0a,0x00,
+};
+/*
+    #include <metal_stdlib>
+    #include <simd/simd.h>
+    
+    using namespace metal;
+    
+    struct main0_out
+    {
+        float4 frag_color [[color(0)]];
+    };
+    
+    struct main0_in
+    {
+        float2 uv [[user(locn0)]];
+    };
+    
+    #line 12 "../src/sokol/shader.glsl"
+    fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[texture(0)]], sampler texSmplr [[sampler(0)]])
+    {
+        main0_out out = {};
+    #line 12 "../src/sokol/shader.glsl"
+        out.frag_color = tex.sample(texSmplr, in.uv);
+        return out;
+    }
+    
+*/
+static const char fs_source_metal_macos[473] = {
+    0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,0x20,0x3c,0x6d,0x65,0x74,0x61,0x6c,0x5f,
+    0x73,0x74,0x64,0x6c,0x69,0x62,0x3e,0x0a,0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,
+    0x20,0x3c,0x73,0x69,0x6d,0x64,0x2f,0x73,0x69,0x6d,0x64,0x2e,0x68,0x3e,0x0a,0x0a,
+    0x75,0x73,0x69,0x6e,0x67,0x20,0x6e,0x61,0x6d,0x65,0x73,0x70,0x61,0x63,0x65,0x20,
+    0x6d,0x65,0x74,0x61,0x6c,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x6d,
+    0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,
+    0x6c,0x6f,0x61,0x74,0x34,0x20,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,
+    0x20,0x5b,0x5b,0x63,0x6f,0x6c,0x6f,0x72,0x28,0x30,0x29,0x5d,0x5d,0x3b,0x0a,0x7d,
+    0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,
+    0x69,0x6e,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,
+    0x75,0x76,0x20,0x5b,0x5b,0x75,0x73,0x65,0x72,0x28,0x6c,0x6f,0x63,0x6e,0x30,0x29,
+    0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x32,
+    0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,
+    0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x66,0x72,0x61,0x67,
+    0x6d,0x65,0x6e,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6d,
+    0x61,0x69,0x6e,0x30,0x28,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x20,0x69,0x6e,
+    0x20,0x5b,0x5b,0x73,0x74,0x61,0x67,0x65,0x5f,0x69,0x6e,0x5d,0x5d,0x2c,0x20,0x74,
+    0x65,0x78,0x74,0x75,0x72,0x65,0x32,0x64,0x3c,0x66,0x6c,0x6f,0x61,0x74,0x3e,0x20,
+    0x74,0x65,0x78,0x20,0x5b,0x5b,0x74,0x65,0x78,0x74,0x75,0x72,0x65,0x28,0x30,0x29,
+    0x5d,0x5d,0x2c,0x20,0x73,0x61,0x6d,0x70,0x6c,0x65,0x72,0x20,0x74,0x65,0x78,0x53,
+    0x6d,0x70,0x6c,0x72,0x20,0x5b,0x5b,0x73,0x61,0x6d,0x70,0x6c,0x65,0x72,0x28,0x30,
+    0x29,0x5d,0x5d,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x6d,0x61,0x69,0x6e,0x30,
+    0x5f,0x6f,0x75,0x74,0x20,0x6f,0x75,0x74,0x20,0x3d,0x20,0x7b,0x7d,0x3b,0x0a,0x23,
+    0x6c,0x69,0x6e,0x65,0x20,0x31,0x32,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,
+    0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,
+    0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x6f,0x75,0x74,0x2e,0x66,0x72,0x61,0x67,0x5f,
+    0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,0x20,0x74,0x65,0x78,0x2e,0x73,0x61,0x6d,0x70,
+    0x6c,0x65,0x28,0x74,0x65,0x78,0x53,0x6d,0x70,0x6c,0x72,0x2c,0x20,0x69,0x6e,0x2e,
+    0x75,0x76,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x72,0x65,0x74,0x75,0x72,0x6e,0x20,
+    0x6f,0x75,0x74,0x3b,0x0a,0x7d,0x0a,0x0a,0x00,
+};
+/*
+    #include <metal_stdlib>
+    #include <simd/simd.h>
+    
+    using namespace metal;
+    
+    struct vs_path_params
+    {
+        float4x4 mvp;
+        int fillType;
+        float2 gradientStart;
+        float2 gradientEnd;
+    };
+    
+    struct main0_out
+    {
+        float2 gradient_uv [[user(locn0)]];
+        float4 gl_Position [[position]];
+    };
+    
+    struct main0_in
+    {
+        float2 position [[attribute(0)]];
+    };
+    
+    #line 17 "../src/sokol/shader.glsl"
+    vertex main0_out main0(main0_in in [[stage_in]], constant vs_path_params& _22 [[buffer(0)]])
+    {
+        main0_out out = {};
+    #line 17 "../src/sokol/shader.glsl"
+        out.gl_Position = _22.mvp * float4(in.position, 0.0, 1.0);
+    #line 19 "../src/sokol/shader.glsl"
+        if (_22.fillType == 1)
+        {
+    #line 21 "../src/sokol/shader.glsl"
+            float2 _54 = _22.gradientEnd - _22.gradientStart;
+    #line 22 "../src/sokol/shader.glsl"
+            float _59 = _54.x;
+            float _64 = _54.y;
+    #line 23 "../src/sokol/shader.glsl"
+            out.gradient_uv.x = dot(in.position - _22.gradientStart, _54) / ((_59 * _59) + (_64 * _64));
+        }
+        else
+        {
+    #line 25 "../src/sokol/shader.glsl"
+            if (_22.fillType == 2)
+            {
+    #line 27 "../src/sokol/shader.glsl"
+                out.gradient_uv = (in.position - _22.gradientStart) / float2(distance(_22.gradientStart, _22.gradientEnd));
+            }
+        }
+        return out;
+    }
+    
+*/
+static const char vs_path_source_metal_macos[1280] = {
+    0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,0x20,0x3c,0x6d,0x65,0x74,0x61,0x6c,0x5f,
+    0x73,0x74,0x64,0x6c,0x69,0x62,0x3e,0x0a,0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,
+    0x20,0x3c,0x73,0x69,0x6d,0x64,0x2f,0x73,0x69,0x6d,0x64,0x2e,0x68,0x3e,0x0a,0x0a,
+    0x75,0x73,0x69,0x6e,0x67,0x20,0x6e,0x61,0x6d,0x65,0x73,0x70,0x61,0x63,0x65,0x20,
+    0x6d,0x65,0x74,0x61,0x6c,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x76,
+    0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x0a,0x7b,0x0a,
+    0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x78,0x34,0x20,0x6d,0x76,0x70,
+    0x3b,0x0a,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x66,0x69,0x6c,0x6c,0x54,0x79,
+    0x70,0x65,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x67,
+    0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x3b,0x0a,0x20,0x20,
+    0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,
+    0x74,0x45,0x6e,0x64,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,
+    0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x0a,0x7b,0x0a,0x20,0x20,0x20,
+    0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,
+    0x5f,0x75,0x76,0x20,0x5b,0x5b,0x75,0x73,0x65,0x72,0x28,0x6c,0x6f,0x63,0x6e,0x30,
+    0x29,0x5d,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,
+    0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x5b,0x5b,0x70,0x6f,
+    0x73,0x69,0x74,0x69,0x6f,0x6e,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74,
+    0x72,0x75,0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x0a,0x7b,0x0a,
+    0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x70,0x6f,0x73,0x69,0x74,
+    0x69,0x6f,0x6e,0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28,
+    0x30,0x29,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,
+    0x31,0x37,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,
+    0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x76,0x65,
+    0x72,0x74,0x65,0x78,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6d,
+    0x61,0x69,0x6e,0x30,0x28,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x20,0x69,0x6e,
+    0x20,0x5b,0x5b,0x73,0x74,0x61,0x67,0x65,0x5f,0x69,0x6e,0x5d,0x5d,0x2c,0x20,0x63,
+    0x6f,0x6e,0x73,0x74,0x61,0x6e,0x74,0x20,0x76,0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,
+    0x70,0x61,0x72,0x61,0x6d,0x73,0x26,0x20,0x5f,0x32,0x32,0x20,0x5b,0x5b,0x62,0x75,
+    0x66,0x66,0x65,0x72,0x28,0x30,0x29,0x5d,0x5d,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,
+    0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6f,0x75,0x74,0x20,0x3d,
+    0x20,0x7b,0x7d,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x37,0x20,0x22,0x2e,
+    0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,
+    0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x6f,0x75,0x74,
+    0x2e,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x5f,
+    0x32,0x32,0x2e,0x6d,0x76,0x70,0x20,0x2a,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x28,
+    0x69,0x6e,0x2e,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x2c,0x20,0x30,0x2e,0x30,
+    0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x39,
+    0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,
+    0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,
+    0x69,0x66,0x20,0x28,0x5f,0x32,0x32,0x2e,0x66,0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,
+    0x20,0x3d,0x3d,0x20,0x31,0x29,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,
+    0x6e,0x65,0x20,0x32,0x31,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,
+    0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,
+    0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,
+    0x5f,0x35,0x34,0x20,0x3d,0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,
+    0x6e,0x74,0x45,0x6e,0x64,0x20,0x2d,0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,
+    0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,
+    0x20,0x32,0x32,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,
+    0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x35,0x39,
+    0x20,0x3d,0x20,0x5f,0x35,0x34,0x2e,0x78,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x36,0x34,0x20,0x3d,0x20,0x5f,0x35,
+    0x34,0x2e,0x79,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x33,0x20,0x22,0x2e,
+    0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,
+    0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x6f,0x75,0x74,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,
+    0x2e,0x78,0x20,0x3d,0x20,0x64,0x6f,0x74,0x28,0x69,0x6e,0x2e,0x70,0x6f,0x73,0x69,
+    0x74,0x69,0x6f,0x6e,0x20,0x2d,0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,0x69,
+    0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x2c,0x20,0x5f,0x35,0x34,0x29,0x20,0x2f,
+    0x20,0x28,0x28,0x5f,0x35,0x39,0x20,0x2a,0x20,0x5f,0x35,0x39,0x29,0x20,0x2b,0x20,
+    0x28,0x5f,0x36,0x34,0x20,0x2a,0x20,0x5f,0x36,0x34,0x29,0x29,0x3b,0x0a,0x20,0x20,
+    0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x65,0x6c,0x73,0x65,0x0a,0x20,0x20,0x20,
+    0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x35,0x20,0x22,0x2e,0x2e,0x2f,
+    0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,
+    0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x69,
+    0x66,0x20,0x28,0x5f,0x32,0x32,0x2e,0x66,0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,0x20,
+    0x3d,0x3d,0x20,0x32,0x29,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,
+    0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x37,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,
+    0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,
+    0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x6f,0x75,0x74,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x20,
+    0x3d,0x20,0x28,0x69,0x6e,0x2e,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x2d,
+    0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,
+    0x72,0x74,0x29,0x20,0x2f,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x28,0x64,0x69,0x73,
+    0x74,0x61,0x6e,0x63,0x65,0x28,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,
+    0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x2c,0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,
+    0x64,0x69,0x65,0x6e,0x74,0x45,0x6e,0x64,0x29,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,
+    0x72,0x65,0x74,0x75,0x72,0x6e,0x20,0x6f,0x75,0x74,0x3b,0x0a,0x7d,0x0a,0x0a,0x00,
+
+};
+/*
+    #include <metal_stdlib>
+    #include <simd/simd.h>
+    
+    using namespace metal;
+    
+    struct fs_path_uniforms
+    {
+        int fillType;
+        float4 colors[16];
+        float4 stops[4];
+        int stopCount;
+    };
+    
+    struct main0_out
+    {
+        float4 frag_color [[color(0)]];
+    };
+    
+    struct main0_in
+    {
+        float2 gradient_uv [[user(locn0)]];
+    };
+    
+    #line 18 "../src/sokol/shader.glsl"
+    fragment main0_out main0(main0_in in [[stage_in]], constant fs_path_uniforms& _18 [[buffer(0)]])
+    {
+        main0_out out = {};
+    #line 18 "../src/sokol/shader.glsl"
+        if (_18.fillType == 0)
+        {
+    #line 20 "../src/sokol/shader.glsl"
+            out.frag_color = _18.colors[0];
+        }
+        else
+        {
+    #line 23 "../src/sokol/shader.glsl"
+            float _39;
+            if (_18.fillType == 1)
+            {
+                _39 = in.gradient_uv.x;
+            }
+            else
+            {
+                _39 = length(in.gradient_uv);
+            }
+    #line 24 "../src/sokol/shader.glsl"
+    #line 25 "../src/sokol/shader.glsl"
+            float4 color = mix(_18.colors[0], _18.colors[1], float4(smoothstep(_18.stops[0].x, _18.stops[0].y, _39)));
+    #line 26 "../src/sokol/shader.glsl"
+            for (int i = 1; i < 15; i++)
+            {
+    #line 28 "../src/sokol/shader.glsl"
+                if (i >= (_18.stopCount - 1))
+                {
+    #line 30 "../src/sokol/shader.glsl"
+                    break;
+                }
+    #line 33 "../src/sokol/shader.glsl"
+    #line 35 "../src/sokol/shader.glsl"
+    #line 34 "../src/sokol/shader.glsl"
+                int _91 = i + 1;
+    #line 35 "../src/sokol/shader.glsl"
+                color = mix(color, _18.colors[_91], float4(smoothstep(_18.stops[i / 4][i % 4], _18.stops[_91 / 4][_91 % 4], _39)));
+            }
+    #line 37 "../src/sokol/shader.glsl"
+            out.frag_color = color;
+        }
+        return out;
+    }
+    
+*/
+static const char fs_path_source_metal_macos[1686] = {
+    0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,0x20,0x3c,0x6d,0x65,0x74,0x61,0x6c,0x5f,
+    0x73,0x74,0x64,0x6c,0x69,0x62,0x3e,0x0a,0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,
+    0x20,0x3c,0x73,0x69,0x6d,0x64,0x2f,0x73,0x69,0x6d,0x64,0x2e,0x68,0x3e,0x0a,0x0a,
+    0x75,0x73,0x69,0x6e,0x67,0x20,0x6e,0x61,0x6d,0x65,0x73,0x70,0x61,0x63,0x65,0x20,
+    0x6d,0x65,0x74,0x61,0x6c,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x66,
+    0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,0x75,0x6e,0x69,0x66,0x6f,0x72,0x6d,0x73,0x0a,
+    0x7b,0x0a,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x66,0x69,0x6c,0x6c,0x54,0x79,
+    0x70,0x65,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x63,
+    0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x31,0x36,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,
+    0x6c,0x6f,0x61,0x74,0x34,0x20,0x73,0x74,0x6f,0x70,0x73,0x5b,0x34,0x5d,0x3b,0x0a,
+    0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x73,0x74,0x6f,0x70,0x43,0x6f,0x75,0x6e,
+    0x74,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x6d,0x61,
+    0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,
+    0x6f,0x61,0x74,0x34,0x20,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x20,
+    0x5b,0x5b,0x63,0x6f,0x6c,0x6f,0x72,0x28,0x30,0x29,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,
+    0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,
+    0x6e,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x67,
+    0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x20,0x5b,0x5b,0x75,0x73,0x65,
+    0x72,0x28,0x6c,0x6f,0x63,0x6e,0x30,0x29,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,
+    0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x38,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,
+    0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,
+    0x73,0x6c,0x22,0x0a,0x66,0x72,0x61,0x67,0x6d,0x65,0x6e,0x74,0x20,0x6d,0x61,0x69,
+    0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x28,0x6d,0x61,0x69,
+    0x6e,0x30,0x5f,0x69,0x6e,0x20,0x69,0x6e,0x20,0x5b,0x5b,0x73,0x74,0x61,0x67,0x65,
+    0x5f,0x69,0x6e,0x5d,0x5d,0x2c,0x20,0x63,0x6f,0x6e,0x73,0x74,0x61,0x6e,0x74,0x20,
+    0x66,0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,0x75,0x6e,0x69,0x66,0x6f,0x72,0x6d,0x73,
+    0x26,0x20,0x5f,0x31,0x38,0x20,0x5b,0x5b,0x62,0x75,0x66,0x66,0x65,0x72,0x28,0x30,
+    0x29,0x5d,0x5d,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x6d,0x61,0x69,0x6e,0x30,
+    0x5f,0x6f,0x75,0x74,0x20,0x6f,0x75,0x74,0x20,0x3d,0x20,0x7b,0x7d,0x3b,0x0a,0x23,
+    0x6c,0x69,0x6e,0x65,0x20,0x31,0x38,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,
+    0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,
+    0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,0x5f,0x31,0x38,0x2e,0x66,
+    0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,0x20,0x3d,0x3d,0x20,0x30,0x29,0x0a,0x20,0x20,
+    0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x30,0x20,0x22,0x2e,0x2e,
+    0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,
+    0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x6f,0x75,0x74,0x2e,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,
+    0x20,0x5f,0x31,0x38,0x2e,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x30,0x5d,0x3b,0x0a,
+    0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x65,0x6c,0x73,0x65,0x0a,0x20,
+    0x20,0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x33,0x20,0x22,0x2e,
+    0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,
+    0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x33,0x39,0x3b,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,0x5f,0x31,0x38,0x2e,0x66,0x69,0x6c,0x6c,
+    0x54,0x79,0x70,0x65,0x20,0x3d,0x3d,0x20,0x31,0x29,0x0a,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x5f,0x33,0x39,0x20,0x3d,0x20,0x69,0x6e,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,
+    0x6e,0x74,0x5f,0x75,0x76,0x2e,0x78,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x65,0x6c,0x73,0x65,0x0a,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x5f,0x33,0x39,0x20,0x3d,0x20,0x6c,0x65,0x6e,0x67,
+    0x74,0x68,0x28,0x69,0x6e,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,
+    0x76,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7d,0x0a,0x23,0x6c,
+    0x69,0x6e,0x65,0x20,0x32,0x34,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,
+    0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,
+    0x22,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x35,0x20,0x22,0x2e,0x2e,0x2f,0x73,
+    0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,
+    0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x6c,
+    0x6f,0x61,0x74,0x34,0x20,0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,0x20,0x6d,0x69,0x78,
+    0x28,0x5f,0x31,0x38,0x2e,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x30,0x5d,0x2c,0x20,
+    0x5f,0x31,0x38,0x2e,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x31,0x5d,0x2c,0x20,0x66,
+    0x6c,0x6f,0x61,0x74,0x34,0x28,0x73,0x6d,0x6f,0x6f,0x74,0x68,0x73,0x74,0x65,0x70,
+    0x28,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x73,0x5b,0x30,0x5d,0x2e,0x78,0x2c,
+    0x20,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x73,0x5b,0x30,0x5d,0x2e,0x79,0x2c,
+    0x20,0x5f,0x33,0x39,0x29,0x29,0x29,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,
+    0x36,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,
+    0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x66,0x6f,0x72,0x20,0x28,0x69,0x6e,0x74,0x20,0x69,0x20,
+    0x3d,0x20,0x31,0x3b,0x20,0x69,0x20,0x3c,0x20,0x31,0x35,0x3b,0x20,0x69,0x2b,0x2b,
+    0x29,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,
+    0x65,0x20,0x32,0x38,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,
+    0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,
+    0x69,0x20,0x3e,0x3d,0x20,0x28,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x43,0x6f,
+    0x75,0x6e,0x74,0x20,0x2d,0x20,0x31,0x29,0x29,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,0x30,
+    0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,
+    0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x62,0x72,0x65,0x61,
+    0x6b,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7d,
+    0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,0x33,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,
+    0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,
+    0x6c,0x73,0x6c,0x22,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,0x35,0x20,0x22,0x2e,
+    0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,
+    0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,
+    0x34,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,
+    0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x5f,0x39,0x31,
+    0x20,0x3d,0x20,0x69,0x20,0x2b,0x20,0x31,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,
+    0x33,0x35,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,
+    0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x63,0x6f,0x6c,0x6f,0x72,0x20,
+    0x3d,0x20,0x6d,0x69,0x78,0x28,0x63,0x6f,0x6c,0x6f,0x72,0x2c,0x20,0x5f,0x31,0x38,
+    0x2e,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x5f,0x39,0x31,0x5d,0x2c,0x20,0x66,0x6c,
+    0x6f,0x61,0x74,0x34,0x28,0x73,0x6d,0x6f,0x6f,0x74,0x68,0x73,0x74,0x65,0x70,0x28,
+    0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x73,0x5b,0x69,0x20,0x2f,0x20,0x34,0x5d,
+    0x5b,0x69,0x20,0x25,0x20,0x34,0x5d,0x2c,0x20,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,
+    0x70,0x73,0x5b,0x5f,0x39,0x31,0x20,0x2f,0x20,0x34,0x5d,0x5b,0x5f,0x39,0x31,0x20,
+    0x25,0x20,0x34,0x5d,0x2c,0x20,0x5f,0x33,0x39,0x29,0x29,0x29,0x3b,0x0a,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x7d,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,0x37,
+    0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,
+    0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x6f,0x75,0x74,0x2e,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,
+    0x6f,0x72,0x20,0x3d,0x20,0x63,0x6f,0x6c,0x6f,0x72,0x3b,0x0a,0x20,0x20,0x20,0x20,
+    0x7d,0x0a,0x20,0x20,0x20,0x20,0x72,0x65,0x74,0x75,0x72,0x6e,0x20,0x6f,0x75,0x74,
+    0x3b,0x0a,0x7d,0x0a,0x0a,0x00,
+};
+/*
+    #include <metal_stdlib>
+    #include <simd/simd.h>
+    
+    using namespace metal;
+    
+    struct vs_params
+    {
+        float4x4 mvp;
+    };
+    
+    struct main0_out
+    {
+        float2 uv [[user(locn0)]];
+        float4 gl_Position [[position]];
+    };
+    
+    struct main0_in
+    {
+        float2 position [[attribute(0)]];
+        float2 texcoord0 [[attribute(1)]];
+    };
+    
+    #line 16 "../src/sokol/shader.glsl"
+    vertex main0_out main0(main0_in in [[stage_in]], constant vs_params& _21 [[buffer(0)]])
+    {
+        main0_out out = {};
+    #line 16 "../src/sokol/shader.glsl"
+        out.gl_Position = _21.mvp * float4(in.position.x, in.position.y, 0.0, 1.0);
+    #line 17 "../src/sokol/shader.glsl"
+        out.uv = in.texcoord0;
+        return out;
+    }
+    
+*/
+static const char vs_source_metal_ios[652] = {
+    0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,0x20,0x3c,0x6d,0x65,0x74,0x61,0x6c,0x5f,
+    0x73,0x74,0x64,0x6c,0x69,0x62,0x3e,0x0a,0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,
+    0x20,0x3c,0x73,0x69,0x6d,0x64,0x2f,0x73,0x69,0x6d,0x64,0x2e,0x68,0x3e,0x0a,0x0a,
+    0x75,0x73,0x69,0x6e,0x67,0x20,0x6e,0x61,0x6d,0x65,0x73,0x70,0x61,0x63,0x65,0x20,
+    0x6d,0x65,0x74,0x61,0x6c,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x76,
+    0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,
+    0x6c,0x6f,0x61,0x74,0x34,0x78,0x34,0x20,0x6d,0x76,0x70,0x3b,0x0a,0x7d,0x3b,0x0a,
+    0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,
+    0x74,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x75,
+    0x76,0x20,0x5b,0x5b,0x75,0x73,0x65,0x72,0x28,0x6c,0x6f,0x63,0x6e,0x30,0x29,0x5d,
+    0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x67,0x6c,
+    0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x5b,0x5b,0x70,0x6f,0x73,0x69,
+    0x74,0x69,0x6f,0x6e,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,
+    0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x0a,0x7b,0x0a,0x20,0x20,
+    0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,
+    0x6e,0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28,0x30,0x29,
+    0x5d,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x74,
+    0x65,0x78,0x63,0x6f,0x6f,0x72,0x64,0x30,0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69,
+    0x62,0x75,0x74,0x65,0x28,0x31,0x29,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x23,
+    0x6c,0x69,0x6e,0x65,0x20,0x31,0x36,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,
+    0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,
+    0x6c,0x22,0x0a,0x76,0x65,0x72,0x74,0x65,0x78,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,
+    0x6f,0x75,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x28,0x6d,0x61,0x69,0x6e,0x30,0x5f,
+    0x69,0x6e,0x20,0x69,0x6e,0x20,0x5b,0x5b,0x73,0x74,0x61,0x67,0x65,0x5f,0x69,0x6e,
+    0x5d,0x5d,0x2c,0x20,0x63,0x6f,0x6e,0x73,0x74,0x61,0x6e,0x74,0x20,0x76,0x73,0x5f,
+    0x70,0x61,0x72,0x61,0x6d,0x73,0x26,0x20,0x5f,0x32,0x31,0x20,0x5b,0x5b,0x62,0x75,
+    0x66,0x66,0x65,0x72,0x28,0x30,0x29,0x5d,0x5d,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,
+    0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6f,0x75,0x74,0x20,0x3d,
+    0x20,0x7b,0x7d,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x36,0x20,0x22,0x2e,
+    0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,
+    0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x6f,0x75,0x74,
+    0x2e,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x5f,
+    0x32,0x31,0x2e,0x6d,0x76,0x70,0x20,0x2a,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x28,
+    0x69,0x6e,0x2e,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x2e,0x78,0x2c,0x20,0x69,
+    0x6e,0x2e,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x2e,0x79,0x2c,0x20,0x30,0x2e,
+    0x30,0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,
+    0x37,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,
+    0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,
+    0x20,0x6f,0x75,0x74,0x2e,0x75,0x76,0x20,0x3d,0x20,0x69,0x6e,0x2e,0x74,0x65,0x78,
+    0x63,0x6f,0x6f,0x72,0x64,0x30,0x3b,0x0a,0x20,0x20,0x20,0x20,0x72,0x65,0x74,0x75,
+    0x72,0x6e,0x20,0x6f,0x75,0x74,0x3b,0x0a,0x7d,0x0a,0x0a,0x00,
+};
+/*
+    #include <metal_stdlib>
+    #include <simd/simd.h>
+    
+    using namespace metal;
+    
+    struct main0_out
+    {
+        float4 frag_color [[color(0)]];
+    };
+    
+    struct main0_in
+    {
+        float2 uv [[user(locn0)]];
+    };
+    
+    #line 12 "../src/sokol/shader.glsl"
+    fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[texture(0)]], sampler texSmplr [[sampler(0)]])
+    {
+        main0_out out = {};
+    #line 12 "../src/sokol/shader.glsl"
+        out.frag_color = tex.sample(texSmplr, in.uv);
+        return out;
+    }
+    
+*/
+static const char fs_source_metal_ios[473] = {
+    0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,0x20,0x3c,0x6d,0x65,0x74,0x61,0x6c,0x5f,
+    0x73,0x74,0x64,0x6c,0x69,0x62,0x3e,0x0a,0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,
+    0x20,0x3c,0x73,0x69,0x6d,0x64,0x2f,0x73,0x69,0x6d,0x64,0x2e,0x68,0x3e,0x0a,0x0a,
+    0x75,0x73,0x69,0x6e,0x67,0x20,0x6e,0x61,0x6d,0x65,0x73,0x70,0x61,0x63,0x65,0x20,
+    0x6d,0x65,0x74,0x61,0x6c,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x6d,
+    0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,
+    0x6c,0x6f,0x61,0x74,0x34,0x20,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,
+    0x20,0x5b,0x5b,0x63,0x6f,0x6c,0x6f,0x72,0x28,0x30,0x29,0x5d,0x5d,0x3b,0x0a,0x7d,
+    0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,
+    0x69,0x6e,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,
+    0x75,0x76,0x20,0x5b,0x5b,0x75,0x73,0x65,0x72,0x28,0x6c,0x6f,0x63,0x6e,0x30,0x29,
+    0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x32,
+    0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,
+    0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x66,0x72,0x61,0x67,
+    0x6d,0x65,0x6e,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6d,
+    0x61,0x69,0x6e,0x30,0x28,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x20,0x69,0x6e,
+    0x20,0x5b,0x5b,0x73,0x74,0x61,0x67,0x65,0x5f,0x69,0x6e,0x5d,0x5d,0x2c,0x20,0x74,
+    0x65,0x78,0x74,0x75,0x72,0x65,0x32,0x64,0x3c,0x66,0x6c,0x6f,0x61,0x74,0x3e,0x20,
+    0x74,0x65,0x78,0x20,0x5b,0x5b,0x74,0x65,0x78,0x74,0x75,0x72,0x65,0x28,0x30,0x29,
+    0x5d,0x5d,0x2c,0x20,0x73,0x61,0x6d,0x70,0x6c,0x65,0x72,0x20,0x74,0x65,0x78,0x53,
+    0x6d,0x70,0x6c,0x72,0x20,0x5b,0x5b,0x73,0x61,0x6d,0x70,0x6c,0x65,0x72,0x28,0x30,
+    0x29,0x5d,0x5d,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x6d,0x61,0x69,0x6e,0x30,
+    0x5f,0x6f,0x75,0x74,0x20,0x6f,0x75,0x74,0x20,0x3d,0x20,0x7b,0x7d,0x3b,0x0a,0x23,
+    0x6c,0x69,0x6e,0x65,0x20,0x31,0x32,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,
+    0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,
+    0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x6f,0x75,0x74,0x2e,0x66,0x72,0x61,0x67,0x5f,
+    0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,0x20,0x74,0x65,0x78,0x2e,0x73,0x61,0x6d,0x70,
+    0x6c,0x65,0x28,0x74,0x65,0x78,0x53,0x6d,0x70,0x6c,0x72,0x2c,0x20,0x69,0x6e,0x2e,
+    0x75,0x76,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x72,0x65,0x74,0x75,0x72,0x6e,0x20,
+    0x6f,0x75,0x74,0x3b,0x0a,0x7d,0x0a,0x0a,0x00,
+};
+/*
+    #include <metal_stdlib>
+    #include <simd/simd.h>
+    
+    using namespace metal;
+    
+    struct vs_path_params
+    {
+        float4x4 mvp;
+        int fillType;
+        float2 gradientStart;
+        float2 gradientEnd;
+    };
+    
+    struct main0_out
+    {
+        float2 gradient_uv [[user(locn0)]];
+        float4 gl_Position [[position]];
+    };
+    
+    struct main0_in
+    {
+        float2 position [[attribute(0)]];
+    };
+    
+    #line 17 "../src/sokol/shader.glsl"
+    vertex main0_out main0(main0_in in [[stage_in]], constant vs_path_params& _22 [[buffer(0)]])
+    {
+        main0_out out = {};
+    #line 17 "../src/sokol/shader.glsl"
+        out.gl_Position = _22.mvp * float4(in.position, 0.0, 1.0);
+    #line 19 "../src/sokol/shader.glsl"
+        if (_22.fillType == 1)
+        {
+    #line 21 "../src/sokol/shader.glsl"
+            float2 _54 = _22.gradientEnd - _22.gradientStart;
+    #line 22 "../src/sokol/shader.glsl"
+            float _59 = _54.x;
+            float _64 = _54.y;
+    #line 23 "../src/sokol/shader.glsl"
+            out.gradient_uv.x = dot(in.position - _22.gradientStart, _54) / ((_59 * _59) + (_64 * _64));
+        }
+        else
+        {
+    #line 25 "../src/sokol/shader.glsl"
+            if (_22.fillType == 2)
+            {
+    #line 27 "../src/sokol/shader.glsl"
+                out.gradient_uv = (in.position - _22.gradientStart) / float2(distance(_22.gradientStart, _22.gradientEnd));
+            }
+        }
+        return out;
+    }
+    
+*/
+static const char vs_path_source_metal_ios[1280] = {
+    0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,0x20,0x3c,0x6d,0x65,0x74,0x61,0x6c,0x5f,
+    0x73,0x74,0x64,0x6c,0x69,0x62,0x3e,0x0a,0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,
+    0x20,0x3c,0x73,0x69,0x6d,0x64,0x2f,0x73,0x69,0x6d,0x64,0x2e,0x68,0x3e,0x0a,0x0a,
+    0x75,0x73,0x69,0x6e,0x67,0x20,0x6e,0x61,0x6d,0x65,0x73,0x70,0x61,0x63,0x65,0x20,
+    0x6d,0x65,0x74,0x61,0x6c,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x76,
+    0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x0a,0x7b,0x0a,
+    0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x78,0x34,0x20,0x6d,0x76,0x70,
+    0x3b,0x0a,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x66,0x69,0x6c,0x6c,0x54,0x79,
+    0x70,0x65,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x67,
+    0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x3b,0x0a,0x20,0x20,
+    0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,
+    0x74,0x45,0x6e,0x64,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,
+    0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x0a,0x7b,0x0a,0x20,0x20,0x20,
+    0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,
+    0x5f,0x75,0x76,0x20,0x5b,0x5b,0x75,0x73,0x65,0x72,0x28,0x6c,0x6f,0x63,0x6e,0x30,
+    0x29,0x5d,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,
+    0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x5b,0x5b,0x70,0x6f,
+    0x73,0x69,0x74,0x69,0x6f,0x6e,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74,
+    0x72,0x75,0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x0a,0x7b,0x0a,
+    0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x70,0x6f,0x73,0x69,0x74,
+    0x69,0x6f,0x6e,0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28,
+    0x30,0x29,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,
+    0x31,0x37,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,
+    0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x76,0x65,
+    0x72,0x74,0x65,0x78,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6d,
+    0x61,0x69,0x6e,0x30,0x28,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x20,0x69,0x6e,
+    0x20,0x5b,0x5b,0x73,0x74,0x61,0x67,0x65,0x5f,0x69,0x6e,0x5d,0x5d,0x2c,0x20,0x63,
+    0x6f,0x6e,0x73,0x74,0x61,0x6e,0x74,0x20,0x76,0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,
+    0x70,0x61,0x72,0x61,0x6d,0x73,0x26,0x20,0x5f,0x32,0x32,0x20,0x5b,0x5b,0x62,0x75,
+    0x66,0x66,0x65,0x72,0x28,0x30,0x29,0x5d,0x5d,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,
+    0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6f,0x75,0x74,0x20,0x3d,
+    0x20,0x7b,0x7d,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x37,0x20,0x22,0x2e,
+    0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,
+    0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x6f,0x75,0x74,
+    0x2e,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x5f,
+    0x32,0x32,0x2e,0x6d,0x76,0x70,0x20,0x2a,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x28,
+    0x69,0x6e,0x2e,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x2c,0x20,0x30,0x2e,0x30,
+    0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x39,
+    0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,
+    0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,
+    0x69,0x66,0x20,0x28,0x5f,0x32,0x32,0x2e,0x66,0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,
+    0x20,0x3d,0x3d,0x20,0x31,0x29,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,
+    0x6e,0x65,0x20,0x32,0x31,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,
+    0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,
+    0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,
+    0x5f,0x35,0x34,0x20,0x3d,0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,
+    0x6e,0x74,0x45,0x6e,0x64,0x20,0x2d,0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,
+    0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,
+    0x20,0x32,0x32,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,
+    0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x35,0x39,
+    0x20,0x3d,0x20,0x5f,0x35,0x34,0x2e,0x78,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x36,0x34,0x20,0x3d,0x20,0x5f,0x35,
+    0x34,0x2e,0x79,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x33,0x20,0x22,0x2e,
+    0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,
+    0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x6f,0x75,0x74,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,
+    0x2e,0x78,0x20,0x3d,0x20,0x64,0x6f,0x74,0x28,0x69,0x6e,0x2e,0x70,0x6f,0x73,0x69,
+    0x74,0x69,0x6f,0x6e,0x20,0x2d,0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,0x69,
+    0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x2c,0x20,0x5f,0x35,0x34,0x29,0x20,0x2f,
+    0x20,0x28,0x28,0x5f,0x35,0x39,0x20,0x2a,0x20,0x5f,0x35,0x39,0x29,0x20,0x2b,0x20,
+    0x28,0x5f,0x36,0x34,0x20,0x2a,0x20,0x5f,0x36,0x34,0x29,0x29,0x3b,0x0a,0x20,0x20,
+    0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x65,0x6c,0x73,0x65,0x0a,0x20,0x20,0x20,
+    0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x35,0x20,0x22,0x2e,0x2e,0x2f,
+    0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,
+    0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x69,
+    0x66,0x20,0x28,0x5f,0x32,0x32,0x2e,0x66,0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,0x20,
+    0x3d,0x3d,0x20,0x32,0x29,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,
+    0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x37,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,
+    0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,
+    0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x6f,0x75,0x74,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x20,
+    0x3d,0x20,0x28,0x69,0x6e,0x2e,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x2d,
+    0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,
+    0x72,0x74,0x29,0x20,0x2f,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x28,0x64,0x69,0x73,
+    0x74,0x61,0x6e,0x63,0x65,0x28,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,
+    0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x2c,0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,
+    0x64,0x69,0x65,0x6e,0x74,0x45,0x6e,0x64,0x29,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,
+    0x72,0x65,0x74,0x75,0x72,0x6e,0x20,0x6f,0x75,0x74,0x3b,0x0a,0x7d,0x0a,0x0a,0x00,
+
+};
+/*
+    #include <metal_stdlib>
+    #include <simd/simd.h>
+    
+    using namespace metal;
+    
+    struct fs_path_uniforms
+    {
+        int fillType;
+        float4 colors[16];
+        float4 stops[4];
+        int stopCount;
+    };
+    
+    struct main0_out
+    {
+        float4 frag_color [[color(0)]];
+    };
+    
+    struct main0_in
+    {
+        float2 gradient_uv [[user(locn0)]];
+    };
+    
+    #line 18 "../src/sokol/shader.glsl"
+    fragment main0_out main0(main0_in in [[stage_in]], constant fs_path_uniforms& _18 [[buffer(0)]])
+    {
+        main0_out out = {};
+    #line 18 "../src/sokol/shader.glsl"
+        if (_18.fillType == 0)
+        {
+    #line 20 "../src/sokol/shader.glsl"
+            out.frag_color = _18.colors[0];
+        }
+        else
+        {
+    #line 23 "../src/sokol/shader.glsl"
+            float _39;
+            if (_18.fillType == 1)
+            {
+                _39 = in.gradient_uv.x;
+            }
+            else
+            {
+                _39 = length(in.gradient_uv);
+            }
+    #line 24 "../src/sokol/shader.glsl"
+    #line 25 "../src/sokol/shader.glsl"
+            float4 color = mix(_18.colors[0], _18.colors[1], float4(smoothstep(_18.stops[0].x, _18.stops[0].y, _39)));
+    #line 26 "../src/sokol/shader.glsl"
+            for (int i = 1; i < 15; i++)
+            {
+    #line 28 "../src/sokol/shader.glsl"
+                if (i >= (_18.stopCount - 1))
+                {
+    #line 30 "../src/sokol/shader.glsl"
+                    break;
+                }
+    #line 33 "../src/sokol/shader.glsl"
+    #line 35 "../src/sokol/shader.glsl"
+    #line 34 "../src/sokol/shader.glsl"
+                int _91 = i + 1;
+    #line 35 "../src/sokol/shader.glsl"
+                color = mix(color, _18.colors[_91], float4(smoothstep(_18.stops[i / 4][i % 4], _18.stops[_91 / 4][_91 % 4], _39)));
+            }
+    #line 37 "../src/sokol/shader.glsl"
+            out.frag_color = color;
+        }
+        return out;
+    }
+    
+*/
+static const char fs_path_source_metal_ios[1686] = {
+    0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,0x20,0x3c,0x6d,0x65,0x74,0x61,0x6c,0x5f,
+    0x73,0x74,0x64,0x6c,0x69,0x62,0x3e,0x0a,0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,
+    0x20,0x3c,0x73,0x69,0x6d,0x64,0x2f,0x73,0x69,0x6d,0x64,0x2e,0x68,0x3e,0x0a,0x0a,
+    0x75,0x73,0x69,0x6e,0x67,0x20,0x6e,0x61,0x6d,0x65,0x73,0x70,0x61,0x63,0x65,0x20,
+    0x6d,0x65,0x74,0x61,0x6c,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x66,
+    0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,0x75,0x6e,0x69,0x66,0x6f,0x72,0x6d,0x73,0x0a,
+    0x7b,0x0a,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x66,0x69,0x6c,0x6c,0x54,0x79,
+    0x70,0x65,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x63,
+    0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x31,0x36,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,
+    0x6c,0x6f,0x61,0x74,0x34,0x20,0x73,0x74,0x6f,0x70,0x73,0x5b,0x34,0x5d,0x3b,0x0a,
+    0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x73,0x74,0x6f,0x70,0x43,0x6f,0x75,0x6e,
+    0x74,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x6d,0x61,
+    0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,
+    0x6f,0x61,0x74,0x34,0x20,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x20,
+    0x5b,0x5b,0x63,0x6f,0x6c,0x6f,0x72,0x28,0x30,0x29,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,
+    0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,
+    0x6e,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x67,
+    0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x20,0x5b,0x5b,0x75,0x73,0x65,
+    0x72,0x28,0x6c,0x6f,0x63,0x6e,0x30,0x29,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,
+    0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x38,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,
+    0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,
+    0x73,0x6c,0x22,0x0a,0x66,0x72,0x61,0x67,0x6d,0x65,0x6e,0x74,0x20,0x6d,0x61,0x69,
+    0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x28,0x6d,0x61,0x69,
+    0x6e,0x30,0x5f,0x69,0x6e,0x20,0x69,0x6e,0x20,0x5b,0x5b,0x73,0x74,0x61,0x67,0x65,
+    0x5f,0x69,0x6e,0x5d,0x5d,0x2c,0x20,0x63,0x6f,0x6e,0x73,0x74,0x61,0x6e,0x74,0x20,
+    0x66,0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,0x75,0x6e,0x69,0x66,0x6f,0x72,0x6d,0x73,
+    0x26,0x20,0x5f,0x31,0x38,0x20,0x5b,0x5b,0x62,0x75,0x66,0x66,0x65,0x72,0x28,0x30,
+    0x29,0x5d,0x5d,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x6d,0x61,0x69,0x6e,0x30,
+    0x5f,0x6f,0x75,0x74,0x20,0x6f,0x75,0x74,0x20,0x3d,0x20,0x7b,0x7d,0x3b,0x0a,0x23,
+    0x6c,0x69,0x6e,0x65,0x20,0x31,0x38,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,
+    0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,
+    0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,0x5f,0x31,0x38,0x2e,0x66,
+    0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,0x20,0x3d,0x3d,0x20,0x30,0x29,0x0a,0x20,0x20,
+    0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x30,0x20,0x22,0x2e,0x2e,
+    0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,
+    0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x6f,0x75,0x74,0x2e,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,
+    0x20,0x5f,0x31,0x38,0x2e,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x30,0x5d,0x3b,0x0a,
+    0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x65,0x6c,0x73,0x65,0x0a,0x20,
+    0x20,0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x33,0x20,0x22,0x2e,
+    0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,
+    0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x33,0x39,0x3b,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,0x5f,0x31,0x38,0x2e,0x66,0x69,0x6c,0x6c,
+    0x54,0x79,0x70,0x65,0x20,0x3d,0x3d,0x20,0x31,0x29,0x0a,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x5f,0x33,0x39,0x20,0x3d,0x20,0x69,0x6e,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,
+    0x6e,0x74,0x5f,0x75,0x76,0x2e,0x78,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x65,0x6c,0x73,0x65,0x0a,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x5f,0x33,0x39,0x20,0x3d,0x20,0x6c,0x65,0x6e,0x67,
+    0x74,0x68,0x28,0x69,0x6e,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,
+    0x76,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7d,0x0a,0x23,0x6c,
+    0x69,0x6e,0x65,0x20,0x32,0x34,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,
+    0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,
+    0x22,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x35,0x20,0x22,0x2e,0x2e,0x2f,0x73,
+    0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,
+    0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x6c,
+    0x6f,0x61,0x74,0x34,0x20,0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,0x20,0x6d,0x69,0x78,
+    0x28,0x5f,0x31,0x38,0x2e,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x30,0x5d,0x2c,0x20,
+    0x5f,0x31,0x38,0x2e,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x31,0x5d,0x2c,0x20,0x66,
+    0x6c,0x6f,0x61,0x74,0x34,0x28,0x73,0x6d,0x6f,0x6f,0x74,0x68,0x73,0x74,0x65,0x70,
+    0x28,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x73,0x5b,0x30,0x5d,0x2e,0x78,0x2c,
+    0x20,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x73,0x5b,0x30,0x5d,0x2e,0x79,0x2c,
+    0x20,0x5f,0x33,0x39,0x29,0x29,0x29,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,
+    0x36,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,
+    0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x66,0x6f,0x72,0x20,0x28,0x69,0x6e,0x74,0x20,0x69,0x20,
+    0x3d,0x20,0x31,0x3b,0x20,0x69,0x20,0x3c,0x20,0x31,0x35,0x3b,0x20,0x69,0x2b,0x2b,
+    0x29,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,
+    0x65,0x20,0x32,0x38,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,
+    0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,
+    0x69,0x20,0x3e,0x3d,0x20,0x28,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x43,0x6f,
+    0x75,0x6e,0x74,0x20,0x2d,0x20,0x31,0x29,0x29,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,0x30,
+    0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,
+    0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x62,0x72,0x65,0x61,
+    0x6b,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7d,
+    0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,0x33,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,
+    0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,
+    0x6c,0x73,0x6c,0x22,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,0x35,0x20,0x22,0x2e,
+    0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,
+    0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,
+    0x34,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,
+    0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x5f,0x39,0x31,
+    0x20,0x3d,0x20,0x69,0x20,0x2b,0x20,0x31,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,
+    0x33,0x35,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,
+    0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x63,0x6f,0x6c,0x6f,0x72,0x20,
+    0x3d,0x20,0x6d,0x69,0x78,0x28,0x63,0x6f,0x6c,0x6f,0x72,0x2c,0x20,0x5f,0x31,0x38,
+    0x2e,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x5f,0x39,0x31,0x5d,0x2c,0x20,0x66,0x6c,
+    0x6f,0x61,0x74,0x34,0x28,0x73,0x6d,0x6f,0x6f,0x74,0x68,0x73,0x74,0x65,0x70,0x28,
+    0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x73,0x5b,0x69,0x20,0x2f,0x20,0x34,0x5d,
+    0x5b,0x69,0x20,0x25,0x20,0x34,0x5d,0x2c,0x20,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,
+    0x70,0x73,0x5b,0x5f,0x39,0x31,0x20,0x2f,0x20,0x34,0x5d,0x5b,0x5f,0x39,0x31,0x20,
+    0x25,0x20,0x34,0x5d,0x2c,0x20,0x5f,0x33,0x39,0x29,0x29,0x29,0x3b,0x0a,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x7d,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,0x37,
+    0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,
+    0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x6f,0x75,0x74,0x2e,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,
+    0x6f,0x72,0x20,0x3d,0x20,0x63,0x6f,0x6c,0x6f,0x72,0x3b,0x0a,0x20,0x20,0x20,0x20,
+    0x7d,0x0a,0x20,0x20,0x20,0x20,0x72,0x65,0x74,0x75,0x72,0x6e,0x20,0x6f,0x75,0x74,
+    0x3b,0x0a,0x7d,0x0a,0x0a,0x00,
+};
+/*
+    #include <metal_stdlib>
+    #include <simd/simd.h>
+    
+    using namespace metal;
+    
+    struct vs_params
+    {
+        float4x4 mvp;
+    };
+    
+    struct main0_out
+    {
+        float2 uv [[user(locn0)]];
+        float4 gl_Position [[position]];
+    };
+    
+    struct main0_in
+    {
+        float2 position [[attribute(0)]];
+        float2 texcoord0 [[attribute(1)]];
+    };
+    
+    #line 16 "../src/sokol/shader.glsl"
+    vertex main0_out main0(main0_in in [[stage_in]], constant vs_params& _21 [[buffer(0)]])
+    {
+        main0_out out = {};
+    #line 16 "../src/sokol/shader.glsl"
+        out.gl_Position = _21.mvp * float4(in.position.x, in.position.y, 0.0, 1.0);
+    #line 17 "../src/sokol/shader.glsl"
+        out.uv = in.texcoord0;
+        return out;
+    }
+    
+*/
+static const char vs_source_metal_sim[652] = {
+    0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,0x20,0x3c,0x6d,0x65,0x74,0x61,0x6c,0x5f,
+    0x73,0x74,0x64,0x6c,0x69,0x62,0x3e,0x0a,0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,
+    0x20,0x3c,0x73,0x69,0x6d,0x64,0x2f,0x73,0x69,0x6d,0x64,0x2e,0x68,0x3e,0x0a,0x0a,
+    0x75,0x73,0x69,0x6e,0x67,0x20,0x6e,0x61,0x6d,0x65,0x73,0x70,0x61,0x63,0x65,0x20,
+    0x6d,0x65,0x74,0x61,0x6c,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x76,
+    0x73,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,
+    0x6c,0x6f,0x61,0x74,0x34,0x78,0x34,0x20,0x6d,0x76,0x70,0x3b,0x0a,0x7d,0x3b,0x0a,
+    0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,
+    0x74,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x75,
+    0x76,0x20,0x5b,0x5b,0x75,0x73,0x65,0x72,0x28,0x6c,0x6f,0x63,0x6e,0x30,0x29,0x5d,
+    0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x67,0x6c,
+    0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x5b,0x5b,0x70,0x6f,0x73,0x69,
+    0x74,0x69,0x6f,0x6e,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,
+    0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x0a,0x7b,0x0a,0x20,0x20,
+    0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,
+    0x6e,0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28,0x30,0x29,
+    0x5d,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x74,
+    0x65,0x78,0x63,0x6f,0x6f,0x72,0x64,0x30,0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69,
+    0x62,0x75,0x74,0x65,0x28,0x31,0x29,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x23,
+    0x6c,0x69,0x6e,0x65,0x20,0x31,0x36,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,
+    0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,
+    0x6c,0x22,0x0a,0x76,0x65,0x72,0x74,0x65,0x78,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,
+    0x6f,0x75,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x28,0x6d,0x61,0x69,0x6e,0x30,0x5f,
+    0x69,0x6e,0x20,0x69,0x6e,0x20,0x5b,0x5b,0x73,0x74,0x61,0x67,0x65,0x5f,0x69,0x6e,
+    0x5d,0x5d,0x2c,0x20,0x63,0x6f,0x6e,0x73,0x74,0x61,0x6e,0x74,0x20,0x76,0x73,0x5f,
+    0x70,0x61,0x72,0x61,0x6d,0x73,0x26,0x20,0x5f,0x32,0x31,0x20,0x5b,0x5b,0x62,0x75,
+    0x66,0x66,0x65,0x72,0x28,0x30,0x29,0x5d,0x5d,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,
+    0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6f,0x75,0x74,0x20,0x3d,
+    0x20,0x7b,0x7d,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x36,0x20,0x22,0x2e,
+    0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,
+    0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x6f,0x75,0x74,
+    0x2e,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x5f,
+    0x32,0x31,0x2e,0x6d,0x76,0x70,0x20,0x2a,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x28,
+    0x69,0x6e,0x2e,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x2e,0x78,0x2c,0x20,0x69,
+    0x6e,0x2e,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x2e,0x79,0x2c,0x20,0x30,0x2e,
+    0x30,0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,
+    0x37,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,
+    0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,
+    0x20,0x6f,0x75,0x74,0x2e,0x75,0x76,0x20,0x3d,0x20,0x69,0x6e,0x2e,0x74,0x65,0x78,
+    0x63,0x6f,0x6f,0x72,0x64,0x30,0x3b,0x0a,0x20,0x20,0x20,0x20,0x72,0x65,0x74,0x75,
+    0x72,0x6e,0x20,0x6f,0x75,0x74,0x3b,0x0a,0x7d,0x0a,0x0a,0x00,
+};
+/*
+    #include <metal_stdlib>
+    #include <simd/simd.h>
+    
+    using namespace metal;
+    
+    struct main0_out
+    {
+        float4 frag_color [[color(0)]];
+    };
+    
+    struct main0_in
+    {
+        float2 uv [[user(locn0)]];
+    };
+    
+    #line 12 "../src/sokol/shader.glsl"
+    fragment main0_out main0(main0_in in [[stage_in]], texture2d<float> tex [[texture(0)]], sampler texSmplr [[sampler(0)]])
+    {
+        main0_out out = {};
+    #line 12 "../src/sokol/shader.glsl"
+        out.frag_color = tex.sample(texSmplr, in.uv);
+        return out;
+    }
+    
+*/
+static const char fs_source_metal_sim[473] = {
+    0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,0x20,0x3c,0x6d,0x65,0x74,0x61,0x6c,0x5f,
+    0x73,0x74,0x64,0x6c,0x69,0x62,0x3e,0x0a,0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,
+    0x20,0x3c,0x73,0x69,0x6d,0x64,0x2f,0x73,0x69,0x6d,0x64,0x2e,0x68,0x3e,0x0a,0x0a,
+    0x75,0x73,0x69,0x6e,0x67,0x20,0x6e,0x61,0x6d,0x65,0x73,0x70,0x61,0x63,0x65,0x20,
+    0x6d,0x65,0x74,0x61,0x6c,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x6d,
+    0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,
+    0x6c,0x6f,0x61,0x74,0x34,0x20,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,
+    0x20,0x5b,0x5b,0x63,0x6f,0x6c,0x6f,0x72,0x28,0x30,0x29,0x5d,0x5d,0x3b,0x0a,0x7d,
+    0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,
+    0x69,0x6e,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,
+    0x75,0x76,0x20,0x5b,0x5b,0x75,0x73,0x65,0x72,0x28,0x6c,0x6f,0x63,0x6e,0x30,0x29,
+    0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x32,
+    0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,
+    0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x66,0x72,0x61,0x67,
+    0x6d,0x65,0x6e,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6d,
+    0x61,0x69,0x6e,0x30,0x28,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x20,0x69,0x6e,
+    0x20,0x5b,0x5b,0x73,0x74,0x61,0x67,0x65,0x5f,0x69,0x6e,0x5d,0x5d,0x2c,0x20,0x74,
+    0x65,0x78,0x74,0x75,0x72,0x65,0x32,0x64,0x3c,0x66,0x6c,0x6f,0x61,0x74,0x3e,0x20,
+    0x74,0x65,0x78,0x20,0x5b,0x5b,0x74,0x65,0x78,0x74,0x75,0x72,0x65,0x28,0x30,0x29,
+    0x5d,0x5d,0x2c,0x20,0x73,0x61,0x6d,0x70,0x6c,0x65,0x72,0x20,0x74,0x65,0x78,0x53,
+    0x6d,0x70,0x6c,0x72,0x20,0x5b,0x5b,0x73,0x61,0x6d,0x70,0x6c,0x65,0x72,0x28,0x30,
+    0x29,0x5d,0x5d,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x6d,0x61,0x69,0x6e,0x30,
+    0x5f,0x6f,0x75,0x74,0x20,0x6f,0x75,0x74,0x20,0x3d,0x20,0x7b,0x7d,0x3b,0x0a,0x23,
+    0x6c,0x69,0x6e,0x65,0x20,0x31,0x32,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,
+    0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,
+    0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x6f,0x75,0x74,0x2e,0x66,0x72,0x61,0x67,0x5f,
+    0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,0x20,0x74,0x65,0x78,0x2e,0x73,0x61,0x6d,0x70,
+    0x6c,0x65,0x28,0x74,0x65,0x78,0x53,0x6d,0x70,0x6c,0x72,0x2c,0x20,0x69,0x6e,0x2e,
+    0x75,0x76,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x72,0x65,0x74,0x75,0x72,0x6e,0x20,
+    0x6f,0x75,0x74,0x3b,0x0a,0x7d,0x0a,0x0a,0x00,
+};
+/*
+    #include <metal_stdlib>
+    #include <simd/simd.h>
+    
+    using namespace metal;
+    
+    struct vs_path_params
+    {
+        float4x4 mvp;
+        int fillType;
+        float2 gradientStart;
+        float2 gradientEnd;
+    };
+    
+    struct main0_out
+    {
+        float2 gradient_uv [[user(locn0)]];
+        float4 gl_Position [[position]];
+    };
+    
+    struct main0_in
+    {
+        float2 position [[attribute(0)]];
+    };
+    
+    #line 17 "../src/sokol/shader.glsl"
+    vertex main0_out main0(main0_in in [[stage_in]], constant vs_path_params& _22 [[buffer(0)]])
+    {
+        main0_out out = {};
+    #line 17 "../src/sokol/shader.glsl"
+        out.gl_Position = _22.mvp * float4(in.position, 0.0, 1.0);
+    #line 19 "../src/sokol/shader.glsl"
+        if (_22.fillType == 1)
+        {
+    #line 21 "../src/sokol/shader.glsl"
+            float2 _54 = _22.gradientEnd - _22.gradientStart;
+    #line 22 "../src/sokol/shader.glsl"
+            float _59 = _54.x;
+            float _64 = _54.y;
+    #line 23 "../src/sokol/shader.glsl"
+            out.gradient_uv.x = dot(in.position - _22.gradientStart, _54) / ((_59 * _59) + (_64 * _64));
+        }
+        else
+        {
+    #line 25 "../src/sokol/shader.glsl"
+            if (_22.fillType == 2)
+            {
+    #line 27 "../src/sokol/shader.glsl"
+                out.gradient_uv = (in.position - _22.gradientStart) / float2(distance(_22.gradientStart, _22.gradientEnd));
+            }
+        }
+        return out;
+    }
+    
+*/
+static const char vs_path_source_metal_sim[1280] = {
+    0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,0x20,0x3c,0x6d,0x65,0x74,0x61,0x6c,0x5f,
+    0x73,0x74,0x64,0x6c,0x69,0x62,0x3e,0x0a,0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,
+    0x20,0x3c,0x73,0x69,0x6d,0x64,0x2f,0x73,0x69,0x6d,0x64,0x2e,0x68,0x3e,0x0a,0x0a,
+    0x75,0x73,0x69,0x6e,0x67,0x20,0x6e,0x61,0x6d,0x65,0x73,0x70,0x61,0x63,0x65,0x20,
+    0x6d,0x65,0x74,0x61,0x6c,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x76,
+    0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,0x70,0x61,0x72,0x61,0x6d,0x73,0x0a,0x7b,0x0a,
+    0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x78,0x34,0x20,0x6d,0x76,0x70,
+    0x3b,0x0a,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x66,0x69,0x6c,0x6c,0x54,0x79,
+    0x70,0x65,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x67,
+    0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x3b,0x0a,0x20,0x20,
+    0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,
+    0x74,0x45,0x6e,0x64,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,
+    0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x0a,0x7b,0x0a,0x20,0x20,0x20,
+    0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,
+    0x5f,0x75,0x76,0x20,0x5b,0x5b,0x75,0x73,0x65,0x72,0x28,0x6c,0x6f,0x63,0x6e,0x30,
+    0x29,0x5d,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,
+    0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x5b,0x5b,0x70,0x6f,
+    0x73,0x69,0x74,0x69,0x6f,0x6e,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74,
+    0x72,0x75,0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x0a,0x7b,0x0a,
+    0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x70,0x6f,0x73,0x69,0x74,
+    0x69,0x6f,0x6e,0x20,0x5b,0x5b,0x61,0x74,0x74,0x72,0x69,0x62,0x75,0x74,0x65,0x28,
+    0x30,0x29,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,
+    0x31,0x37,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,
+    0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x76,0x65,
+    0x72,0x74,0x65,0x78,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6d,
+    0x61,0x69,0x6e,0x30,0x28,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,0x6e,0x20,0x69,0x6e,
+    0x20,0x5b,0x5b,0x73,0x74,0x61,0x67,0x65,0x5f,0x69,0x6e,0x5d,0x5d,0x2c,0x20,0x63,
+    0x6f,0x6e,0x73,0x74,0x61,0x6e,0x74,0x20,0x76,0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,
+    0x70,0x61,0x72,0x61,0x6d,0x73,0x26,0x20,0x5f,0x32,0x32,0x20,0x5b,0x5b,0x62,0x75,
+    0x66,0x66,0x65,0x72,0x28,0x30,0x29,0x5d,0x5d,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,
+    0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6f,0x75,0x74,0x20,0x3d,
+    0x20,0x7b,0x7d,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x37,0x20,0x22,0x2e,
+    0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,
+    0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x6f,0x75,0x74,
+    0x2e,0x67,0x6c,0x5f,0x50,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x3d,0x20,0x5f,
+    0x32,0x32,0x2e,0x6d,0x76,0x70,0x20,0x2a,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x28,
+    0x69,0x6e,0x2e,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x2c,0x20,0x30,0x2e,0x30,
+    0x2c,0x20,0x31,0x2e,0x30,0x29,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x39,
+    0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,
+    0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,
+    0x69,0x66,0x20,0x28,0x5f,0x32,0x32,0x2e,0x66,0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,
+    0x20,0x3d,0x3d,0x20,0x31,0x29,0x0a,0x20,0x20,0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,
+    0x6e,0x65,0x20,0x32,0x31,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,
+    0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,
+    0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,
+    0x5f,0x35,0x34,0x20,0x3d,0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,
+    0x6e,0x74,0x45,0x6e,0x64,0x20,0x2d,0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,
+    0x69,0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,
+    0x20,0x32,0x32,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,
+    0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x35,0x39,
+    0x20,0x3d,0x20,0x5f,0x35,0x34,0x2e,0x78,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x36,0x34,0x20,0x3d,0x20,0x5f,0x35,
+    0x34,0x2e,0x79,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x33,0x20,0x22,0x2e,
+    0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,
+    0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x6f,0x75,0x74,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,
+    0x2e,0x78,0x20,0x3d,0x20,0x64,0x6f,0x74,0x28,0x69,0x6e,0x2e,0x70,0x6f,0x73,0x69,
+    0x74,0x69,0x6f,0x6e,0x20,0x2d,0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,0x69,
+    0x65,0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x2c,0x20,0x5f,0x35,0x34,0x29,0x20,0x2f,
+    0x20,0x28,0x28,0x5f,0x35,0x39,0x20,0x2a,0x20,0x5f,0x35,0x39,0x29,0x20,0x2b,0x20,
+    0x28,0x5f,0x36,0x34,0x20,0x2a,0x20,0x5f,0x36,0x34,0x29,0x29,0x3b,0x0a,0x20,0x20,
+    0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x65,0x6c,0x73,0x65,0x0a,0x20,0x20,0x20,
+    0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x35,0x20,0x22,0x2e,0x2e,0x2f,
+    0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,
+    0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x69,
+    0x66,0x20,0x28,0x5f,0x32,0x32,0x2e,0x66,0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,0x20,
+    0x3d,0x3d,0x20,0x32,0x29,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,
+    0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x37,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,
+    0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,
+    0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x6f,0x75,0x74,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x20,
+    0x3d,0x20,0x28,0x69,0x6e,0x2e,0x70,0x6f,0x73,0x69,0x74,0x69,0x6f,0x6e,0x20,0x2d,
+    0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x53,0x74,0x61,
+    0x72,0x74,0x29,0x20,0x2f,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x28,0x64,0x69,0x73,
+    0x74,0x61,0x6e,0x63,0x65,0x28,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,
+    0x6e,0x74,0x53,0x74,0x61,0x72,0x74,0x2c,0x20,0x5f,0x32,0x32,0x2e,0x67,0x72,0x61,
+    0x64,0x69,0x65,0x6e,0x74,0x45,0x6e,0x64,0x29,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,
+    0x72,0x65,0x74,0x75,0x72,0x6e,0x20,0x6f,0x75,0x74,0x3b,0x0a,0x7d,0x0a,0x0a,0x00,
+
+};
+/*
+    #include <metal_stdlib>
+    #include <simd/simd.h>
+    
+    using namespace metal;
+    
+    struct fs_path_uniforms
+    {
+        int fillType;
+        float4 colors[16];
+        float4 stops[4];
+        int stopCount;
+    };
+    
+    struct main0_out
+    {
+        float4 frag_color [[color(0)]];
+    };
+    
+    struct main0_in
+    {
+        float2 gradient_uv [[user(locn0)]];
+    };
+    
+    #line 18 "../src/sokol/shader.glsl"
+    fragment main0_out main0(main0_in in [[stage_in]], constant fs_path_uniforms& _18 [[buffer(0)]])
+    {
+        main0_out out = {};
+    #line 18 "../src/sokol/shader.glsl"
+        if (_18.fillType == 0)
+        {
+    #line 20 "../src/sokol/shader.glsl"
+            out.frag_color = _18.colors[0];
+        }
+        else
+        {
+    #line 23 "../src/sokol/shader.glsl"
+            float _39;
+            if (_18.fillType == 1)
+            {
+                _39 = in.gradient_uv.x;
+            }
+            else
+            {
+                _39 = length(in.gradient_uv);
+            }
+    #line 24 "../src/sokol/shader.glsl"
+    #line 25 "../src/sokol/shader.glsl"
+            float4 color = mix(_18.colors[0], _18.colors[1], float4(smoothstep(_18.stops[0].x, _18.stops[0].y, _39)));
+    #line 26 "../src/sokol/shader.glsl"
+            for (int i = 1; i < 15; i++)
+            {
+    #line 28 "../src/sokol/shader.glsl"
+                if (i >= (_18.stopCount - 1))
+                {
+    #line 30 "../src/sokol/shader.glsl"
+                    break;
+                }
+    #line 33 "../src/sokol/shader.glsl"
+    #line 35 "../src/sokol/shader.glsl"
+    #line 34 "../src/sokol/shader.glsl"
+                int _91 = i + 1;
+    #line 35 "../src/sokol/shader.glsl"
+                color = mix(color, _18.colors[_91], float4(smoothstep(_18.stops[i / 4][i % 4], _18.stops[_91 / 4][_91 % 4], _39)));
+            }
+    #line 37 "../src/sokol/shader.glsl"
+            out.frag_color = color;
+        }
+        return out;
+    }
+    
+*/
+static const char fs_path_source_metal_sim[1686] = {
+    0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,0x20,0x3c,0x6d,0x65,0x74,0x61,0x6c,0x5f,
+    0x73,0x74,0x64,0x6c,0x69,0x62,0x3e,0x0a,0x23,0x69,0x6e,0x63,0x6c,0x75,0x64,0x65,
+    0x20,0x3c,0x73,0x69,0x6d,0x64,0x2f,0x73,0x69,0x6d,0x64,0x2e,0x68,0x3e,0x0a,0x0a,
+    0x75,0x73,0x69,0x6e,0x67,0x20,0x6e,0x61,0x6d,0x65,0x73,0x70,0x61,0x63,0x65,0x20,
+    0x6d,0x65,0x74,0x61,0x6c,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x66,
+    0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,0x75,0x6e,0x69,0x66,0x6f,0x72,0x6d,0x73,0x0a,
+    0x7b,0x0a,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x66,0x69,0x6c,0x6c,0x54,0x79,
+    0x70,0x65,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x34,0x20,0x63,
+    0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x31,0x36,0x5d,0x3b,0x0a,0x20,0x20,0x20,0x20,0x66,
+    0x6c,0x6f,0x61,0x74,0x34,0x20,0x73,0x74,0x6f,0x70,0x73,0x5b,0x34,0x5d,0x3b,0x0a,
+    0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x73,0x74,0x6f,0x70,0x43,0x6f,0x75,0x6e,
+    0x74,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x6d,0x61,
+    0x69,0x6e,0x30,0x5f,0x6f,0x75,0x74,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,
+    0x6f,0x61,0x74,0x34,0x20,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x20,
+    0x5b,0x5b,0x63,0x6f,0x6c,0x6f,0x72,0x28,0x30,0x29,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,
+    0x0a,0x0a,0x73,0x74,0x72,0x75,0x63,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x5f,0x69,
+    0x6e,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x66,0x6c,0x6f,0x61,0x74,0x32,0x20,0x67,
+    0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,0x76,0x20,0x5b,0x5b,0x75,0x73,0x65,
+    0x72,0x28,0x6c,0x6f,0x63,0x6e,0x30,0x29,0x5d,0x5d,0x3b,0x0a,0x7d,0x3b,0x0a,0x0a,
+    0x23,0x6c,0x69,0x6e,0x65,0x20,0x31,0x38,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,
+    0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,
+    0x73,0x6c,0x22,0x0a,0x66,0x72,0x61,0x67,0x6d,0x65,0x6e,0x74,0x20,0x6d,0x61,0x69,
+    0x6e,0x30,0x5f,0x6f,0x75,0x74,0x20,0x6d,0x61,0x69,0x6e,0x30,0x28,0x6d,0x61,0x69,
+    0x6e,0x30,0x5f,0x69,0x6e,0x20,0x69,0x6e,0x20,0x5b,0x5b,0x73,0x74,0x61,0x67,0x65,
+    0x5f,0x69,0x6e,0x5d,0x5d,0x2c,0x20,0x63,0x6f,0x6e,0x73,0x74,0x61,0x6e,0x74,0x20,
+    0x66,0x73,0x5f,0x70,0x61,0x74,0x68,0x5f,0x75,0x6e,0x69,0x66,0x6f,0x72,0x6d,0x73,
+    0x26,0x20,0x5f,0x31,0x38,0x20,0x5b,0x5b,0x62,0x75,0x66,0x66,0x65,0x72,0x28,0x30,
+    0x29,0x5d,0x5d,0x29,0x0a,0x7b,0x0a,0x20,0x20,0x20,0x20,0x6d,0x61,0x69,0x6e,0x30,
+    0x5f,0x6f,0x75,0x74,0x20,0x6f,0x75,0x74,0x20,0x3d,0x20,0x7b,0x7d,0x3b,0x0a,0x23,
+    0x6c,0x69,0x6e,0x65,0x20,0x31,0x38,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,
+    0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,
+    0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,0x5f,0x31,0x38,0x2e,0x66,
+    0x69,0x6c,0x6c,0x54,0x79,0x70,0x65,0x20,0x3d,0x3d,0x20,0x30,0x29,0x0a,0x20,0x20,
+    0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x30,0x20,0x22,0x2e,0x2e,
+    0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,
+    0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x6f,0x75,0x74,0x2e,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,
+    0x20,0x5f,0x31,0x38,0x2e,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x30,0x5d,0x3b,0x0a,
+    0x20,0x20,0x20,0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x65,0x6c,0x73,0x65,0x0a,0x20,
+    0x20,0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x33,0x20,0x22,0x2e,
+    0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,
+    0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x66,0x6c,0x6f,0x61,0x74,0x20,0x5f,0x33,0x39,0x3b,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,0x5f,0x31,0x38,0x2e,0x66,0x69,0x6c,0x6c,
+    0x54,0x79,0x70,0x65,0x20,0x3d,0x3d,0x20,0x31,0x29,0x0a,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x5f,0x33,0x39,0x20,0x3d,0x20,0x69,0x6e,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,
+    0x6e,0x74,0x5f,0x75,0x76,0x2e,0x78,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x7d,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x65,0x6c,0x73,0x65,0x0a,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x5f,0x33,0x39,0x20,0x3d,0x20,0x6c,0x65,0x6e,0x67,
+    0x74,0x68,0x28,0x69,0x6e,0x2e,0x67,0x72,0x61,0x64,0x69,0x65,0x6e,0x74,0x5f,0x75,
+    0x76,0x29,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7d,0x0a,0x23,0x6c,
+    0x69,0x6e,0x65,0x20,0x32,0x34,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,
+    0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,
+    0x22,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,0x35,0x20,0x22,0x2e,0x2e,0x2f,0x73,
+    0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,
+    0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x66,0x6c,
+    0x6f,0x61,0x74,0x34,0x20,0x63,0x6f,0x6c,0x6f,0x72,0x20,0x3d,0x20,0x6d,0x69,0x78,
+    0x28,0x5f,0x31,0x38,0x2e,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x30,0x5d,0x2c,0x20,
+    0x5f,0x31,0x38,0x2e,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x31,0x5d,0x2c,0x20,0x66,
+    0x6c,0x6f,0x61,0x74,0x34,0x28,0x73,0x6d,0x6f,0x6f,0x74,0x68,0x73,0x74,0x65,0x70,
+    0x28,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x73,0x5b,0x30,0x5d,0x2e,0x78,0x2c,
+    0x20,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x73,0x5b,0x30,0x5d,0x2e,0x79,0x2c,
+    0x20,0x5f,0x33,0x39,0x29,0x29,0x29,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x32,
+    0x36,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,
+    0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x66,0x6f,0x72,0x20,0x28,0x69,0x6e,0x74,0x20,0x69,0x20,
+    0x3d,0x20,0x31,0x3b,0x20,0x69,0x20,0x3c,0x20,0x31,0x35,0x3b,0x20,0x69,0x2b,0x2b,
+    0x29,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,
+    0x65,0x20,0x32,0x38,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,
+    0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x69,0x66,0x20,0x28,
+    0x69,0x20,0x3e,0x3d,0x20,0x28,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x43,0x6f,
+    0x75,0x6e,0x74,0x20,0x2d,0x20,0x31,0x29,0x29,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x7b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,0x30,
+    0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,
+    0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x62,0x72,0x65,0x61,
+    0x6b,0x3b,0x0a,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x7d,
+    0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,0x33,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,
+    0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,
+    0x6c,0x73,0x6c,0x22,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,0x35,0x20,0x22,0x2e,
+    0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,0x68,0x61,0x64,
+    0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,
+    0x34,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,
+    0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x69,0x6e,0x74,0x20,0x5f,0x39,0x31,
+    0x20,0x3d,0x20,0x69,0x20,0x2b,0x20,0x31,0x3b,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,
+    0x33,0x35,0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,
+    0x2f,0x73,0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x20,0x63,0x6f,0x6c,0x6f,0x72,0x20,
+    0x3d,0x20,0x6d,0x69,0x78,0x28,0x63,0x6f,0x6c,0x6f,0x72,0x2c,0x20,0x5f,0x31,0x38,
+    0x2e,0x63,0x6f,0x6c,0x6f,0x72,0x73,0x5b,0x5f,0x39,0x31,0x5d,0x2c,0x20,0x66,0x6c,
+    0x6f,0x61,0x74,0x34,0x28,0x73,0x6d,0x6f,0x6f,0x74,0x68,0x73,0x74,0x65,0x70,0x28,
+    0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,0x70,0x73,0x5b,0x69,0x20,0x2f,0x20,0x34,0x5d,
+    0x5b,0x69,0x20,0x25,0x20,0x34,0x5d,0x2c,0x20,0x5f,0x31,0x38,0x2e,0x73,0x74,0x6f,
+    0x70,0x73,0x5b,0x5f,0x39,0x31,0x20,0x2f,0x20,0x34,0x5d,0x5b,0x5f,0x39,0x31,0x20,
+    0x25,0x20,0x34,0x5d,0x2c,0x20,0x5f,0x33,0x39,0x29,0x29,0x29,0x3b,0x0a,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x20,0x20,0x7d,0x0a,0x23,0x6c,0x69,0x6e,0x65,0x20,0x33,0x37,
+    0x20,0x22,0x2e,0x2e,0x2f,0x73,0x72,0x63,0x2f,0x73,0x6f,0x6b,0x6f,0x6c,0x2f,0x73,
+    0x68,0x61,0x64,0x65,0x72,0x2e,0x67,0x6c,0x73,0x6c,0x22,0x0a,0x20,0x20,0x20,0x20,
+    0x20,0x20,0x20,0x20,0x6f,0x75,0x74,0x2e,0x66,0x72,0x61,0x67,0x5f,0x63,0x6f,0x6c,
+    0x6f,0x72,0x20,0x3d,0x20,0x63,0x6f,0x6c,0x6f,0x72,0x3b,0x0a,0x20,0x20,0x20,0x20,
+    0x7d,0x0a,0x20,0x20,0x20,0x20,0x72,0x65,0x74,0x75,0x72,0x6e,0x20,0x6f,0x75,0x74,
+    0x3b,0x0a,0x7d,0x0a,0x0a,0x00,
+};
+#if !defined(SOKOL_GFX_INCLUDED)
+  #error "Please include sokol_gfx.h before shader.h"
+#endif
+static inline const sg_shader_desc* rive_tess_shader_desc(sg_backend backend) {
+  if (backend == SG_BACKEND_GLCORE33) {
+    static sg_shader_desc desc;
+    static bool valid;
+    if (!valid) {
+      valid = true;
+      desc.attrs[0].name = "position";
+      desc.attrs[1].name = "texcoord0";
+      desc.vs.source = vs_source_glsl330;
+      desc.vs.entry = "main";
+      desc.vs.uniform_blocks[0].size = 64;
+      desc.vs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.vs.uniform_blocks[0].uniforms[0].name = "vs_params";
+      desc.vs.uniform_blocks[0].uniforms[0].type = SG_UNIFORMTYPE_FLOAT4;
+      desc.vs.uniform_blocks[0].uniforms[0].array_count = 4;
+      desc.fs.source = fs_source_glsl330;
+      desc.fs.entry = "main";
+      desc.fs.images[0].name = "tex";
+      desc.fs.images[0].image_type = SG_IMAGETYPE_2D;
+      desc.fs.images[0].sampler_type = SG_SAMPLERTYPE_FLOAT;
+      desc.label = "rive_tess_shader";
+    }
+    return &desc;
+  }
+  if (backend == SG_BACKEND_GLES2) {
+    static sg_shader_desc desc;
+    static bool valid;
+    if (!valid) {
+      valid = true;
+      desc.attrs[0].name = "position";
+      desc.attrs[1].name = "texcoord0";
+      desc.vs.source = vs_source_glsl100;
+      desc.vs.entry = "main";
+      desc.vs.uniform_blocks[0].size = 64;
+      desc.vs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.vs.uniform_blocks[0].uniforms[0].name = "vs_params";
+      desc.vs.uniform_blocks[0].uniforms[0].type = SG_UNIFORMTYPE_FLOAT4;
+      desc.vs.uniform_blocks[0].uniforms[0].array_count = 4;
+      desc.fs.source = fs_source_glsl100;
+      desc.fs.entry = "main";
+      desc.fs.images[0].name = "tex";
+      desc.fs.images[0].image_type = SG_IMAGETYPE_2D;
+      desc.fs.images[0].sampler_type = SG_SAMPLERTYPE_FLOAT;
+      desc.label = "rive_tess_shader";
+    }
+    return &desc;
+  }
+  if (backend == SG_BACKEND_GLES3) {
+    static sg_shader_desc desc;
+    static bool valid;
+    if (!valid) {
+      valid = true;
+      desc.attrs[0].name = "position";
+      desc.attrs[1].name = "texcoord0";
+      desc.vs.source = vs_source_glsl300es;
+      desc.vs.entry = "main";
+      desc.vs.uniform_blocks[0].size = 64;
+      desc.vs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.vs.uniform_blocks[0].uniforms[0].name = "vs_params";
+      desc.vs.uniform_blocks[0].uniforms[0].type = SG_UNIFORMTYPE_FLOAT4;
+      desc.vs.uniform_blocks[0].uniforms[0].array_count = 4;
+      desc.fs.source = fs_source_glsl300es;
+      desc.fs.entry = "main";
+      desc.fs.images[0].name = "tex";
+      desc.fs.images[0].image_type = SG_IMAGETYPE_2D;
+      desc.fs.images[0].sampler_type = SG_SAMPLERTYPE_FLOAT;
+      desc.label = "rive_tess_shader";
+    }
+    return &desc;
+  }
+  if (backend == SG_BACKEND_D3D11) {
+    static sg_shader_desc desc;
+    static bool valid;
+    if (!valid) {
+      valid = true;
+      desc.attrs[0].sem_name = "TEXCOORD";
+      desc.attrs[0].sem_index = 0;
+      desc.attrs[1].sem_name = "TEXCOORD";
+      desc.attrs[1].sem_index = 1;
+      desc.vs.source = vs_source_hlsl5;
+      desc.vs.d3d11_target = "vs_5_0";
+      desc.vs.entry = "main";
+      desc.vs.uniform_blocks[0].size = 64;
+      desc.vs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.fs.source = fs_source_hlsl5;
+      desc.fs.d3d11_target = "ps_5_0";
+      desc.fs.entry = "main";
+      desc.fs.images[0].name = "tex";
+      desc.fs.images[0].image_type = SG_IMAGETYPE_2D;
+      desc.fs.images[0].sampler_type = SG_SAMPLERTYPE_FLOAT;
+      desc.label = "rive_tess_shader";
+    }
+    return &desc;
+  }
+  if (backend == SG_BACKEND_METAL_MACOS) {
+    static sg_shader_desc desc;
+    static bool valid;
+    if (!valid) {
+      valid = true;
+      desc.vs.source = vs_source_metal_macos;
+      desc.vs.entry = "main0";
+      desc.vs.uniform_blocks[0].size = 64;
+      desc.vs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.fs.source = fs_source_metal_macos;
+      desc.fs.entry = "main0";
+      desc.fs.images[0].name = "tex";
+      desc.fs.images[0].image_type = SG_IMAGETYPE_2D;
+      desc.fs.images[0].sampler_type = SG_SAMPLERTYPE_FLOAT;
+      desc.label = "rive_tess_shader";
+    }
+    return &desc;
+  }
+  if (backend == SG_BACKEND_METAL_IOS) {
+    static sg_shader_desc desc;
+    static bool valid;
+    if (!valid) {
+      valid = true;
+      desc.vs.source = vs_source_metal_ios;
+      desc.vs.entry = "main0";
+      desc.vs.uniform_blocks[0].size = 64;
+      desc.vs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.fs.source = fs_source_metal_ios;
+      desc.fs.entry = "main0";
+      desc.fs.images[0].name = "tex";
+      desc.fs.images[0].image_type = SG_IMAGETYPE_2D;
+      desc.fs.images[0].sampler_type = SG_SAMPLERTYPE_FLOAT;
+      desc.label = "rive_tess_shader";
+    }
+    return &desc;
+  }
+  if (backend == SG_BACKEND_METAL_SIMULATOR) {
+    static sg_shader_desc desc;
+    static bool valid;
+    if (!valid) {
+      valid = true;
+      desc.vs.source = vs_source_metal_sim;
+      desc.vs.entry = "main0";
+      desc.vs.uniform_blocks[0].size = 64;
+      desc.vs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.fs.source = fs_source_metal_sim;
+      desc.fs.entry = "main0";
+      desc.fs.images[0].name = "tex";
+      desc.fs.images[0].image_type = SG_IMAGETYPE_2D;
+      desc.fs.images[0].sampler_type = SG_SAMPLERTYPE_FLOAT;
+      desc.label = "rive_tess_shader";
+    }
+    return &desc;
+  }
+  return 0;
+}
+static inline const sg_shader_desc* rive_tess_path_shader_desc(sg_backend backend) {
+  if (backend == SG_BACKEND_GLCORE33) {
+    static sg_shader_desc desc;
+    static bool valid;
+    if (!valid) {
+      valid = true;
+      desc.attrs[0].name = "position";
+      desc.vs.source = vs_path_source_glsl330;
+      desc.vs.entry = "main";
+      desc.vs.uniform_blocks[0].size = 96;
+      desc.vs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.vs.uniform_blocks[0].uniforms[0].name = "_22.mvp";
+      desc.vs.uniform_blocks[0].uniforms[0].type = SG_UNIFORMTYPE_MAT4;
+      desc.vs.uniform_blocks[0].uniforms[0].array_count = 1;
+      desc.vs.uniform_blocks[0].uniforms[1].name = "_22.fillType";
+      desc.vs.uniform_blocks[0].uniforms[1].type = SG_UNIFORMTYPE_INT;
+      desc.vs.uniform_blocks[0].uniforms[1].array_count = 1;
+      desc.vs.uniform_blocks[0].uniforms[2].name = "_22.gradientStart";
+      desc.vs.uniform_blocks[0].uniforms[2].type = SG_UNIFORMTYPE_FLOAT2;
+      desc.vs.uniform_blocks[0].uniforms[2].array_count = 1;
+      desc.vs.uniform_blocks[0].uniforms[3].name = "_22.gradientEnd";
+      desc.vs.uniform_blocks[0].uniforms[3].type = SG_UNIFORMTYPE_FLOAT2;
+      desc.vs.uniform_blocks[0].uniforms[3].array_count = 1;
+      desc.fs.source = fs_path_source_glsl330;
+      desc.fs.entry = "main";
+      desc.fs.uniform_blocks[0].size = 352;
+      desc.fs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.fs.uniform_blocks[0].uniforms[0].name = "_18.fillType";
+      desc.fs.uniform_blocks[0].uniforms[0].type = SG_UNIFORMTYPE_INT;
+      desc.fs.uniform_blocks[0].uniforms[0].array_count = 1;
+      desc.fs.uniform_blocks[0].uniforms[1].name = "_18.colors";
+      desc.fs.uniform_blocks[0].uniforms[1].type = SG_UNIFORMTYPE_FLOAT4;
+      desc.fs.uniform_blocks[0].uniforms[1].array_count = 16;
+      desc.fs.uniform_blocks[0].uniforms[2].name = "_18.stops";
+      desc.fs.uniform_blocks[0].uniforms[2].type = SG_UNIFORMTYPE_FLOAT4;
+      desc.fs.uniform_blocks[0].uniforms[2].array_count = 4;
+      desc.fs.uniform_blocks[0].uniforms[3].name = "_18.stopCount";
+      desc.fs.uniform_blocks[0].uniforms[3].type = SG_UNIFORMTYPE_INT;
+      desc.fs.uniform_blocks[0].uniforms[3].array_count = 1;
+      desc.label = "rive_tess_path_shader";
+    }
+    return &desc;
+  }
+  if (backend == SG_BACKEND_GLES2) {
+    static sg_shader_desc desc;
+    static bool valid;
+    if (!valid) {
+      valid = true;
+      desc.attrs[0].name = "position";
+      desc.vs.source = vs_path_source_glsl100;
+      desc.vs.entry = "main";
+      desc.vs.uniform_blocks[0].size = 96;
+      desc.vs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.vs.uniform_blocks[0].uniforms[0].name = "_22.mvp";
+      desc.vs.uniform_blocks[0].uniforms[0].type = SG_UNIFORMTYPE_MAT4;
+      desc.vs.uniform_blocks[0].uniforms[0].array_count = 1;
+      desc.vs.uniform_blocks[0].uniforms[1].name = "_22.fillType";
+      desc.vs.uniform_blocks[0].uniforms[1].type = SG_UNIFORMTYPE_INT;
+      desc.vs.uniform_blocks[0].uniforms[1].array_count = 1;
+      desc.vs.uniform_blocks[0].uniforms[2].name = "_22.gradientStart";
+      desc.vs.uniform_blocks[0].uniforms[2].type = SG_UNIFORMTYPE_FLOAT2;
+      desc.vs.uniform_blocks[0].uniforms[2].array_count = 1;
+      desc.vs.uniform_blocks[0].uniforms[3].name = "_22.gradientEnd";
+      desc.vs.uniform_blocks[0].uniforms[3].type = SG_UNIFORMTYPE_FLOAT2;
+      desc.vs.uniform_blocks[0].uniforms[3].array_count = 1;
+      desc.fs.source = fs_path_source_glsl100;
+      desc.fs.entry = "main";
+      desc.fs.uniform_blocks[0].size = 352;
+      desc.fs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.fs.uniform_blocks[0].uniforms[0].name = "_18.fillType";
+      desc.fs.uniform_blocks[0].uniforms[0].type = SG_UNIFORMTYPE_INT;
+      desc.fs.uniform_blocks[0].uniforms[0].array_count = 1;
+      desc.fs.uniform_blocks[0].uniforms[1].name = "_18.colors";
+      desc.fs.uniform_blocks[0].uniforms[1].type = SG_UNIFORMTYPE_FLOAT4;
+      desc.fs.uniform_blocks[0].uniforms[1].array_count = 16;
+      desc.fs.uniform_blocks[0].uniforms[2].name = "_18.stops";
+      desc.fs.uniform_blocks[0].uniforms[2].type = SG_UNIFORMTYPE_FLOAT4;
+      desc.fs.uniform_blocks[0].uniforms[2].array_count = 4;
+      desc.fs.uniform_blocks[0].uniforms[3].name = "_18.stopCount";
+      desc.fs.uniform_blocks[0].uniforms[3].type = SG_UNIFORMTYPE_INT;
+      desc.fs.uniform_blocks[0].uniforms[3].array_count = 1;
+      desc.label = "rive_tess_path_shader";
+    }
+    return &desc;
+  }
+  if (backend == SG_BACKEND_GLES3) {
+    static sg_shader_desc desc;
+    static bool valid;
+    if (!valid) {
+      valid = true;
+      desc.attrs[0].name = "position";
+      desc.vs.source = vs_path_source_glsl300es;
+      desc.vs.entry = "main";
+      desc.vs.uniform_blocks[0].size = 96;
+      desc.vs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.vs.uniform_blocks[0].uniforms[0].name = "_22.mvp";
+      desc.vs.uniform_blocks[0].uniforms[0].type = SG_UNIFORMTYPE_MAT4;
+      desc.vs.uniform_blocks[0].uniforms[0].array_count = 1;
+      desc.vs.uniform_blocks[0].uniforms[1].name = "_22.fillType";
+      desc.vs.uniform_blocks[0].uniforms[1].type = SG_UNIFORMTYPE_INT;
+      desc.vs.uniform_blocks[0].uniforms[1].array_count = 1;
+      desc.vs.uniform_blocks[0].uniforms[2].name = "_22.gradientStart";
+      desc.vs.uniform_blocks[0].uniforms[2].type = SG_UNIFORMTYPE_FLOAT2;
+      desc.vs.uniform_blocks[0].uniforms[2].array_count = 1;
+      desc.vs.uniform_blocks[0].uniforms[3].name = "_22.gradientEnd";
+      desc.vs.uniform_blocks[0].uniforms[3].type = SG_UNIFORMTYPE_FLOAT2;
+      desc.vs.uniform_blocks[0].uniforms[3].array_count = 1;
+      desc.fs.source = fs_path_source_glsl300es;
+      desc.fs.entry = "main";
+      desc.fs.uniform_blocks[0].size = 352;
+      desc.fs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.fs.uniform_blocks[0].uniforms[0].name = "_18.fillType";
+      desc.fs.uniform_blocks[0].uniforms[0].type = SG_UNIFORMTYPE_INT;
+      desc.fs.uniform_blocks[0].uniforms[0].array_count = 1;
+      desc.fs.uniform_blocks[0].uniforms[1].name = "_18.colors";
+      desc.fs.uniform_blocks[0].uniforms[1].type = SG_UNIFORMTYPE_FLOAT4;
+      desc.fs.uniform_blocks[0].uniforms[1].array_count = 16;
+      desc.fs.uniform_blocks[0].uniforms[2].name = "_18.stops";
+      desc.fs.uniform_blocks[0].uniforms[2].type = SG_UNIFORMTYPE_FLOAT4;
+      desc.fs.uniform_blocks[0].uniforms[2].array_count = 4;
+      desc.fs.uniform_blocks[0].uniforms[3].name = "_18.stopCount";
+      desc.fs.uniform_blocks[0].uniforms[3].type = SG_UNIFORMTYPE_INT;
+      desc.fs.uniform_blocks[0].uniforms[3].array_count = 1;
+      desc.label = "rive_tess_path_shader";
+    }
+    return &desc;
+  }
+  if (backend == SG_BACKEND_D3D11) {
+    static sg_shader_desc desc;
+    static bool valid;
+    if (!valid) {
+      valid = true;
+      desc.attrs[0].sem_name = "TEXCOORD";
+      desc.attrs[0].sem_index = 0;
+      desc.vs.source = vs_path_source_hlsl5;
+      desc.vs.d3d11_target = "vs_5_0";
+      desc.vs.entry = "main";
+      desc.vs.uniform_blocks[0].size = 96;
+      desc.vs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.fs.source = fs_path_source_hlsl5;
+      desc.fs.d3d11_target = "ps_5_0";
+      desc.fs.entry = "main";
+      desc.fs.uniform_blocks[0].size = 352;
+      desc.fs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.label = "rive_tess_path_shader";
+    }
+    return &desc;
+  }
+  if (backend == SG_BACKEND_METAL_MACOS) {
+    static sg_shader_desc desc;
+    static bool valid;
+    if (!valid) {
+      valid = true;
+      desc.vs.source = vs_path_source_metal_macos;
+      desc.vs.entry = "main0";
+      desc.vs.uniform_blocks[0].size = 96;
+      desc.vs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.fs.source = fs_path_source_metal_macos;
+      desc.fs.entry = "main0";
+      desc.fs.uniform_blocks[0].size = 352;
+      desc.fs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.label = "rive_tess_path_shader";
+    }
+    return &desc;
+  }
+  if (backend == SG_BACKEND_METAL_IOS) {
+    static sg_shader_desc desc;
+    static bool valid;
+    if (!valid) {
+      valid = true;
+      desc.vs.source = vs_path_source_metal_ios;
+      desc.vs.entry = "main0";
+      desc.vs.uniform_blocks[0].size = 96;
+      desc.vs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.fs.source = fs_path_source_metal_ios;
+      desc.fs.entry = "main0";
+      desc.fs.uniform_blocks[0].size = 352;
+      desc.fs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.label = "rive_tess_path_shader";
+    }
+    return &desc;
+  }
+  if (backend == SG_BACKEND_METAL_SIMULATOR) {
+    static sg_shader_desc desc;
+    static bool valid;
+    if (!valid) {
+      valid = true;
+      desc.vs.source = vs_path_source_metal_sim;
+      desc.vs.entry = "main0";
+      desc.vs.uniform_blocks[0].size = 96;
+      desc.vs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.fs.source = fs_path_source_metal_sim;
+      desc.fs.entry = "main0";
+      desc.fs.uniform_blocks[0].size = 352;
+      desc.fs.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_STD140;
+      desc.label = "rive_tess_path_shader";
+    }
+    return &desc;
+  }
+  return 0;
+}
diff --git a/tess/src/sokol/shader.glsl b/tess/src/sokol/shader.glsl
new file mode 100644
index 0000000..f4b70b7
--- /dev/null
+++ b/tess/src/sokol/shader.glsl
@@ -0,0 +1,101 @@
+// clang-format off
+// clang-format doesn't handle the '@' directives.
+
+@ctype mat4 rive::Mat4
+@ctype vec2 rive::Vec2D
+
+@vs vs
+uniform vs_params {
+    mat4 mvp;
+};
+
+in vec2 position;
+in vec2 texcoord0;
+
+out vec2 uv;
+
+void main() {
+    gl_Position = mvp * vec4(position.x, position.y, 0.0, 1.0);
+    uv = texcoord0;
+}
+@end
+
+@fs fs
+uniform sampler2D tex;
+
+in vec2 uv;
+out vec4 frag_color;
+
+void main() {
+    frag_color = texture(tex, uv);
+}
+@end
+
+@program rive_tess vs fs
+
+
+@vs vs_path
+uniform vs_path_params {
+    mat4 mvp;
+    int fillType;
+    vec2 gradientStart;
+    vec2 gradientEnd;
+};
+
+in vec2 position;
+out vec2 gradient_uv;
+
+void main() {
+    gl_Position =  mvp * vec4(position, 0.0, 1.0);
+    
+    if(fillType == 1) {
+        // Linear gradient.
+        vec2 toEnd = gradientEnd - gradientStart;
+        float lengthSquared = toEnd.x * toEnd.x + toEnd.y * toEnd.y;
+        gradient_uv.x = dot(position - gradientStart, toEnd) / lengthSquared;
+    }
+    else if(fillType == 2) {
+        // fillType == 2 (Radial gradient)
+        gradient_uv = (position - gradientStart) / distance(gradientStart, gradientEnd);
+    }
+}
+@end
+
+@fs fs_path
+
+uniform fs_path_uniforms {
+    int fillType;
+    vec4 colors[16];
+    vec4 stops[4];
+    int stopCount;
+};
+
+in vec2 gradient_uv;
+out vec4 frag_color;
+
+void main() {
+    if (fillType == 0) {
+        // Solid color.
+        frag_color = colors[0];
+    }
+    else {
+        float f = fillType == 1 ? gradient_uv.x : length(gradient_uv);
+        vec4 color =
+            mix(colors[0], colors[1], smoothstep(stops[0][0], stops[0][1], f));
+        for (int i = 1; i < 15; ++i)
+        {
+            if (i >= stopCount - 1)
+            {
+                break;
+            }
+            
+            color = mix(color,
+                            colors[i + 1],
+                            smoothstep(stops[i/4][i%4], stops[(i+1)/4][(i+1)%4], f));
+        }
+        frag_color = color;
+    }
+}
+@end
+
+@program rive_tess_path vs_path fs_path
diff --git a/tess/src/sokol/sokol_factory.cpp b/tess/src/sokol/sokol_factory.cpp
new file mode 100644
index 0000000..00904ab
--- /dev/null
+++ b/tess/src/sokol/sokol_factory.cpp
@@ -0,0 +1,33 @@
+#include "rive/tess/sokol/sokol_factory.hpp"
+
+using namespace rive;
+
+class NoOpRenderPaint : public lite_rtti_override<RenderPaint, NoOpRenderPaint>
+{
+public:
+    void color(unsigned int value) override {}
+    void style(RenderPaintStyle value) override {}
+    void thickness(float value) override {}
+    void join(StrokeJoin value) override {}
+    void cap(StrokeCap value) override {}
+    void blendMode(BlendMode value) override {}
+    void shader(rcp<RenderShader>) override {}
+    void invalidateStroke() override {}
+};
+
+class NoOpRenderPath : public lite_rtti_override<RenderPath, NoOpRenderPath>
+{
+public:
+    void rewind() override {}
+
+    void fillRule(FillRule value) override {}
+    void addPath(CommandPath* path, const Mat2D& transform) override {}
+    void addRenderPath(RenderPath* path, const Mat2D& transform) override {}
+
+    void moveTo(float x, float y) override {}
+    void lineTo(float x, float y) override {}
+    void cubicTo(float ox, float oy, float ix, float iy, float x, float y) override {}
+    void close() override {}
+};
+
+SokolFactory::SokolFactory() {}
diff --git a/tess/src/sokol/sokol_tess_renderer.cpp b/tess/src/sokol/sokol_tess_renderer.cpp
new file mode 100644
index 0000000..346a8c1
--- /dev/null
+++ b/tess/src/sokol/sokol_tess_renderer.cpp
@@ -0,0 +1,1118 @@
+#include "rive/tess/sokol/sokol_tess_renderer.hpp"
+#include "rive/tess/sokol/sokol_factory.hpp"
+#include "rive/tess/tess_render_path.hpp"
+#include "rive/tess/contour_stroke.hpp"
+#include "generated/shader.h"
+#include <unordered_set>
+
+using namespace rive;
+
+static void fillColorBuffer(float* buffer, ColorInt value)
+{
+    buffer[0] = (float)colorRed(value) / 0xFF;
+    buffer[1] = (float)colorGreen(value) / 0xFF;
+    buffer[2] = (float)colorBlue(value) / 0xFF;
+    buffer[3] = colorOpacity(value);
+}
+
+class SokolRenderPath : public lite_rtti_override<TessRenderPath, SokolRenderPath>
+{
+public:
+    SokolRenderPath() {}
+    SokolRenderPath(RawPath& rawPath, FillRule fillRule) : lite_rtti_override(rawPath, fillRule) {}
+
+    ~SokolRenderPath()
+    {
+        sg_destroy_buffer(m_vertexBuffer);
+        sg_destroy_buffer(m_indexBuffer);
+    }
+
+private:
+    std::vector<Vec2D> m_vertices;
+    std::vector<uint16_t> m_indices;
+
+    sg_buffer m_vertexBuffer = {0};
+    sg_buffer m_indexBuffer = {0};
+
+    std::size_t m_boundsIndex = 0;
+
+protected:
+    void addTriangles(rive::Span<const rive::Vec2D> vts, rive::Span<const uint16_t> idx) override
+    {
+        m_vertices.insert(m_vertices.end(), vts.begin(), vts.end());
+        m_indices.insert(m_indices.end(), idx.begin(), idx.end());
+    }
+
+    void setTriangulatedBounds(const AABB& value) override
+    {
+        m_boundsIndex = m_vertices.size();
+        m_vertices.emplace_back(Vec2D(value.minX, value.minY));
+        m_vertices.emplace_back(Vec2D(value.maxX, value.minY));
+        m_vertices.emplace_back(Vec2D(value.maxX, value.maxY));
+        m_vertices.emplace_back(Vec2D(value.minX, value.maxY));
+    }
+
+public:
+    void rewind() override
+    {
+        TessRenderPath::rewind();
+        m_vertices.clear();
+        m_indices.clear();
+    }
+
+    void drawStroke(ContourStroke* stroke)
+    {
+        if (isContainer())
+        {
+            for (auto& subPath : m_subPaths)
+            {
+                LITE_RTTI_CAST_OR_CONTINUE(sokolPath, SokolRenderPath*, subPath.path());
+                sokolPath->drawStroke(stroke);
+            }
+            return;
+        }
+        std::size_t start, end;
+        stroke->nextRenderOffset(start, end);
+        sg_draw(start < 2 ? 0 : (start - 2) * 3, end - start < 2 ? 0 : (end - start - 2) * 3, 1);
+    }
+
+    void drawFill()
+    {
+        if (triangulate())
+        {
+            sg_destroy_buffer(m_vertexBuffer);
+            sg_destroy_buffer(m_indexBuffer);
+            if (m_indices.size() == 0 || m_vertices.size() == 0)
+            {
+                m_vertexBuffer = {0};
+                m_indexBuffer = {0};
+                return;
+            }
+
+            m_vertexBuffer = sg_make_buffer((sg_buffer_desc){
+                .type = SG_BUFFERTYPE_VERTEXBUFFER,
+                .data =
+                    {
+                        m_vertices.data(),
+                        m_vertices.size() * sizeof(Vec2D),
+                    },
+            });
+
+            m_indexBuffer = sg_make_buffer((sg_buffer_desc){
+                .type = SG_BUFFERTYPE_INDEXBUFFER,
+                .data =
+                    {
+                        m_indices.data(),
+                        m_indices.size() * sizeof(uint16_t),
+                    },
+            });
+        }
+
+        if (m_vertexBuffer.id == 0)
+        {
+            return;
+        }
+
+        sg_bindings bind = {
+            .vertex_buffers[0] = m_vertexBuffer,
+            .index_buffer = m_indexBuffer,
+        };
+
+        sg_apply_bindings(&bind);
+        sg_draw(0, m_indices.size(), 1);
+    }
+
+    void drawBounds(const sg_buffer& boundsIndexBuffer)
+    {
+        if (m_vertexBuffer.id == 0)
+        {
+            return;
+        }
+        sg_bindings bind = {
+            .vertex_buffers[0] = m_vertexBuffer,
+            .vertex_buffer_offsets[0] = (int)(m_boundsIndex * sizeof(Vec2D)),
+            .index_buffer = boundsIndexBuffer,
+        };
+
+        sg_apply_bindings(&bind);
+        sg_draw(0, 6, 1);
+    }
+};
+
+// Returns a full-formed RenderPath -- can be treated as immutable
+rcp<RenderPath> SokolFactory::makeRenderPath(RawPath& rawPath, FillRule rule)
+{
+    return make_rcp<SokolRenderPath>(rawPath, rule);
+}
+
+rcp<RenderPath> SokolFactory::makeEmptyRenderPath() { return make_rcp<SokolRenderPath>(); }
+
+class SokolBuffer : public lite_rtti_override<RenderBuffer, SokolBuffer>
+{
+public:
+    SokolBuffer(RenderBufferType type, RenderBufferFlags renderBufferFlags, size_t 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.
+        if (!(flags() & RenderBufferFlags::mappedOnceAtInitialization))
+        {
+            m_buffer = sg_make_buffer(makeNoDataBufferDesc());
+        }
+    }
+    ~SokolBuffer() { sg_destroy_buffer(m_buffer); }
+
+    sg_buffer buffer()
+    {
+        // In the case of RenderBufferFlags::mappedOnceAtInitialization, the client is expected to
+        // map()/unmap() before we need to access this buffer for rendering.
+        assert(m_buffer.id);
+        return m_buffer;
+    }
+
+protected:
+    void* onMap() override
+    {
+        // An immutable buffer is only mapped once, and then we delete m_mappedMemory.
+        assert(m_mappedMemory);
+        return m_mappedMemory.get();
+    }
+
+    void onUnmap() override
+    {
+        if (flags() & RenderBufferFlags::mappedOnceAtInitialization)
+        {
+            // We are an immutable buffer. Creation was deferred until now.
+            assert(!m_buffer.id);
+            assert(m_mappedMemory);
+            sg_buffer_desc bufferDesc = makeNoDataBufferDesc();
+            bufferDesc.data = {m_mappedMemory.get(), sizeInBytes()};
+            m_buffer = sg_make_buffer(bufferDesc);
+            m_mappedMemory.reset(); // This buffer will never be mapped again.
+        }
+        else
+        {
+            assert(m_buffer.id);
+            sg_update_buffer(m_buffer,
+                             sg_range{.ptr = m_mappedMemory.get(), .size = sizeInBytes()});
+        }
+    }
+
+private:
+    sg_buffer_desc makeNoDataBufferDesc() const
+    {
+        return {
+            .size = sizeInBytes(),
+            .usage = (flags() & RenderBufferFlags::mappedOnceAtInitialization) ? SG_USAGE_IMMUTABLE
+                                                                               : SG_USAGE_STREAM,
+            .type = (type() == RenderBufferType::index) ? SG_BUFFERTYPE_INDEXBUFFER
+                                                        : SG_BUFFERTYPE_VERTEXBUFFER,
+        };
+    };
+
+    sg_buffer m_buffer{};
+    std::unique_ptr<char[]> m_mappedMemory;
+};
+
+rcp<RenderBuffer> SokolFactory::makeRenderBuffer(RenderBufferType type,
+                                                 RenderBufferFlags flags,
+                                                 size_t sizeInBytes)
+{
+    return make_rcp<SokolBuffer>(type, flags, sizeInBytes);
+}
+
+sg_pipeline vectorPipeline(sg_shader shader,
+                           sg_blend_state blend,
+                           sg_stencil_state stencil,
+                           sg_color_mask colorMask = SG_COLORMASK_RGBA)
+{
+    return sg_make_pipeline((sg_pipeline_desc){
+        .layout =
+            {
+                .attrs =
+                    {
+                        [ATTR_vs_path_position] =
+                            {
+                                .format = SG_VERTEXFORMAT_FLOAT2,
+                                .buffer_index = 0,
+                            },
+                    },
+            },
+        .shader = shader,
+        .index_type = SG_INDEXTYPE_UINT16,
+        .cull_mode = SG_CULLMODE_NONE,
+        .depth =
+            {
+                .compare = SG_COMPAREFUNC_ALWAYS,
+                .write_enabled = false,
+            },
+        .colors =
+            {
+                [0] =
+                    {
+                        .write_mask = colorMask,
+                        .blend = blend,
+                    },
+            },
+        .stencil = stencil,
+        .label = "path-pipeline",
+    });
+}
+
+SokolTessRenderer::SokolTessRenderer()
+{
+    m_meshPipeline = sg_make_pipeline((sg_pipeline_desc){
+        .layout =
+            {
+                .attrs =
+                    {
+                        [ATTR_vs_position] = {.format = SG_VERTEXFORMAT_FLOAT2, .buffer_index = 0},
+                        [ATTR_vs_texcoord0] = {.format = SG_VERTEXFORMAT_FLOAT2, .buffer_index = 1},
+                    },
+            },
+        .shader = sg_make_shader(rive_tess_shader_desc(sg_query_backend())),
+        .index_type = SG_INDEXTYPE_UINT16,
+        .cull_mode = SG_CULLMODE_NONE,
+        .depth =
+            {
+                .compare = SG_COMPAREFUNC_ALWAYS,
+                .write_enabled = false,
+            },
+        .colors =
+            {
+                [0] =
+                    {
+                        .write_mask = SG_COLORMASK_RGBA,
+                        .blend =
+                            {
+                                .enabled = true,
+                                .src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA,
+                                .dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
+                            },
+                    },
+            },
+        .label = "mesh-pipeline",
+    });
+
+    auto uberShader = sg_make_shader(rive_tess_path_shader_desc(sg_query_backend()));
+
+    assert(maxClippingPaths < 256);
+
+    // Src Over Pipelines
+    {
+        m_pathPipeline[0] = vectorPipeline(uberShader,
+                                           {
+                                               .enabled = true,
+                                               .src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA,
+                                               .dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
+                                           },
+                                           {
+                                               .enabled = false,
+                                           });
+
+        for (std::size_t i = 1; i <= maxClippingPaths; i++)
+        {
+            m_pathPipeline[i] =
+                vectorPipeline(uberShader,
+                               {
+                                   .enabled = true,
+                                   .src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA,
+                                   .dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
+                               },
+                               {
+                                   .enabled = true,
+                                   .ref = (uint8_t)i,
+                                   .read_mask = 0xFF,
+                                   .write_mask = 0x00,
+                                   .front =
+                                       {
+                                           .compare = SG_COMPAREFUNC_EQUAL,
+                                       },
+                                   .back =
+                                       {
+                                           .compare = SG_COMPAREFUNC_EQUAL,
+                                       },
+                               });
+        }
+    }
+
+    // Screen Pipelines
+    {
+        m_pathScreenPipeline[0] =
+            vectorPipeline(uberShader,
+                           {
+                               .enabled = true,
+                               .src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA,
+                               .dst_factor_rgb = SG_BLENDFACTOR_ONE,
+                               .src_factor_alpha = SG_BLENDFACTOR_SRC_ALPHA,
+                               .dst_factor_alpha = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
+                           },
+                           {
+                               .enabled = false,
+                           });
+
+        for (std::size_t i = 1; i <= maxClippingPaths; i++)
+        {
+            m_pathScreenPipeline[i] =
+                vectorPipeline(uberShader,
+                               {
+                                   .enabled = true,
+                                   .src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA,
+                                   .dst_factor_rgb = SG_BLENDFACTOR_ONE,
+                                   .src_factor_alpha = SG_BLENDFACTOR_SRC_ALPHA,
+                                   .dst_factor_alpha = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
+                               },
+                               {
+                                   .enabled = true,
+                                   .ref = (uint8_t)i,
+                                   .read_mask = 0xFF,
+                                   .write_mask = 0x00,
+                                   .front =
+                                       {
+                                           .compare = SG_COMPAREFUNC_EQUAL,
+                                       },
+                                   .back =
+                                       {
+                                           .compare = SG_COMPAREFUNC_EQUAL,
+                                       },
+                               });
+        }
+    }
+
+    // Additive Pipelines
+    {
+        m_pathAdditivePipeline[0] = vectorPipeline(uberShader,
+                                                   {
+                                                       .enabled = true,
+                                                       .src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA,
+                                                       .dst_factor_rgb = SG_BLENDFACTOR_ONE,
+                                                   },
+                                                   {
+                                                       .enabled = false,
+                                                   });
+
+        for (std::size_t i = 1; i <= maxClippingPaths; i++)
+        {
+            m_pathAdditivePipeline[i] =
+                vectorPipeline(uberShader,
+                               {
+                                   .enabled = true,
+                                   .src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA,
+                                   .dst_factor_rgb = SG_BLENDFACTOR_ONE,
+                               },
+                               {
+                                   .enabled = true,
+                                   .ref = (uint8_t)i,
+                                   .read_mask = 0xFF,
+                                   .write_mask = 0x00,
+                                   .front =
+                                       {
+                                           .compare = SG_COMPAREFUNC_EQUAL,
+                                       },
+                                   .back =
+                                       {
+                                           .compare = SG_COMPAREFUNC_EQUAL,
+                                       },
+                               });
+        }
+    }
+
+    // Multiply Pipelines
+    {
+        m_pathMultiplyPipeline[0] =
+            vectorPipeline(uberShader,
+                           {
+                               .enabled = true,
+                               .src_factor_rgb = SG_BLENDFACTOR_DST_COLOR,
+                               .dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
+                           },
+                           {
+                               .enabled = false,
+                           });
+
+        for (std::size_t i = 1; i <= maxClippingPaths; i++)
+        {
+            m_pathMultiplyPipeline[i] =
+                vectorPipeline(uberShader,
+                               {
+                                   .enabled = true,
+                                   .src_factor_rgb = SG_BLENDFACTOR_DST_COLOR,
+                                   .dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
+                               },
+                               {
+                                   .enabled = true,
+                                   .ref = (uint8_t)i,
+                                   .read_mask = 0xFF,
+                                   .write_mask = 0x00,
+                                   .front =
+                                       {
+                                           .compare = SG_COMPAREFUNC_EQUAL,
+                                       },
+                                   .back =
+                                       {
+                                           .compare = SG_COMPAREFUNC_EQUAL,
+                                       },
+                               });
+        }
+    }
+
+    m_incClipPipeline = vectorPipeline(uberShader,
+                                       {
+                                           .enabled = false,
+                                       },
+                                       {
+                                           .enabled = true,
+                                           .read_mask = 0xFF,
+                                           .write_mask = 0xFF,
+                                           .front =
+                                               {
+                                                   .compare = SG_COMPAREFUNC_ALWAYS,
+                                                   .depth_fail_op = SG_STENCILOP_KEEP,
+                                                   .fail_op = SG_STENCILOP_KEEP,
+                                                   .pass_op = SG_STENCILOP_INCR_CLAMP,
+                                               },
+                                           .back =
+                                               {
+                                                   .compare = SG_COMPAREFUNC_ALWAYS,
+                                                   .depth_fail_op = SG_STENCILOP_KEEP,
+                                                   .fail_op = SG_STENCILOP_KEEP,
+                                                   .pass_op = SG_STENCILOP_INCR_CLAMP,
+                                               },
+                                       },
+                                       SG_COLORMASK_NONE);
+
+    m_decClipPipeline = vectorPipeline(uberShader,
+                                       {
+                                           .enabled = false,
+                                       },
+                                       {
+                                           .enabled = true,
+                                           .read_mask = 0xFF,
+                                           .write_mask = 0xFF,
+                                           .front =
+                                               {
+                                                   .compare = SG_COMPAREFUNC_ALWAYS,
+                                                   .depth_fail_op = SG_STENCILOP_KEEP,
+                                                   .fail_op = SG_STENCILOP_KEEP,
+                                                   .pass_op = SG_STENCILOP_DECR_CLAMP,
+                                               },
+                                           .back =
+                                               {
+                                                   .compare = SG_COMPAREFUNC_ALWAYS,
+                                                   .depth_fail_op = SG_STENCILOP_KEEP,
+                                                   .fail_op = SG_STENCILOP_KEEP,
+                                                   .pass_op = SG_STENCILOP_DECR_CLAMP,
+                                               },
+                                       },
+                                       SG_COLORMASK_NONE);
+
+    uint16_t indices[] = {0, 1, 2, 0, 2, 3};
+
+    m_boundsIndices = sg_make_buffer((sg_buffer_desc){
+        .type = SG_BUFFERTYPE_INDEXBUFFER,
+        .data = SG_RANGE(indices),
+    });
+}
+
+SokolTessRenderer::~SokolTessRenderer()
+{
+    sg_destroy_buffer(m_boundsIndices);
+    sg_destroy_pipeline(m_meshPipeline);
+    sg_destroy_pipeline(m_incClipPipeline);
+    sg_destroy_pipeline(m_decClipPipeline);
+    for (std::size_t i = 0; i <= maxClippingPaths; i++)
+    {
+        sg_destroy_pipeline(m_pathPipeline[i]);
+        sg_destroy_pipeline(m_pathScreenPipeline[i]);
+    }
+}
+
+void SokolTessRenderer::orthographicProjection(float left,
+                                               float right,
+                                               float bottom,
+                                               float top,
+                                               float near,
+                                               float far)
+{
+    m_Projection[0] = 2.0f / (right - left);
+    m_Projection[1] = 0.0f;
+    m_Projection[2] = 0.0f;
+    m_Projection[3] = 0.0f;
+
+    m_Projection[4] = 0.0f;
+    m_Projection[5] = 2.0f / (top - bottom);
+    m_Projection[6] = 0.0f;
+    m_Projection[7] = 0.0f;
+
+#ifdef SOKOL_GLCORE33
+    m_Projection[8] = 0.0f;
+    m_Projection[9] = 0.0f;
+    m_Projection[10] = 2.0f / (near - far);
+    m_Projection[11] = 0.0f;
+
+    m_Projection[12] = (right + left) / (left - right);
+    m_Projection[13] = (top + bottom) / (bottom - top);
+    m_Projection[14] = (far + near) / (near - far);
+    m_Projection[15] = 1.0f;
+#else
+    // NDC are slightly different in Metal:
+    // https://metashapes.com/blog/opengl-metal-projection-matrix-problem/
+    m_Projection[8] = 0.0f;
+    m_Projection[9] = 0.0f;
+    m_Projection[10] = 1.0f / (far - near);
+    m_Projection[11] = 0.0f;
+
+    m_Projection[12] = (right + left) / (left - right);
+    m_Projection[13] = (top + bottom) / (bottom - top);
+    m_Projection[14] = near / (near - far);
+    m_Projection[15] = 1.0f;
+#endif
+    // for (int i = 0; i < 4; i++) {
+    //     int b = i * 4;
+    //     printf("%f\t%f\t%f\t%f\n",
+    //            m_Projection[b],
+    //            m_Projection[b + 1],
+    //            m_Projection[b + 2],
+    //            m_Projection[b + 3]);
+    // }
+}
+
+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;
+
+    setPipeline(m_meshPipeline);
+    sg_bindings bind = {
+        .vertex_buffers[0] = sokolImage->vertexBuffer(),
+        .vertex_buffers[1] = sokolImage->uvBuffer(),
+        .index_buffer = m_boundsIndices,
+        .fs_images[SLOT_tex] = sokolImage->image(),
+    };
+
+    sg_apply_bindings(&bind);
+    sg_apply_uniforms(SG_SHADERSTAGE_VS, SLOT_vs_params, SG_RANGE_REF(vs_params));
+    sg_draw(0, 6, 1);
+}
+
+void SokolTessRenderer::drawImageMesh(const RenderImage* renderImage,
+                                      rcp<RenderBuffer> vertices_f32,
+                                      rcp<RenderBuffer> uvCoords_f32,
+                                      rcp<RenderBuffer> indices_u16,
+                                      uint32_t vertexCount,
+                                      uint32_t indexCount,
+                                      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();
+    vs_params.mvp = m_Projection * world;
+
+    setPipeline(m_meshPipeline);
+    sg_bindings bind = {
+        .vertex_buffers[0] = sokolVertices->buffer(),
+        .vertex_buffers[1] = sokolUVCoords->buffer(),
+        .index_buffer = sokolIndices->buffer(),
+        .fs_images[SLOT_tex] = sokolRenderImage->image(),
+    };
+
+    sg_apply_bindings(&bind);
+    sg_apply_uniforms(SG_SHADERSTAGE_VS, SLOT_vs_params, SG_RANGE_REF(vs_params));
+    sg_draw(0, indexCount, 1);
+}
+
+class SokolGradient : public lite_rtti_override<RenderShader, SokolGradient>
+{
+private:
+    Vec2D m_start;
+    Vec2D m_end;
+    int m_type;
+    std::vector<float> m_colors;
+    std::vector<float> m_stops;
+    bool m_isVisible = false;
+
+private:
+    // General gradient
+    SokolGradient(int type, const ColorInt colors[], const float stops[], size_t count) :
+        m_type(type)
+    {
+        m_stops.resize(count);
+        m_colors.resize(count * 4);
+        for (int i = 0, colorIndex = 0; i < count; i++, colorIndex += 4)
+        {
+            fillColorBuffer(&m_colors[colorIndex], colors[i]);
+            m_stops[i] = stops[i];
+            if (m_colors[colorIndex + 3] > 0.0f)
+            {
+                m_isVisible = true;
+            }
+        }
+    }
+
+public:
+    // Linear gradient
+    SokolGradient(float sx,
+                  float sy,
+                  float ex,
+                  float ey,
+                  const ColorInt colors[],
+                  const float stops[],
+                  size_t count) :
+        SokolGradient(1, colors, stops, count)
+    {
+        m_start = Vec2D(sx, sy);
+        m_end = Vec2D(ex, ey);
+    }
+
+    SokolGradient(float cx,
+                  float cy,
+                  float radius,
+                  const ColorInt colors[], // [count]
+                  const float stops[],     // [count]
+                  size_t count) :
+        SokolGradient(2, colors, stops, count)
+    {
+        m_start = Vec2D(cx, cy);
+        m_end = Vec2D(cx + radius, cy);
+    }
+
+    void bind(vs_path_params_t& vertexUniforms, fs_path_uniforms_t& fragmentUniforms)
+    {
+        auto stopCount = m_stops.size();
+        vertexUniforms.fillType = fragmentUniforms.fillType = m_type;
+        vertexUniforms.gradientStart = m_start;
+        vertexUniforms.gradientEnd = m_end;
+        fragmentUniforms.stopCount = stopCount;
+        for (int i = 0; i < stopCount; i++)
+        {
+            auto colorBufferIndex = i * 4;
+            for (int j = 0; j < 4; j++)
+            {
+                fragmentUniforms.colors[i][j] = m_colors[colorBufferIndex + j];
+            }
+            fragmentUniforms.stops[i / 4][i % 4] = m_stops[i];
+        }
+    }
+};
+
+rcp<RenderShader> SokolFactory::makeLinearGradient(float sx,
+                                                   float sy,
+                                                   float ex,
+                                                   float ey,
+                                                   const ColorInt colors[],
+                                                   const float stops[],
+                                                   size_t count)
+{
+    return rcp<RenderShader>(new SokolGradient(sx, sy, ex, ey, colors, stops, count));
+}
+
+rcp<RenderShader> SokolFactory::makeRadialGradient(float cx,
+                                                   float cy,
+                                                   float radius,
+                                                   const ColorInt colors[], // [count]
+                                                   const float stops[],     // [count]
+                                                   size_t count)
+{
+    return rcp<RenderShader>(new SokolGradient(cx, cy, radius, colors, stops, count));
+}
+
+class SokolRenderPaint : public lite_rtti_override<RenderPaint, SokolRenderPaint>
+{
+private:
+    fs_path_uniforms_t m_uniforms = {0};
+    rcp<SokolGradient> m_shader;
+    RenderPaintStyle m_style;
+    std::unique_ptr<ContourStroke> m_stroke;
+    bool m_strokeDirty = false;
+    float m_strokeThickness = 0.0f;
+    StrokeJoin m_strokeJoin;
+    StrokeCap m_strokeCap;
+
+    sg_buffer m_strokeVertexBuffer = {0};
+    sg_buffer m_strokeIndexBuffer = {0};
+    std::vector<std::size_t> m_StrokeOffsets;
+
+    BlendMode m_blendMode = BlendMode::srcOver;
+
+public:
+    ~SokolRenderPaint() override
+    {
+        sg_destroy_buffer(m_strokeVertexBuffer);
+        sg_destroy_buffer(m_strokeIndexBuffer);
+    }
+
+    void color(ColorInt value) override
+    {
+        fillColorBuffer(m_uniforms.colors[0], value);
+        m_uniforms.fillType = 0;
+    }
+
+    void style(RenderPaintStyle value) override
+    {
+        m_style = value;
+
+        switch (m_style)
+        {
+            case RenderPaintStyle::fill:
+                m_stroke = nullptr;
+                m_strokeDirty = false;
+                break;
+            case RenderPaintStyle::stroke:
+                m_stroke = rivestd::make_unique<ContourStroke>();
+                m_strokeDirty = true;
+                break;
+        }
+    }
+
+    RenderPaintStyle style() const { return m_style; }
+
+    void thickness(float value) override
+    {
+        m_strokeThickness = value;
+        m_strokeDirty = true;
+    }
+
+    void join(StrokeJoin value) override
+    {
+        m_strokeJoin = value;
+        m_strokeDirty = true;
+    }
+
+    void cap(StrokeCap value) override
+    {
+        m_strokeCap = value;
+        m_strokeDirty = true;
+    }
+
+    void invalidateStroke() override
+    {
+        if (m_stroke)
+        {
+            m_strokeDirty = true;
+        }
+    }
+
+    void blendMode(BlendMode value) override { m_blendMode = value; }
+    BlendMode blendMode() const { return m_blendMode; }
+
+    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)
+        {
+            m_shader->bind(vertexUniforms, m_uniforms);
+        }
+
+        sg_apply_uniforms(SG_SHADERSTAGE_VS, SLOT_vs_path_params, SG_RANGE_REF(vertexUniforms));
+        sg_apply_uniforms(SG_SHADERSTAGE_FS, SLOT_fs_path_uniforms, SG_RANGE_REF(m_uniforms));
+        if (m_stroke != nullptr)
+        {
+            if (m_strokeDirty)
+            {
+                static Mat2D identity;
+                m_stroke->reset();
+                path->extrudeStroke(m_stroke.get(),
+                                    m_strokeJoin,
+                                    m_strokeCap,
+                                    m_strokeThickness / 2.0f,
+                                    identity);
+                m_strokeDirty = false;
+
+                const std::vector<Vec2D>& strip = m_stroke->triangleStrip();
+
+                sg_destroy_buffer(m_strokeVertexBuffer);
+                sg_destroy_buffer(m_strokeIndexBuffer);
+                auto size = strip.size();
+                if (size <= 2)
+                {
+                    m_strokeVertexBuffer = {0};
+                    m_strokeIndexBuffer = {0};
+                    return;
+                }
+
+                m_strokeVertexBuffer = sg_make_buffer((sg_buffer_desc){
+                    .type = SG_BUFFERTYPE_VERTEXBUFFER,
+                    .data =
+                        {
+                            strip.data(),
+                            strip.size() * sizeof(Vec2D),
+                        },
+                });
+
+                // Let's use a tris index buffer so we can keep the same sokol pipeline.
+                std::vector<uint16_t> indices;
+
+                // Build them by stroke offsets (where each offset represents a sub-path, or a move
+                // to)
+                m_stroke->resetRenderOffset();
+                m_StrokeOffsets.clear();
+                while (true)
+                {
+                    std::size_t strokeStart, strokeEnd;
+                    if (!m_stroke->nextRenderOffset(strokeStart, strokeEnd))
+                    {
+                        break;
+                    }
+                    std::size_t length = strokeEnd - strokeStart;
+                    if (length > 2)
+                    {
+                        for (std::size_t i = 0, end = length - 2; i < end; i++)
+                        {
+                            if ((i % 2) == 1)
+                            {
+                                indices.push_back(i + strokeStart);
+                                indices.push_back(i + 1 + strokeStart);
+                                indices.push_back(i + 2 + strokeStart);
+                            }
+                            else
+                            {
+                                indices.push_back(i + strokeStart);
+                                indices.push_back(i + 2 + strokeStart);
+                                indices.push_back(i + 1 + strokeStart);
+                            }
+                        }
+                        m_StrokeOffsets.push_back(indices.size());
+                    }
+                }
+
+                m_strokeIndexBuffer = sg_make_buffer((sg_buffer_desc){
+                    .type = SG_BUFFERTYPE_INDEXBUFFER,
+                    .data =
+                        {
+                            indices.data(),
+                            indices.size() * sizeof(uint16_t),
+                        },
+                });
+            }
+            if (m_strokeVertexBuffer.id == 0)
+            {
+                return;
+            }
+
+            sg_bindings bind = {
+                .vertex_buffers[0] = m_strokeVertexBuffer,
+                .index_buffer = m_strokeIndexBuffer,
+            };
+
+            sg_apply_bindings(&bind);
+
+            m_stroke->resetRenderOffset();
+            // path->drawStroke(m_stroke.get());
+            std::size_t start = 0;
+            for (auto end : m_StrokeOffsets)
+            {
+                sg_draw(start, end - start, 1);
+                start = end;
+            }
+        }
+        else
+        {
+            path->drawFill();
+        }
+    }
+};
+
+rcp<RenderPaint> SokolFactory::makeRenderPaint() { return make_rcp<SokolRenderPaint>(); }
+
+void SokolTessRenderer::restore()
+{
+    TessRenderer::restore();
+    if (m_Stack.size() == 1)
+    {
+        // When we've fully restored, immediately update clip to not wait for next draw.
+        applyClipping();
+        m_currentPipeline = {0};
+    }
+}
+
+void SokolTessRenderer::applyClipping()
+{
+    if (!m_IsClippingDirty)
+    {
+        return;
+    }
+    m_IsClippingDirty = false;
+    RenderState& state = m_Stack.back();
+
+    auto currentClipLength = m_ClipPaths.size();
+    if (currentClipLength == state.clipPaths.size())
+    {
+        // Same length so now check if they're all the same.
+        bool allSame = true;
+        for (std::size_t i = 0; i < currentClipLength; i++)
+        {
+            if (state.clipPaths[i].path() != m_ClipPaths[i].path())
+            {
+                allSame = false;
+                break;
+            }
+        }
+        if (allSame)
+        {
+            return;
+        }
+    }
+
+    vs_path_params_t vs_params = {.fillType = 0};
+    fs_path_uniforms_t uniforms = {0};
+
+    // Decr any paths from the last clip that are gone.
+    std::unordered_set<RenderPath*> alreadyApplied;
+
+    for (auto appliedPath : m_ClipPaths)
+    {
+        bool decr = true;
+        for (auto nextClipPath : state.clipPaths)
+        {
+            if (nextClipPath.path() == appliedPath.path())
+            {
+                decr = false;
+                alreadyApplied.insert(appliedPath.path());
+                break;
+            }
+        }
+        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));
+            sokolPath->drawFill();
+        }
+    }
+
+    // Incr any paths that are added.
+    for (auto nextClipPath : state.clipPaths)
+    {
+        if (alreadyApplied.count(nextClipPath.path()))
+        {
+            // Already applied.
+            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));
+        sokolPath->drawFill();
+    }
+
+    // Pick which pipeline to use for draw path operations.
+    // TODO: something similar for draw mesh.
+    m_clipCount = state.clipPaths.size();
+
+    m_ClipPaths = state.clipPaths;
+}
+
+void SokolTessRenderer::reset() { m_currentPipeline = {0}; }
+void SokolTessRenderer::setPipeline(sg_pipeline pipeline)
+{
+    if (m_currentPipeline.id == pipeline.id)
+    {
+        return;
+    }
+    m_currentPipeline = pipeline;
+    sg_apply_pipeline(pipeline);
+}
+
+void SokolTessRenderer::drawPath(RenderPath* path, RenderPaint* 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};
+    const Mat2D& world = transform();
+
+    vs_params.mvp = m_Projection * world;
+    switch (sokolPaint->blendMode())
+    {
+        case BlendMode::srcOver:
+            setPipeline(m_pathPipeline[m_clipCount]);
+            break;
+        case BlendMode::screen:
+            setPipeline(m_pathScreenPipeline[m_clipCount]);
+            break;
+        case BlendMode::colorDodge:
+            setPipeline(m_pathAdditivePipeline[m_clipCount]);
+            break;
+        case BlendMode::multiply:
+            setPipeline(m_pathMultiplyPipeline[m_clipCount]);
+            break;
+        default:
+            setPipeline(m_pathScreenPipeline[m_clipCount]);
+            break;
+    }
+
+    sokolPaint->draw(vs_params, sokolPath);
+}
+
+SokolRenderImageResource::SokolRenderImageResource(const uint8_t* bytes,
+                                                   uint32_t width,
+                                                   uint32_t height) :
+    m_gpuResource(sg_make_image((sg_image_desc){
+        .width = (int)width,
+        .height = (int)height,
+        .data.subimage[0][0] = {bytes, width * height * 4},
+        .pixel_format = SG_PIXELFORMAT_RGBA8,
+    }))
+{}
+SokolRenderImageResource::~SokolRenderImageResource() { sg_destroy_image(m_gpuResource); }
+
+SokolRenderImage::SokolRenderImage(rcp<SokolRenderImageResource> image,
+                                   uint32_t width,
+                                   uint32_t height,
+                                   const Mat2D& uvTransform) :
+
+    lite_rtti_override(uvTransform), m_gpuImage(image)
+
+{
+    float halfWidth = width / 2.0f;
+    float halfHeight = height / 2.0f;
+    Vec2D points[] = {
+        Vec2D(-halfWidth, -halfHeight),
+        Vec2D(halfWidth, -halfHeight),
+        Vec2D(halfWidth, halfHeight),
+        Vec2D(-halfWidth, halfHeight),
+    };
+    m_vertexBuffer = sg_make_buffer((sg_buffer_desc){
+        .type = SG_BUFFERTYPE_VERTEXBUFFER,
+        .data = SG_RANGE(points),
+    });
+
+    Vec2D uv[] = {
+        uvTransform * Vec2D(0.0f, 0.0f),
+        uvTransform * Vec2D(1.0f, 0.0f),
+        uvTransform * Vec2D(1.0f, 1.0f),
+        uvTransform * Vec2D(0.0f, 1.0f),
+    };
+
+    m_uvBuffer = sg_make_buffer((sg_buffer_desc){
+        .type = SG_BUFFERTYPE_VERTEXBUFFER,
+        .data = SG_RANGE(uv),
+    });
+}
+
+SokolRenderImage::~SokolRenderImage()
+{
+    sg_destroy_buffer(m_vertexBuffer);
+    sg_destroy_buffer(m_uvBuffer);
+}
diff --git a/tess/src/sub_path.cpp b/tess/src/sub_path.cpp
new file mode 100644
index 0000000..73e58af
--- /dev/null
+++ b/tess/src/sub_path.cpp
@@ -0,0 +1,8 @@
+#include "rive/tess/sub_path.hpp"
+
+using namespace rive;
+
+SubPath::SubPath(RenderPath* path, const Mat2D& transform) : m_Path(path), m_Transform(transform) {}
+
+RenderPath* SubPath::path() const { return m_Path; }
+const Mat2D& SubPath::transform() const { return m_Transform; }
\ No newline at end of file
diff --git a/tess/src/tess_render_path.cpp b/tess/src/tess_render_path.cpp
new file mode 100644
index 0000000..2252b5e
--- /dev/null
+++ b/tess/src/tess_render_path.cpp
@@ -0,0 +1,219 @@
+#include "rive/tess/tess_render_path.hpp"
+#include "rive/tess/contour_stroke.hpp"
+#include "tesselator.h"
+
+static const float contourThreshold = 1.0f;
+
+using namespace rive;
+TessRenderPath::TessRenderPath() : m_segmentedContour(contourThreshold) {}
+TessRenderPath::TessRenderPath(RawPath& rawPath, FillRule fillRule) :
+    m_fillRule(fillRule), m_segmentedContour(contourThreshold)
+{
+    m_rawPath.swap(rawPath);
+}
+
+TessRenderPath::~TessRenderPath() {}
+
+void TessRenderPath::rewind()
+{
+    m_rawPath.rewind();
+    m_subPaths.clear();
+    m_isContourDirty = m_isTriangulationDirty = true;
+    m_isClosed = false;
+}
+
+void TessRenderPath::fillRule(FillRule value) { m_fillRule = value; }
+
+void TessRenderPath::moveTo(float x, float y) { m_rawPath.moveTo(x, y); }
+void TessRenderPath::lineTo(float x, float y) { m_rawPath.lineTo(x, y); }
+void TessRenderPath::cubicTo(float ox, float oy, float ix, float iy, float x, float y)
+{
+    m_rawPath.cubicTo(ox, oy, ix, iy, x, y);
+}
+void TessRenderPath::close()
+{
+    m_rawPath.close();
+    m_isClosed = true;
+}
+
+void TessRenderPath::addRenderPath(RenderPath* path, const Mat2D& transform)
+{
+    m_subPaths.emplace_back(SubPath(path, transform));
+}
+
+const SegmentedContour& TessRenderPath::segmentedContour() const { return m_segmentedContour; }
+
+// Helper for earcut to understand Vec2D
+namespace mapbox
+{
+namespace util
+{
+
+template <> struct nth<0, Vec2D>
+{
+    inline static float get(const Vec2D& t) { return t.x; };
+};
+template <> struct nth<1, Vec2D>
+{
+    inline static float get(const Vec2D& t) { return t.y; };
+};
+
+} // namespace util
+} // namespace mapbox
+
+const RawPath& TessRenderPath::rawPath() const { return m_rawPath; }
+
+void* stdAlloc(void* userData, unsigned int size)
+{
+    int* allocated = (int*)userData;
+    TESS_NOTUSED(userData);
+    *allocated += (int)size;
+    return malloc(size);
+}
+
+void stdFree(void* userData, void* ptr)
+{
+    TESS_NOTUSED(userData);
+    free(ptr);
+}
+
+bool TessRenderPath::triangulate()
+{
+    if (!m_isTriangulationDirty)
+    {
+        return false;
+    }
+    m_isTriangulationDirty = false;
+    triangulate(this);
+    return true;
+}
+
+void TessRenderPath::triangulate(TessRenderPath* containerPath)
+{
+    AABB bounds = AABB::forExpansion();
+    // If there's a single path, we're going to try to assume the user isn't
+    // doing any funky self overlapping winding and we'll try to triangulate it
+    // quickly as a single polygon.
+    if (m_subPaths.size() == 0)
+    {
+        if (!empty())
+        {
+            Mat2D identity;
+            contour(identity);
+
+            bounds = m_segmentedContour.bounds();
+
+            auto contour = m_segmentedContour.contourPoints();
+            auto contours = rive::make_span(&contour, 1);
+            m_earcut(contours);
+
+            containerPath->addTriangles(contour, m_earcut.indices);
+        }
+    }
+    else if (m_subPaths.size() == 1)
+    {
+        // We're a container but we only have 1 path, let's see if we can use
+        // our fast triangulator.
+        SubPath& subPath = m_subPaths.front();
+        auto subRenderPath = static_cast<TessRenderPath*>(subPath.path());
+        if (subRenderPath->isContainer())
+        {
+            // Nope, subpath is also a container, keep going.
+            subRenderPath->triangulate(containerPath);
+        }
+        else if (!subRenderPath->empty())
+        {
+            // Yes, it's a single path with commands, triangulate it.
+            subRenderPath->contour(subPath.transform());
+            const SegmentedContour& segmentedContour = subRenderPath->segmentedContour();
+            auto contour = segmentedContour.contourPoints();
+            auto contours = rive::make_span(&contour, 1);
+            m_earcut(contours);
+
+            containerPath->addTriangles(contour, m_earcut.indices);
+        }
+    }
+    else
+    {
+        // We're a container with multiple sub-paths.
+        TESStesselator* tess = nullptr;
+        for (SubPath& subPath : m_subPaths)
+        {
+            auto subRenderPath = static_cast<TessRenderPath*>(subPath.path());
+            if (subRenderPath->isContainer())
+            {
+                subRenderPath->triangulate(containerPath);
+            }
+            else if (!subRenderPath->empty())
+            {
+                if (tess == nullptr)
+                {
+                    tess = tessNewTess(nullptr);
+                }
+                subRenderPath->contour(subPath.transform());
+                const SegmentedContour& segmentedContour = subRenderPath->segmentedContour();
+                auto contour = segmentedContour.contourPoints();
+                tessAddContour(tess, 2, contour.data(), sizeof(float) * 2, contour.size());
+                bounds.expand(segmentedContour.bounds());
+            }
+        }
+        if (tess != nullptr)
+        {
+            if (tessTesselate(tess, TESS_WINDING_POSITIVE, TESS_POLYGONS, 3, 2, 0))
+            {
+                auto verts = tessGetVertices(tess);
+                // const int* vinds = tessGetVertexIndices(tess);
+                auto nverts = tessGetVertexCount(tess);
+                auto elems = tessGetElements(tess);
+                auto nelems = tessGetElementCount(tess);
+
+                std::vector<uint16_t> indices;
+                for (int i = 0; i < nelems * 3; i++)
+                {
+                    indices.push_back(elems[i]);
+                }
+
+                containerPath->addTriangles(
+                    Span<const rive::Vec2D>(reinterpret_cast<const Vec2D*>(verts), nverts),
+                    indices);
+            }
+            tessDeleteTess(tess);
+        }
+    }
+
+    containerPath->setTriangulatedBounds(bounds);
+}
+
+void TessRenderPath::contour(const Mat2D& transform)
+{
+    if (!m_isContourDirty && transform == m_contourTransform)
+    {
+        return;
+    }
+
+    m_isContourDirty = false;
+    m_contourTransform = transform;
+    m_segmentedContour.contour(m_rawPath, transform);
+}
+
+void TessRenderPath::extrudeStroke(ContourStroke* stroke,
+                                   StrokeJoin join,
+                                   StrokeCap cap,
+                                   float strokeWidth,
+                                   const Mat2D& transform)
+{
+    if (isContainer())
+    {
+        for (auto& subPath : m_subPaths)
+        {
+            static_cast<TessRenderPath*>(subPath.path())
+                ->extrudeStroke(stroke, join, cap, strokeWidth, subPath.transform());
+        }
+        return;
+    }
+
+    contour(transform);
+    stroke->extrude(&m_segmentedContour, m_isClosed, join, cap, strokeWidth);
+}
+
+bool TessRenderPath::empty() const { return m_rawPath.empty(); }
diff --git a/tess/src/tess_renderer.cpp b/tess/src/tess_renderer.cpp
new file mode 100644
index 0000000..42f697d
--- /dev/null
+++ b/tess/src/tess_renderer.cpp
@@ -0,0 +1,48 @@
+#include "rive/tess/tess_renderer.hpp"
+#include <cassert>
+
+using namespace rive;
+
+TessRenderer::TessRenderer() { m_Stack.emplace_back(RenderState()); }
+
+void TessRenderer::projection(const Mat4& value) { m_Projection = value; }
+
+void TessRenderer::save() { m_Stack.push_back(m_Stack.back()); }
+void TessRenderer::restore()
+{
+    assert(m_Stack.size() > 1);
+    RenderState& state = m_Stack.back();
+    m_Stack.pop_back();
+
+    // We can only add clipping paths so if they're still the same, nothing has
+    // changed.
+    if (state.clipPaths.size() != m_Stack.back().clipPaths.size())
+    {
+        m_IsClippingDirty = true;
+    }
+}
+
+void TessRenderer::transform(const Mat2D& transform)
+{
+    Mat2D& stackMat = m_Stack.back().transform;
+    stackMat = stackMat * transform;
+}
+
+void TessRenderer::clipPath(RenderPath* path)
+{
+
+    RenderState& state = m_Stack.back();
+    state.clipPaths.emplace_back(SubPath(path, state.transform));
+    m_IsClippingDirty = true;
+}
+
+void TessRenderer::drawImage(const RenderImage*, BlendMode, float opacity) {}
+void TessRenderer::drawImageMesh(const RenderImage*,
+                                 rcp<RenderBuffer> vertices_f32,
+                                 rcp<RenderBuffer> uvCoords_f32,
+                                 rcp<RenderBuffer> indices_u16,
+                                 uint32_t vertexCount,
+                                 uint32_t indexCount,
+                                 BlendMode,
+                                 float opacity)
+{}
diff --git a/tess/test/assets/marty.riv b/tess/test/assets/marty.riv
new file mode 100644
index 0000000..6b4817e
--- /dev/null
+++ b/tess/test/assets/marty.riv
Binary files differ
diff --git a/tess/test/assets/triangle.riv b/tess/test/assets/triangle.riv
new file mode 100644
index 0000000..b86c008
--- /dev/null
+++ b/tess/test/assets/triangle.riv
Binary files differ
diff --git a/tess/test/main_test.cpp b/tess/test/main_test.cpp
new file mode 100644
index 0000000..37eab93
--- /dev/null
+++ b/tess/test/main_test.cpp
@@ -0,0 +1,6 @@
+// The only purpose of this file is to DEFINE the catch config so it can include
+// main()
+
+#define CATCH_CONFIG_MAIN // This tells Catch to provide a main() - only do this
+                          // in one cpp file
+#include <catch.hpp>
\ No newline at end of file
diff --git a/tess/test/mat4_test.cpp b/tess/test/mat4_test.cpp
new file mode 100644
index 0000000..782ca3b
--- /dev/null
+++ b/tess/test/mat4_test.cpp
@@ -0,0 +1,69 @@
+#include <catch.hpp>
+#include "rive/math/mat4.hpp"
+#include "rive/math/mat2d.hpp"
+
+TEST_CASE("Mat2D to Mat4 works", "[mat4]")
+{
+    rive::Mat2D matrix2D(0.1f, 0.2f, 0.0f, 2.0f, 22.0f, 33.0f);
+    rive::Mat4 matrix4x4 = matrix2D;
+    REQUIRE(matrix4x4[0] == 0.1f);
+    REQUIRE(matrix4x4[1] == 0.2f);
+    REQUIRE(matrix4x4[4] == 0.0f);
+    REQUIRE(matrix4x4[5] == 2.0f);
+    REQUIRE(matrix4x4[12] == 22.0f);
+    REQUIRE(matrix4x4[13] == 33.0f);
+}
+
+TEST_CASE("Mat4 times Mat2 works", "[mat4]")
+{
+    rive::Mat2D matrix2D(0.1f, 0.2f, 0.0f, 2.0f, 22.0f, 33.0f);
+    rive::Mat4 identity4x4;
+    rive::Mat4 matrix4x4 = identity4x4 * matrix2D;
+    REQUIRE(matrix4x4[0] == 0.1f);
+    REQUIRE(matrix4x4[1] == 0.2f);
+    REQUIRE(matrix4x4[4] == 0.0f);
+    REQUIRE(matrix4x4[5] == 2.0f);
+    REQUIRE(matrix4x4[12] == 22.0f);
+    REQUIRE(matrix4x4[13] == 33.0f);
+}
+
+TEST_CASE("Mat4 times Mat4 works", "[mat4]")
+{
+    rive::Mat4 a(
+        // clang-format off
+        5.0f, 7.0f, 9.0f, 10.0f,
+        2.0f, 3.0f, 3.0f, 8.0f,
+        8.0f, 10.0f, 2.0f, 3.0f, 
+        3.0f, 3.0f, 4.0f, 8.0f
+        // clang-format on
+    );
+    rive::Mat4 b(
+        // clang-format off
+        3.0f, 10.0f, 12.0f, 18.0f,
+        12.0f, 1.0f, 4.0f, 9.0f,
+        9.0f, 10.0f, 12.0f, 2.0f, 
+        3.0f, 12.0f, 4.0f, 10.0f
+        // clang-format on
+    );
+
+    auto result = b * a;
+    REQUIRE(result[0] == 210.0f);
+    REQUIRE(result[1] == 267.0f);
+    REQUIRE(result[2] == 236.0f);
+    REQUIRE(result[3] == 271.0f);
+
+    REQUIRE(result[4] == 93.0f);
+    REQUIRE(result[5] == 149.0f);
+    REQUIRE(result[6] == 104.0f);
+    REQUIRE(result[7] == 149.0f);
+
+    REQUIRE(result[8] == 171.0f);
+    REQUIRE(result[9] == 146.0f);
+    REQUIRE(result[10] == 172.0f);
+    REQUIRE(result[11] == 268.0f);
+
+    REQUIRE(result[12] == 105.0f);
+    REQUIRE(result[13] == 169.0f);
+    REQUIRE(result[14] == 128.0f);
+    REQUIRE(result[15] == 169.0f);
+}
\ No newline at end of file
diff --git a/tess/test/triangulation_test.cpp b/tess/test/triangulation_test.cpp
new file mode 100644
index 0000000..d2a4b01
--- /dev/null
+++ b/tess/test/triangulation_test.cpp
@@ -0,0 +1,43 @@
+#include "rive/shapes/path.hpp"
+#include "rive/math/mat2d.hpp"
+#include "rive/tess/tess_render_path.hpp"
+#include "rive_file_reader.hpp"
+
+class TestRenderPath : public rive::TessRenderPath
+{
+public:
+    std::vector<rive::Vec2D> vertices;
+    std::vector<uint16_t> indices;
+
+protected:
+    virtual void addTriangles(rive::Span<const rive::Vec2D> vts,
+                              rive::Span<const uint16_t> idx) override
+    {
+        vertices.insert(vertices.end(), vts.begin(), vts.end());
+        indices.insert(indices.end(), idx.begin(), idx.end());
+    }
+
+    void setTriangulatedBounds(const rive::AABB& value) override {}
+};
+
+TEST_CASE("simple triangle path triangulates as expected", "[file]")
+{
+    auto file = ReadRiveFile("../test/assets/triangle.riv");
+    auto artboard = file->artboard();
+    artboard->advance(0.0f);
+
+    auto path = artboard->find<rive::Path>("triangle_path");
+    REQUIRE(path != nullptr);
+    TestRenderPath renderPath;
+    path->rawPath().addTo(&renderPath);
+
+    rive::Mat2D identity;
+    TestRenderPath shapeRenderPath;
+    shapeRenderPath.addRenderPath(&renderPath, identity);
+    shapeRenderPath.triangulate();
+    REQUIRE(shapeRenderPath.vertices.size() == 4);
+    REQUIRE(shapeRenderPath.indices.size() == 3);
+    REQUIRE(shapeRenderPath.indices[0] == 2);
+    REQUIRE(shapeRenderPath.indices[1] == 0);
+    REQUIRE(shapeRenderPath.indices[2] == 1);
+}
\ No newline at end of file
diff --git a/test/aabb_test.cpp b/test/aabb_test.cpp
new file mode 100644
index 0000000..2e380b3
--- /dev/null
+++ b/test/aabb_test.cpp
@@ -0,0 +1,67 @@
+#include <catch.hpp>
+#include "rive/math/mat2d.hpp"
+#include "rive/math/math_types.hpp"
+
+namespace rive
+{
+TEST_CASE("IAABB_join", "[IAABB]")
+{
+    CHECK(IAABB{1, -2, 99, 101}.join({0, 0, 100, 100}) == IAABB{0, -2, 100, 101});
+    CHECK(IAABB{1, -2, 99, 101}.join({2, -3, 98, 103}) == IAABB{1, -3, 99, 103});
+}
+
+TEST_CASE("IAABB_intersect", "[IAABB]")
+{
+    CHECK(IAABB{1, -2, 99, 101}.intersect({0, 0, 100, 100}) == IAABB{1, 0, 99, 100});
+    CHECK(IAABB{1, -2, 99, 101}.intersect({2, -3, 98, 103}) == IAABB{2, -2, 98, 101});
+}
+
+TEST_CASE("IAABB_empty", "[IAABB]")
+{
+    CHECK(IAABB{0, 0, 0, 0}.empty());
+    CHECK(IAABB{0, 0, 0, 1}.empty());
+    CHECK(IAABB{0, 0, 1, 0}.empty());
+    CHECK(!IAABB{0, 0, 1, 1}.empty());
+    CHECK(IAABB{0, 0, -1, -1}.empty());
+    CHECK(IAABB{std::numeric_limits<int32_t>::max(),
+                std::numeric_limits<int32_t>::max(),
+                std::numeric_limits<int32_t>::min(),
+                std::numeric_limits<int32_t>::min()}
+              .empty());
+}
+
+TEST_CASE("isEmptyOrNaN", "[AABB]")
+{
+    auto inf = std::numeric_limits<float>::infinity();
+    auto nan = std::numeric_limits<float>::quiet_NaN();
+    CHECK(!AABB{0, 0, 1, 1}.isEmptyOrNaN());
+    CHECK(!AABB{-inf, -inf, inf, inf}.isEmptyOrNaN());
+    CHECK(AABB{0, 0, 0, 0}.isEmptyOrNaN());
+    CHECK(AABB{0, 0, -1, -2}.isEmptyOrNaN());
+    CHECK(AABB{inf, inf, -inf, -inf}.isEmptyOrNaN());
+    CHECK(AABB{inf, -inf, -inf, inf}.isEmptyOrNaN());
+    CHECK(AABB{-inf, inf, inf, -inf}.isEmptyOrNaN());
+    CHECK(AABB{nan, 0, 10, 10}.isEmptyOrNaN());
+    CHECK(AABB{0, nan, 10, 10}.isEmptyOrNaN());
+    CHECK(AABB{0, 0, nan, 10}.isEmptyOrNaN());
+    CHECK(AABB{0, 0, 10, nan}.isEmptyOrNaN());
+    CHECK(AABB{nan, nan, 10, 10}.isEmptyOrNaN());
+    CHECK(AABB{nan, nan, nan, 10}.isEmptyOrNaN());
+    CHECK(AABB{nan, nan, nan, nan}.isEmptyOrNaN());
+}
+
+TEST_CASE("AABB contains", "[AABB]")
+{
+    CHECK(AABB{0, 0, 100, 100}.contains(Vec2D(20, 20)));
+    CHECK(AABB{0, 0, 100, 100}.contains(Vec2D(0, 0)));
+    CHECK(AABB{0, 0, 100, 100}.contains(Vec2D(100, 100)));
+    CHECK(!AABB{0, 0, 100, 100}.contains(Vec2D(200, 200)));
+    CHECK(!AABB{0, 0, 100, 100}.contains(Vec2D(-200, -200)));
+    auto leftBoundary = 0.f;
+    auto rightBoundary = 100.f;
+    CHECK(!AABB{leftBoundary, 0, rightBoundary, 100.0}.contains(
+        Vec2D(leftBoundary - std::numeric_limits<float>::epsilon(), 50)));
+    CHECK(!AABB{leftBoundary, 0, rightBoundary, 100.0}.contains(
+        Vec2D(rightBoundary + rightBoundary * std::numeric_limits<float>::epsilon(), 50)));
+}
+} // namespace rive
diff --git a/test/animation_state_instance_test.cpp b/test/animation_state_instance_test.cpp
new file mode 100644
index 0000000..918f2e4
--- /dev/null
+++ b/test/animation_state_instance_test.cpp
@@ -0,0 +1,499 @@
+#include "rive/animation/loop.hpp"
+#include "rive/animation/linear_animation.hpp"
+#include "rive/animation/linear_animation_instance.hpp"
+
+#include "rive/animation/animation_state.hpp"
+#include "rive/animation/animation_state_instance.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "utils/no_op_factory.hpp"
+#include "rive/scene.hpp"
+#include <catch.hpp>
+#include <cstdio>
+
+TEST_CASE("AnimationStateInstance advances in step with animation speed 1", "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    // For each of these tests, we cons up a dummy artboard/instance
+    // just to make the animations happy.
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 5
+    linearAnimation->duration(10);
+    linearAnimation->fps(2);
+    linearAnimation->loopValue(static_cast<int>(rive::Loop::oneShot));
+
+    rive::AnimationState* animationState = new rive::AnimationState();
+    animationState->animation(linearAnimation);
+
+    rive::AnimationStateInstance* animationStateInstance =
+        new rive::AnimationStateInstance(animationState, abi.get());
+
+    // play from beginning.
+    rive::StateMachine machine;
+    rive::StateMachineInstance stateMachineInstance(&machine, abi.get());
+    animationStateInstance->advance(2.0, &stateMachineInstance);
+
+    REQUIRE(animationStateInstance->animationInstance()->time() == 2.0);
+    REQUIRE(animationStateInstance->animationInstance()->totalTime() == 2.0);
+
+    delete animationStateInstance;
+    delete animationState;
+    delete linearAnimation;
+}
+
+TEST_CASE("AnimationStateInstance advances twice as fast when speed is doubled", "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    // For each of these tests, we cons up a dummy artboard/instance
+    // just to make the animations happy.
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::StateMachine machine;
+    rive::StateMachineInstance stateMachineInstance(&machine, abi.get());
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 5
+    linearAnimation->duration(10);
+    linearAnimation->fps(2);
+    linearAnimation->loopValue(static_cast<int>(rive::Loop::oneShot));
+
+    rive::AnimationState* animationState = new rive::AnimationState();
+    animationState->animation(linearAnimation);
+    animationState->speed(2);
+
+    rive::AnimationStateInstance* animationStateInstance =
+        new rive::AnimationStateInstance(animationState, abi.get());
+
+    // play from beginning.
+    animationStateInstance->advance(2.0, &stateMachineInstance);
+
+    REQUIRE(animationStateInstance->animationInstance()->time() == 4.0);
+    REQUIRE(animationStateInstance->animationInstance()->totalTime() == 4.0);
+
+    delete animationStateInstance;
+    delete animationState;
+    delete linearAnimation;
+}
+
+TEST_CASE("AnimationStateInstance advances half as fast when speed is halved", "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    // For each of these tests, we cons up a dummy artboard/instance
+    // just to make the animations happy.
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 5
+    linearAnimation->duration(10);
+    linearAnimation->fps(2);
+    linearAnimation->loopValue(static_cast<int>(rive::Loop::oneShot));
+
+    rive::AnimationState* animationState = new rive::AnimationState();
+    animationState->animation(linearAnimation);
+    animationState->speed(0.5);
+
+    rive::AnimationStateInstance* animationStateInstance =
+        new rive::AnimationStateInstance(animationState, abi.get());
+
+    // play from beginning.
+    rive::StateMachine machine;
+    rive::StateMachineInstance stateMachineInstance(&machine, abi.get());
+    animationStateInstance->advance(2.0, &stateMachineInstance);
+
+    REQUIRE(animationStateInstance->animationInstance()->time() == 1.0);
+    REQUIRE(animationStateInstance->animationInstance()->totalTime() == 1.0);
+
+    delete animationStateInstance;
+    delete animationState;
+    delete linearAnimation;
+}
+
+TEST_CASE("AnimationStateInstance advances backwards when speed is negative", "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    // For each of these tests, we cons up a dummy artboard/instance
+    // just to make the animations happy.
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 5
+    linearAnimation->duration(10);
+    linearAnimation->fps(2);
+    linearAnimation->loopValue(static_cast<int>(rive::Loop::loop));
+
+    rive::AnimationState* animationState = new rive::AnimationState();
+    animationState->animation(linearAnimation);
+    animationState->speed(-1);
+
+    rive::AnimationStateInstance* animationStateInstance =
+        new rive::AnimationStateInstance(animationState, abi.get());
+
+    // play from beginning.
+    rive::StateMachine machine;
+    rive::StateMachineInstance stateMachineInstance(&machine, abi.get());
+    animationStateInstance->advance(2.0, &stateMachineInstance);
+
+    // backwards 2 seconds from 5.
+    REQUIRE(animationStateInstance->animationInstance()->time() == 3.0);
+    REQUIRE(animationStateInstance->animationInstance()->totalTime() == 2.0);
+
+    delete animationStateInstance;
+    delete animationState;
+    delete linearAnimation;
+}
+
+TEST_CASE("AnimationStateInstance starts a positive animation at the beginning", "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    // For each of these tests, we cons up a dummy artboard/instance
+    // just to make the animations happy.
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 5
+    linearAnimation->duration(10);
+    linearAnimation->fps(2);
+
+    rive::AnimationState* animationState = new rive::AnimationState();
+    animationState->animation(linearAnimation);
+
+    rive::AnimationStateInstance* animationStateInstance =
+        new rive::AnimationStateInstance(animationState, abi.get());
+
+    // backwards 2 seconds from 5.
+    REQUIRE(animationStateInstance->animationInstance()->time() == 0.0);
+
+    delete animationStateInstance;
+    delete animationState;
+    delete linearAnimation;
+}
+
+TEST_CASE("AnimationStateInstance starts a negative animation at the end", "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    // For each of these tests, we cons up a dummy artboard/instance
+    // just to make the animations happy.
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 5
+    linearAnimation->duration(10);
+    linearAnimation->fps(2);
+    linearAnimation->speed(-1);
+
+    rive::AnimationState* animationState = new rive::AnimationState();
+    animationState->animation(linearAnimation);
+
+    rive::AnimationStateInstance* animationStateInstance =
+        new rive::AnimationStateInstance(animationState, abi.get());
+
+    // backwards 2 seconds from 5.
+    REQUIRE(animationStateInstance->animationInstance()->time() == 5.0);
+
+    delete animationStateInstance;
+    delete animationState;
+    delete linearAnimation;
+}
+
+TEST_CASE("AnimationStateInstance with negative speed starts a positive animation at the end",
+          "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    // For each of these tests, we cons up a dummy artboard/instance
+    // just to make the animations happy.
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 5
+    linearAnimation->duration(10);
+    linearAnimation->fps(2);
+
+    rive::AnimationState* animationState = new rive::AnimationState();
+    animationState->speed(-1);
+    animationState->animation(linearAnimation);
+
+    rive::AnimationStateInstance* animationStateInstance =
+        new rive::AnimationStateInstance(animationState, abi.get());
+
+    // backwards 2 seconds from 5.
+    REQUIRE(animationStateInstance->animationInstance()->time() == 5.0);
+
+    delete animationStateInstance;
+    delete animationState;
+    delete linearAnimation;
+}
+
+TEST_CASE("AnimationStateInstance with negative speed starts a negative animation at the beginning",
+          "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    // For each of these tests, we cons up a dummy artboard/instance
+    // just to make the animations happy.
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 5
+    linearAnimation->duration(10);
+    linearAnimation->fps(2);
+    linearAnimation->speed(-1);
+
+    rive::AnimationState* animationState = new rive::AnimationState();
+    animationState->speed(-1);
+    animationState->animation(linearAnimation);
+
+    rive::AnimationStateInstance* animationStateInstance =
+        new rive::AnimationStateInstance(animationState, abi.get());
+
+    // backwards 2 seconds from 5.
+    REQUIRE(animationStateInstance->animationInstance()->time() == 0.0);
+
+    delete animationStateInstance;
+    delete animationState;
+    delete linearAnimation;
+}
+
+TEST_CASE("AnimationStateInstance spilledTime accounts for Nx speed with oneShot", "[animation]")
+{
+
+    rive::NoOpFactory emptyFactory;
+    // For each of these tests, we cons up a dummy artboard/instance
+    // just to make the animations happy.
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::StateMachine machine;
+    rive::StateMachineInstance stateMachineInstance(&machine, abi.get());
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 2
+    linearAnimation->duration(4);
+    linearAnimation->fps(2);
+    linearAnimation->speed(2);
+    linearAnimation->loopValue(static_cast<int>(rive::Loop::oneShot));
+
+    rive::AnimationState* animationState = new rive::AnimationState();
+    animationState->animation(linearAnimation);
+
+    rive::AnimationStateInstance* animationStateInstance =
+        new rive::AnimationStateInstance(animationState, abi.get());
+
+    // play from beginning.
+    animationStateInstance->advance(3.0, &stateMachineInstance);
+
+    REQUIRE(animationStateInstance->animationInstance()->time() == 2.0);
+    REQUIRE(animationStateInstance->animationInstance()->totalTime() == 6.0);
+    // Duration is 2s but at a 2x speed it takes 1s to end
+    // When advancing 3s, there are still 2s remaining (spilled)
+    REQUIRE(animationStateInstance->animationInstance()->spilledTime() == 2.0);
+
+    delete animationStateInstance;
+    delete animationState;
+    delete linearAnimation;
+}
+
+TEST_CASE("AnimationStateInstance spilledTime accounts for 1/Nx speed with oneShot", "[animation]")
+{
+
+    rive::NoOpFactory emptyFactory;
+    // For each of these tests, we cons up a dummy artboard/instance
+    // just to make the animations happy.
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::StateMachine machine;
+    rive::StateMachineInstance stateMachineInstance(&machine, abi.get());
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 2
+    linearAnimation->duration(4);
+    linearAnimation->fps(2);
+    linearAnimation->speed(0.5);
+    linearAnimation->loopValue(static_cast<int>(rive::Loop::oneShot));
+
+    rive::AnimationState* animationState = new rive::AnimationState();
+    animationState->animation(linearAnimation);
+
+    rive::AnimationStateInstance* animationStateInstance =
+        new rive::AnimationStateInstance(animationState, abi.get());
+
+    // play from beginning.
+    animationStateInstance->advance(5.0, &stateMachineInstance);
+
+    REQUIRE(animationStateInstance->animationInstance()->time() == 2.0);
+    REQUIRE(animationStateInstance->animationInstance()->totalTime() == 2.5);
+    // Duration is 2s but at a 0.5x speed it takes 4s to end
+    // When advancing 5.0s, there are still 1s remaining (spilled)
+    REQUIRE(animationStateInstance->animationInstance()->spilledTime() == 1.0);
+
+    delete animationStateInstance;
+    delete animationState;
+    delete linearAnimation;
+}
+
+TEST_CASE("AnimationStateInstance spilledTime accounts for Nx speed with loop", "[animation]")
+{
+
+    rive::NoOpFactory emptyFactory;
+    // For each of these tests, we cons up a dummy artboard/instance
+    // just to make the animations happy.
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::StateMachine machine;
+    rive::StateMachineInstance stateMachineInstance(&machine, abi.get());
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 2
+    linearAnimation->duration(4);
+    linearAnimation->fps(2);
+    linearAnimation->speed(2);
+    linearAnimation->loopValue(static_cast<int>(rive::Loop::loop));
+
+    rive::AnimationState* animationState = new rive::AnimationState();
+    animationState->animation(linearAnimation);
+
+    rive::AnimationStateInstance* animationStateInstance =
+        new rive::AnimationStateInstance(animationState, abi.get());
+
+    // play from beginning.
+    animationStateInstance->advance(5.5, &stateMachineInstance);
+
+    REQUIRE(animationStateInstance->animationInstance()->time() == 1.0);
+    REQUIRE(animationStateInstance->animationInstance()->totalTime() == 11.0);
+    // Duration is 2s but at a 2x speed it takes 1s to loop
+    // When advancing 5.5s, there is still 0.5s remaining (spilled)
+    REQUIRE(animationStateInstance->animationInstance()->spilledTime() == 0.5);
+
+    delete animationStateInstance;
+    delete animationState;
+    delete linearAnimation;
+}
+
+TEST_CASE("AnimationStateInstance spilledTime accounts for 1/Nx speed with loop", "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    // For each of these tests, we cons up a dummy artboard/instance
+    // just to make the animations happy.
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::StateMachine machine;
+    rive::StateMachineInstance stateMachineInstance(&machine, abi.get());
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 2
+    linearAnimation->duration(4);
+    linearAnimation->fps(2);
+    linearAnimation->speed(0.5);
+    linearAnimation->loopValue(static_cast<int>(rive::Loop::loop));
+
+    rive::AnimationState* animationState = new rive::AnimationState();
+    animationState->animation(linearAnimation);
+
+    rive::AnimationStateInstance* animationStateInstance =
+        new rive::AnimationStateInstance(animationState, abi.get());
+
+    // play from beginning.
+    animationStateInstance->advance(10.0, &stateMachineInstance);
+
+    REQUIRE(animationStateInstance->animationInstance()->time() == 1.0);
+    REQUIRE(animationStateInstance->animationInstance()->totalTime() == 5.0);
+    // Duration is 2s but at a 2x speed it takes 1s to loop
+    // When advancing 5.5s, there is still 0.5s remaining (spilled)
+    REQUIRE(animationStateInstance->animationInstance()->spilledTime() == 2.0);
+
+    delete animationStateInstance;
+    delete animationState;
+    delete linearAnimation;
+}
+
+TEST_CASE("AnimationStateInstance spilledTime accounts for -Nx speed with oneShot", "[animation]")
+{
+
+    rive::NoOpFactory emptyFactory;
+    // For each of these tests, we cons up a dummy artboard/instance
+    // just to make the animations happy.
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::StateMachine machine;
+    rive::StateMachineInstance stateMachineInstance(&machine, abi.get());
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 2
+    linearAnimation->duration(4);
+    linearAnimation->fps(2);
+    linearAnimation->speed(-2);
+    linearAnimation->loopValue(static_cast<int>(rive::Loop::oneShot));
+
+    rive::AnimationState* animationState = new rive::AnimationState();
+    animationState->animation(linearAnimation);
+
+    rive::AnimationStateInstance* animationStateInstance =
+        new rive::AnimationStateInstance(animationState, abi.get());
+
+    // play from beginning.
+    animationStateInstance->advance(3.0, &stateMachineInstance);
+
+    REQUIRE(animationStateInstance->animationInstance()->time() == 0.0);
+    REQUIRE(animationStateInstance->animationInstance()->totalTime() == 6.0);
+    // Duration is 2s but at a -2x speed it takes 1s to end
+    // When advancing at negative speed, time starts at duration
+    // so starting at end and taking 1s to complete
+    // there are still 2s remaining (spilled)
+    REQUIRE(animationStateInstance->animationInstance()->spilledTime() == 2.0);
+
+    delete animationStateInstance;
+    delete animationState;
+    delete linearAnimation;
+}
+
+TEST_CASE("AnimationStateInstance spilledTime accounts for -Nx speed with loop", "[animation]")
+{
+
+    rive::NoOpFactory emptyFactory;
+    // For each of these tests, we cons up a dummy artboard/instance
+    // just to make the animations happy.
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::StateMachine machine;
+    rive::StateMachineInstance stateMachineInstance(&machine, abi.get());
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 2
+    linearAnimation->duration(4);
+    linearAnimation->fps(2);
+    linearAnimation->speed(-2);
+    linearAnimation->loopValue(static_cast<int>(rive::Loop::loop));
+
+    rive::AnimationState* animationState = new rive::AnimationState();
+    animationState->animation(linearAnimation);
+
+    rive::AnimationStateInstance* animationStateInstance =
+        new rive::AnimationStateInstance(animationState, abi.get());
+
+    // play from beginning.
+    animationStateInstance->advance(5.5, &stateMachineInstance);
+
+    REQUIRE(animationStateInstance->animationInstance()->time() == 1.0);
+    REQUIRE(animationStateInstance->animationInstance()->totalTime() == 11.0);
+    // Duration is 2s but at a -2x speed it takes 1s to end
+    // When advancing at negative speed, time starts at duration
+    // so starting at end and taking 1s to complete, it loops 5 times
+    // there is still 0.5s remaining (spilled)
+    REQUIRE(animationStateInstance->animationInstance()->spilledTime() == 0.5);
+
+    delete animationStateInstance;
+    delete animationState;
+    delete linearAnimation;
+}
\ No newline at end of file
diff --git a/test/assets/1.webp b/test/assets/1.webp
new file mode 100644
index 0000000..122741b
--- /dev/null
+++ b/test/assets/1.webp
Binary files differ
diff --git a/test/assets/IBMPlexSansArabic-Regular.ttf b/test/assets/IBMPlexSansArabic-Regular.ttf
new file mode 100644
index 0000000..da3eba1
--- /dev/null
+++ b/test/assets/IBMPlexSansArabic-Regular.ttf
Binary files differ
diff --git a/test/assets/LibreBodoni-Italic-VariableFont_wght.ttf b/test/assets/LibreBodoni-Italic-VariableFont_wght.ttf
new file mode 100644
index 0000000..2c0891e
--- /dev/null
+++ b/test/assets/LibreBodoni-Italic-VariableFont_wght.ttf
Binary files differ
diff --git a/test/assets/Montserrat.ttf b/test/assets/Montserrat.ttf
new file mode 100644
index 0000000..656db66
--- /dev/null
+++ b/test/assets/Montserrat.ttf
Binary files differ
diff --git a/test/assets/NotoSansArabic-VariableFont_wdth,wght.ttf b/test/assets/NotoSansArabic-VariableFont_wdth,wght.ttf
new file mode 100644
index 0000000..0356020
--- /dev/null
+++ b/test/assets/NotoSansArabic-VariableFont_wdth,wght.ttf
Binary files differ
diff --git a/test/assets/RobotoFlex.ttf b/test/assets/RobotoFlex.ttf
new file mode 100644
index 0000000..4cf1ecb
--- /dev/null
+++ b/test/assets/RobotoFlex.ttf
Binary files differ
diff --git a/test/assets/align_target.riv b/test/assets/align_target.riv
new file mode 100644
index 0000000..3849239
--- /dev/null
+++ b/test/assets/align_target.riv
Binary files differ
diff --git a/test/assets/animation_reset_cases.riv b/test/assets/animation_reset_cases.riv
new file mode 100644
index 0000000..09e544f
--- /dev/null
+++ b/test/assets/animation_reset_cases.riv
Binary files differ
diff --git a/test/assets/artboardclipping.riv b/test/assets/artboardclipping.riv
new file mode 100644
index 0000000..16768b8
--- /dev/null
+++ b/test/assets/artboardclipping.riv
Binary files differ
diff --git a/test/assets/audio/song.mp3 b/test/assets/audio/song.mp3
new file mode 100644
index 0000000..1a11620
--- /dev/null
+++ b/test/assets/audio/song.mp3
Binary files differ
diff --git a/test/assets/audio/what.wav b/test/assets/audio/what.wav
new file mode 100644
index 0000000..c108b6a
--- /dev/null
+++ b/test/assets/audio/what.wav
Binary files differ
diff --git a/test/assets/background_measure.riv b/test/assets/background_measure.riv
new file mode 100644
index 0000000..7fe885f
--- /dev/null
+++ b/test/assets/background_measure.riv
Binary files differ
diff --git a/test/assets/bad.jpg b/test/assets/bad.jpg
new file mode 100644
index 0000000..27a5d74
--- /dev/null
+++ b/test/assets/bad.jpg
Binary files differ
diff --git a/test/assets/bad.png b/test/assets/bad.png
new file mode 100644
index 0000000..5392a7e
--- /dev/null
+++ b/test/assets/bad.png
Binary files differ
diff --git a/test/assets/bad_skin.riv b/test/assets/bad_skin.riv
new file mode 100644
index 0000000..e5ba5d5
--- /dev/null
+++ b/test/assets/bad_skin.riv
Binary files differ
diff --git a/test/assets/ball_test.riv b/test/assets/ball_test.riv
new file mode 100644
index 0000000..e0e618d
--- /dev/null
+++ b/test/assets/ball_test.riv
Binary files differ
diff --git a/test/assets/blend_test.riv b/test/assets/blend_test.riv
new file mode 100644
index 0000000..18f3f37
--- /dev/null
+++ b/test/assets/blend_test.riv
Binary files differ
diff --git a/test/assets/bullet_man.riv b/test/assets/bullet_man.riv
new file mode 100644
index 0000000..63e2a15
--- /dev/null
+++ b/test/assets/bullet_man.riv
Binary files differ
diff --git a/test/assets/circle_clips.riv b/test/assets/circle_clips.riv
new file mode 100644
index 0000000..62ba568
--- /dev/null
+++ b/test/assets/circle_clips.riv
Binary files differ
diff --git a/test/assets/click_event.riv b/test/assets/click_event.riv
new file mode 100644
index 0000000..583f6b4
--- /dev/null
+++ b/test/assets/click_event.riv
Binary files differ
diff --git a/test/assets/clip_tests.riv b/test/assets/clip_tests.riv
new file mode 100644
index 0000000..2505f3c
--- /dev/null
+++ b/test/assets/clip_tests.riv
Binary files differ
diff --git a/test/assets/complex_ik_dependency.riv b/test/assets/complex_ik_dependency.riv
new file mode 100644
index 0000000..745fbbb
--- /dev/null
+++ b/test/assets/complex_ik_dependency.riv
Binary files differ
diff --git a/test/assets/cubic_value_test.riv b/test/assets/cubic_value_test.riv
new file mode 100644
index 0000000..891a47b
--- /dev/null
+++ b/test/assets/cubic_value_test.riv
Binary files differ
diff --git a/test/assets/death_knight.riv b/test/assets/death_knight.riv
new file mode 100644
index 0000000..153254d
--- /dev/null
+++ b/test/assets/death_knight.riv
Binary files differ
diff --git a/test/assets/dependency_test.riv b/test/assets/dependency_test.riv
new file mode 100644
index 0000000..fedb586
--- /dev/null
+++ b/test/assets/dependency_test.riv
Binary files differ
diff --git a/test/assets/distance_constraint.riv b/test/assets/distance_constraint.riv
new file mode 100644
index 0000000..1daae68
--- /dev/null
+++ b/test/assets/distance_constraint.riv
Binary files differ
diff --git a/test/assets/double_line.riv b/test/assets/double_line.riv
new file mode 100644
index 0000000..ab6bc19
--- /dev/null
+++ b/test/assets/double_line.riv
Binary files differ
diff --git a/test/assets/draw_rule_cycle.riv b/test/assets/draw_rule_cycle.riv
new file mode 100644
index 0000000..380d59a
--- /dev/null
+++ b/test/assets/draw_rule_cycle.riv
Binary files differ
diff --git a/test/assets/ellipsis.riv b/test/assets/ellipsis.riv
new file mode 100644
index 0000000..2691f20
--- /dev/null
+++ b/test/assets/ellipsis.riv
Binary files differ
diff --git a/test/assets/entry.riv b/test/assets/entry.riv
new file mode 100644
index 0000000..22b208f
--- /dev/null
+++ b/test/assets/entry.riv
Binary files differ
diff --git a/test/assets/event_on_listener.riv b/test/assets/event_on_listener.riv
new file mode 100644
index 0000000..6e2000d
--- /dev/null
+++ b/test/assets/event_on_listener.riv
Binary files differ
diff --git a/test/assets/events_on_states.riv b/test/assets/events_on_states.riv
new file mode 100644
index 0000000..ea95f1c
--- /dev/null
+++ b/test/assets/events_on_states.riv
Binary files differ
diff --git a/test/assets/fix_rectangle.riv b/test/assets/fix_rectangle.riv
new file mode 100644
index 0000000..d5319c5
--- /dev/null
+++ b/test/assets/fix_rectangle.riv
Binary files differ
diff --git a/test/assets/follow_path.riv b/test/assets/follow_path.riv
new file mode 100644
index 0000000..606950c
--- /dev/null
+++ b/test/assets/follow_path.riv
Binary files differ
diff --git a/test/assets/follow_path_path_0_opacity.riv b/test/assets/follow_path_path_0_opacity.riv
new file mode 100644
index 0000000..ce4f0fb
--- /dev/null
+++ b/test/assets/follow_path_path_0_opacity.riv
Binary files differ
diff --git a/test/assets/follow_path_with_0_opacity.riv b/test/assets/follow_path_with_0_opacity.riv
new file mode 100644
index 0000000..2948dee
--- /dev/null
+++ b/test/assets/follow_path_with_0_opacity.riv
Binary files differ
diff --git a/test/assets/hello_world.riv b/test/assets/hello_world.riv
new file mode 100644
index 0000000..dfc05c6
--- /dev/null
+++ b/test/assets/hello_world.riv
Binary files differ
diff --git a/test/assets/hit_test_solos.riv b/test/assets/hit_test_solos.riv
new file mode 100644
index 0000000..46619a3
--- /dev/null
+++ b/test/assets/hit_test_solos.riv
Binary files differ
diff --git a/test/assets/hosted_font_file.riv b/test/assets/hosted_font_file.riv
new file mode 100644
index 0000000..ac0882f
--- /dev/null
+++ b/test/assets/hosted_font_file.riv
Binary files differ
diff --git a/test/assets/hosted_image_file.riv b/test/assets/hosted_image_file.riv
new file mode 100644
index 0000000..3c43288
--- /dev/null
+++ b/test/assets/hosted_image_file.riv
Binary files differ
diff --git a/test/assets/in_band_asset.riv b/test/assets/in_band_asset.riv
new file mode 100644
index 0000000..1aad7e4
--- /dev/null
+++ b/test/assets/in_band_asset.riv
Binary files differ
diff --git a/test/assets/jellyfish_test.riv b/test/assets/jellyfish_test.riv
new file mode 100644
index 0000000..37e6664
--- /dev/null
+++ b/test/assets/jellyfish_test.riv
Binary files differ
diff --git a/test/assets/joystick_flag_test.riv b/test/assets/joystick_flag_test.riv
new file mode 100644
index 0000000..2979532
--- /dev/null
+++ b/test/assets/joystick_flag_test.riv
Binary files differ
diff --git a/test/assets/juice.riv b/test/assets/juice.riv
new file mode 100644
index 0000000..5ae8503
--- /dev/null
+++ b/test/assets/juice.riv
Binary files differ
diff --git a/test/assets/layout/layout_center.riv b/test/assets/layout/layout_center.riv
new file mode 100644
index 0000000..4b57961
--- /dev/null
+++ b/test/assets/layout/layout_center.riv
Binary files differ
diff --git a/test/assets/layout/layout_horizontal.riv b/test/assets/layout/layout_horizontal.riv
new file mode 100644
index 0000000..ad9e1b0
--- /dev/null
+++ b/test/assets/layout/layout_horizontal.riv
Binary files differ
diff --git a/test/assets/layout/layout_horizontal_gaps.riv b/test/assets/layout/layout_horizontal_gaps.riv
new file mode 100644
index 0000000..75def70
--- /dev/null
+++ b/test/assets/layout/layout_horizontal_gaps.riv
Binary files differ
diff --git a/test/assets/layout/layout_horizontal_wrap.riv b/test/assets/layout/layout_horizontal_wrap.riv
new file mode 100644
index 0000000..8ecfdbf
--- /dev/null
+++ b/test/assets/layout/layout_horizontal_wrap.riv
Binary files differ
diff --git a/test/assets/layout/layout_vertical.riv b/test/assets/layout/layout_vertical.riv
new file mode 100644
index 0000000..742e001
--- /dev/null
+++ b/test/assets/layout/layout_vertical.riv
Binary files differ
diff --git a/test/assets/layout/measure_tests.riv b/test/assets/layout/measure_tests.riv
new file mode 100644
index 0000000..9d8595e
--- /dev/null
+++ b/test/assets/layout/measure_tests.riv
Binary files differ
diff --git a/test/assets/light_switch.riv b/test/assets/light_switch.riv
new file mode 100644
index 0000000..a779e6b
--- /dev/null
+++ b/test/assets/light_switch.riv
Binary files differ
diff --git a/test/assets/long_name.riv b/test/assets/long_name.riv
new file mode 100644
index 0000000..8dda98b
--- /dev/null
+++ b/test/assets/long_name.riv
Binary files differ
diff --git a/test/assets/looping_timeline_events.riv b/test/assets/looping_timeline_events.riv
new file mode 100644
index 0000000..f4175b3
--- /dev/null
+++ b/test/assets/looping_timeline_events.riv
Binary files differ
diff --git a/test/assets/modifier_test.riv b/test/assets/modifier_test.riv
new file mode 100644
index 0000000..ae071be
--- /dev/null
+++ b/test/assets/modifier_test.riv
Binary files differ
diff --git a/test/assets/modifier_to_run.riv b/test/assets/modifier_to_run.riv
new file mode 100644
index 0000000..feaf3e3
--- /dev/null
+++ b/test/assets/modifier_to_run.riv
Binary files differ
diff --git a/test/assets/multiple_state_machines.riv b/test/assets/multiple_state_machines.riv
new file mode 100644
index 0000000..59f8cf3
--- /dev/null
+++ b/test/assets/multiple_state_machines.riv
Binary files differ
diff --git a/test/assets/nested_artboard_opacity.riv b/test/assets/nested_artboard_opacity.riv
new file mode 100644
index 0000000..128cc95
--- /dev/null
+++ b/test/assets/nested_artboard_opacity.riv
Binary files differ
diff --git a/test/assets/nested_event_test.riv b/test/assets/nested_event_test.riv
new file mode 100644
index 0000000..75c4cc0
--- /dev/null
+++ b/test/assets/nested_event_test.riv
Binary files differ
diff --git a/test/assets/nested_solo.riv b/test/assets/nested_solo.riv
new file mode 100644
index 0000000..981202c
--- /dev/null
+++ b/test/assets/nested_solo.riv
Binary files differ
diff --git a/test/assets/new_text.riv b/test/assets/new_text.riv
new file mode 100644
index 0000000..cf9ffb7
--- /dev/null
+++ b/test/assets/new_text.riv
Binary files differ
diff --git a/test/assets/off_road_car.riv b/test/assets/off_road_car.riv
new file mode 100644
index 0000000..e60182c
--- /dev/null
+++ b/test/assets/off_road_car.riv
Binary files differ
diff --git a/test/assets/oneshotblend.riv b/test/assets/oneshotblend.riv
new file mode 100644
index 0000000..2ed610b
--- /dev/null
+++ b/test/assets/oneshotblend.riv
Binary files differ
diff --git a/test/assets/opaque_hit_test.riv b/test/assets/opaque_hit_test.riv
new file mode 100644
index 0000000..65c0d81
--- /dev/null
+++ b/test/assets/opaque_hit_test.riv
Binary files differ
diff --git a/test/assets/open_source.jpg b/test/assets/open_source.jpg
new file mode 100644
index 0000000..710a4cf
--- /dev/null
+++ b/test/assets/open_source.jpg
Binary files differ
diff --git a/test/assets/out_of_band/eve-317.png b/test/assets/out_of_band/eve-317.png
new file mode 100644
index 0000000..221934a
--- /dev/null
+++ b/test/assets/out_of_band/eve-317.png
Binary files differ
diff --git a/test/assets/out_of_band/walle-370.png b/test/assets/out_of_band/walle-370.png
new file mode 100644
index 0000000..a3ac8ee
--- /dev/null
+++ b/test/assets/out_of_band/walle-370.png
Binary files differ
diff --git a/test/assets/out_of_band/walle.riv b/test/assets/out_of_band/walle.riv
new file mode 100644
index 0000000..bd0edff
--- /dev/null
+++ b/test/assets/out_of_band/walle.riv
Binary files differ
diff --git a/test/assets/placeholder.png b/test/assets/placeholder.png
new file mode 100644
index 0000000..9c3668f
--- /dev/null
+++ b/test/assets/placeholder.png
Binary files differ
diff --git a/test/assets/pointer_events.riv b/test/assets/pointer_events.riv
new file mode 100644
index 0000000..c65bea3
--- /dev/null
+++ b/test/assets/pointer_events.riv
Binary files differ
diff --git a/test/assets/pointer_events_nested_artboards_in_solos.riv b/test/assets/pointer_events_nested_artboards_in_solos.riv
new file mode 100644
index 0000000..ec39738
--- /dev/null
+++ b/test/assets/pointer_events_nested_artboards_in_solos.riv
Binary files differ
diff --git a/test/assets/quantize_test.riv b/test/assets/quantize_test.riv
new file mode 100644
index 0000000..d5f9fb0
--- /dev/null
+++ b/test/assets/quantize_test.riv
Binary files differ
diff --git a/test/assets/rocket.riv b/test/assets/rocket.riv
new file mode 100644
index 0000000..6e9f38d
--- /dev/null
+++ b/test/assets/rocket.riv
Binary files differ
diff --git a/test/assets/rotation_constraint.riv b/test/assets/rotation_constraint.riv
new file mode 100644
index 0000000..88363ff
--- /dev/null
+++ b/test/assets/rotation_constraint.riv
Binary files differ
diff --git a/test/assets/runtime_nested_inputs.riv b/test/assets/runtime_nested_inputs.riv
new file mode 100644
index 0000000..04c2395
--- /dev/null
+++ b/test/assets/runtime_nested_inputs.riv
Binary files differ
diff --git a/test/assets/runtime_nested_text_runs.riv b/test/assets/runtime_nested_text_runs.riv
new file mode 100644
index 0000000..b7535a0
--- /dev/null
+++ b/test/assets/runtime_nested_text_runs.riv
Binary files differ
diff --git a/test/assets/scale_constraint.riv b/test/assets/scale_constraint.riv
new file mode 100644
index 0000000..d4b5f4c
--- /dev/null
+++ b/test/assets/scale_constraint.riv
Binary files differ
diff --git a/test/assets/shapetest.riv b/test/assets/shapetest.riv
new file mode 100644
index 0000000..a0cd2cf
--- /dev/null
+++ b/test/assets/shapetest.riv
Binary files differ
diff --git a/test/assets/solar-system.riv b/test/assets/solar-system.riv
new file mode 100644
index 0000000..acb0b98
--- /dev/null
+++ b/test/assets/solar-system.riv
Binary files differ
diff --git a/test/assets/solo_test.riv b/test/assets/solo_test.riv
new file mode 100644
index 0000000..345e7e6
--- /dev/null
+++ b/test/assets/solo_test.riv
Binary files differ
diff --git a/test/assets/solos_collapse_tests.riv b/test/assets/solos_collapse_tests.riv
new file mode 100644
index 0000000..6d0006f
--- /dev/null
+++ b/test/assets/solos_collapse_tests.riv
Binary files differ
diff --git a/test/assets/solos_with_nested_artboards.riv b/test/assets/solos_with_nested_artboards.riv
new file mode 100644
index 0000000..6c8e64b
--- /dev/null
+++ b/test/assets/solos_with_nested_artboards.riv
Binary files differ
diff --git a/test/assets/sound.riv b/test/assets/sound.riv
new file mode 100644
index 0000000..6178de5
--- /dev/null
+++ b/test/assets/sound.riv
Binary files differ
diff --git a/test/assets/sound2.riv b/test/assets/sound2.riv
new file mode 100644
index 0000000..4f7e9c5
--- /dev/null
+++ b/test/assets/sound2.riv
Binary files differ
diff --git a/test/assets/state_machine_transition.riv b/test/assets/state_machine_transition.riv
new file mode 100644
index 0000000..58fbd62
--- /dev/null
+++ b/test/assets/state_machine_transition.riv
Binary files differ
diff --git a/test/assets/stroke_name_test.riv b/test/assets/stroke_name_test.riv
new file mode 100644
index 0000000..b1ca076
--- /dev/null
+++ b/test/assets/stroke_name_test.riv
Binary files differ
diff --git a/test/assets/tape.riv b/test/assets/tape.riv
new file mode 100644
index 0000000..cd4fd79
--- /dev/null
+++ b/test/assets/tape.riv
Binary files differ
diff --git a/test/assets/test_elastic.riv b/test/assets/test_elastic.riv
new file mode 100644
index 0000000..a60b644
--- /dev/null
+++ b/test/assets/test_elastic.riv
Binary files differ
diff --git a/test/assets/test_modifier_run.riv b/test/assets/test_modifier_run.riv
new file mode 100644
index 0000000..deaab5d
--- /dev/null
+++ b/test/assets/test_modifier_run.riv
Binary files differ
diff --git a/test/assets/timeline_event_test.riv b/test/assets/timeline_event_test.riv
new file mode 100644
index 0000000..cec3239
--- /dev/null
+++ b/test/assets/timeline_event_test.riv
Binary files differ
diff --git a/test/assets/transform_constraint.riv b/test/assets/transform_constraint.riv
new file mode 100644
index 0000000..30f765f
--- /dev/null
+++ b/test/assets/transform_constraint.riv
Binary files differ
diff --git a/test/assets/translation_constraint.riv b/test/assets/translation_constraint.riv
new file mode 100644
index 0000000..0070b0b
--- /dev/null
+++ b/test/assets/translation_constraint.riv
Binary files differ
diff --git a/test/assets/trim.riv b/test/assets/trim.riv
new file mode 100644
index 0000000..bd95852
--- /dev/null
+++ b/test/assets/trim.riv
Binary files differ
diff --git a/test/assets/trim_path_linear.riv b/test/assets/trim_path_linear.riv
new file mode 100644
index 0000000..d07120f
--- /dev/null
+++ b/test/assets/trim_path_linear.riv
Binary files differ
diff --git a/test/assets/two_artboards.riv b/test/assets/two_artboards.riv
new file mode 100644
index 0000000..eda06e7
--- /dev/null
+++ b/test/assets/two_artboards.riv
Binary files differ
diff --git a/test/assets/two_bone_ik.riv b/test/assets/two_bone_ik.riv
new file mode 100644
index 0000000..c227124
--- /dev/null
+++ b/test/assets/two_bone_ik.riv
Binary files differ
diff --git a/test/assets/walle.riv b/test/assets/walle.riv
new file mode 100644
index 0000000..248193a
--- /dev/null
+++ b/test/assets/walle.riv
Binary files differ
diff --git a/test/assets/zombie_skins.riv b/test/assets/zombie_skins.riv
new file mode 100644
index 0000000..ea9c8e2
--- /dev/null
+++ b/test/assets/zombie_skins.riv
Binary files differ
diff --git a/test/audio_test.cpp b/test/audio_test.cpp
new file mode 100644
index 0000000..734262a
--- /dev/null
+++ b/test/audio_test.cpp
@@ -0,0 +1,233 @@
+#include "rive/audio/audio_engine.hpp"
+#include "rive/audio/audio_source.hpp"
+#include "rive/audio/audio_sound.hpp"
+#include "rive/audio/audio_reader.hpp"
+#include "rive/audio_event.hpp"
+#include "rive/assets/audio_asset.hpp"
+#include "rive_file_reader.hpp"
+#include "catch.hpp"
+#include <string>
+
+using namespace rive;
+
+TEST_CASE("audio engine initializes", "[audio]")
+{
+    rcp<AudioEngine> engine = AudioEngine::Make(2, 44100);
+    REQUIRE(engine != nullptr);
+}
+
+static std::vector<uint8_t> loadFile(const char* filename)
+{
+    FILE* fp = fopen(filename, "rb");
+    REQUIRE(fp != nullptr);
+
+    fseek(fp, 0, SEEK_END);
+    const size_t length = ftell(fp);
+    fseek(fp, 0, SEEK_SET);
+    std::vector<uint8_t> bytes(length);
+    REQUIRE(fread(bytes.data(), 1, length, fp) == length);
+    fclose(fp);
+
+    return bytes;
+}
+
+TEST_CASE("audio source can be opened", "[audio]")
+{
+    rcp<AudioEngine> engine = AudioEngine::Make(2, 44100);
+    REQUIRE(engine != nullptr);
+    auto file = loadFile("../../test/assets/audio/what.wav");
+    auto span = Span<uint8_t>(file);
+    rcp<AudioSource> audioSource = rcp<AudioSource>(new AudioSource(span));
+    REQUIRE(audioSource->channels() == 2);
+    REQUIRE(audioSource->sampleRate() == 44100);
+
+    // Try some different sample rates.
+    {
+        auto reader = audioSource->makeReader(2, 44100);
+        REQUIRE(reader != nullptr);
+        REQUIRE(reader->lengthInFrames() == 9688);
+    }
+    {
+        auto reader = audioSource->makeReader(1, 48000);
+        REQUIRE(reader != nullptr);
+        REQUIRE(reader->lengthInFrames() == 10544);
+    }
+    {
+        auto reader = audioSource->makeReader(2, 32000);
+        REQUIRE(reader != nullptr);
+        REQUIRE(reader->lengthInFrames() == 7029);
+    }
+
+    float channels[2] = {0, 0};
+    engine->initLevelMonitor();
+    engine->levels(Span<float>(&channels[0], 2));
+    REQUIRE(channels[0] == 0);
+    REQUIRE(channels[1] == 0);
+
+    auto sound = engine->play(audioSource, 0, 0, 0);
+    float frames[512 * 2] = {};
+    engine->readAudioFrames(frames, 512);
+    engine->levels(Span<float>(&channels[0], 2));
+    REQUIRE(channels[0] != 0);
+    REQUIRE(channels[1] != 0);
+
+    engine->readAudioFrames(frames, 512);
+    REQUIRE(engine->level(0) != 0);
+    REQUIRE(engine->level(1) != 0);
+}
+
+TEST_CASE("file with audio loads correctly", "[text]")
+{
+    auto file = ReadRiveFile("../../test/assets/sound.riv");
+    auto artboard = file->artboard();
+    REQUIRE(artboard != nullptr);
+
+    auto audioEvents = artboard->find<AudioEvent>();
+    REQUIRE(audioEvents.size() == 1);
+
+    auto audioEvent = audioEvents[0];
+    REQUIRE(audioEvent->asset() != nullptr);
+    REQUIRE(audioEvent->asset()->hasAudioSource());
+
+    // auto textObjects = artboard->find<rive::Text>();
+    // REQUIRE(textObjects.size() == 5);
+
+    // auto styleObjects = artboard->find<rive::TextStyle>();
+    // REQUIRE(styleObjects.size() == 13);
+
+    // auto runObjects = artboard->find<rive::TextValueRun>();
+    // REQUIRE(runObjects.size() == 22);
+
+    // artboard->advance(0.0f);
+    // rive::NoOpRenderer renderer;
+    // artboard->draw(&renderer);
+}
+
+TEST_CASE("audio sound can outlive engine", "[audio]")
+{
+    rcp<AudioSound> sound;
+    {
+        rcp<AudioEngine> engine = AudioEngine::Make(2, 44100);
+        REQUIRE(engine != nullptr);
+        auto file = loadFile("../../test/assets/audio/what.wav");
+        auto span = Span<uint8_t>(file);
+        rcp<AudioSource> audioSource = rcp<AudioSource>(new AudioSource(span));
+        REQUIRE(audioSource->channels() == 2);
+        REQUIRE(audioSource->sampleRate() == 44100);
+
+        sound = engine->play(audioSource, 0, 0, 0);
+        float frames[512 * 2] = {};
+        engine->readAudioFrames(frames, 512);
+    }
+    sound->stop();
+}
+
+TEST_CASE("many audio sounds can outlive engine", "[audio]")
+{
+    std::vector<rcp<AudioSound>> sounds;
+    {
+        rcp<AudioEngine> engine = AudioEngine::Make(2, 44100);
+        REQUIRE(engine != nullptr);
+        auto file = loadFile("../../test/assets/audio/what.wav");
+        auto span = Span<uint8_t>(file);
+        rcp<AudioSource> audioSource = rcp<AudioSource>(new AudioSource(span));
+        REQUIRE(audioSource->channels() == 2);
+        REQUIRE(audioSource->sampleRate() == 44100);
+
+        for (int i = 0; i < 20; i++)
+        {
+            sounds.emplace_back(engine->play(audioSource, 0, 0, 0));
+        }
+        float frames[512 * 2] = {};
+        engine->readAudioFrames(frames, 512);
+    }
+    for (auto sound : sounds)
+    {
+        sound->stop();
+    }
+}
+
+TEST_CASE("audio sounds from different artboards stop accordingly", "[audio]")
+{
+    rcp<AudioEngine> engine = AudioEngine::Make(2, 44100);
+
+    auto file = ReadRiveFile("../../test/assets/sound.riv");
+    auto artboard = file->artboardDefault();
+    artboard->audioEngine(engine);
+    auto artboard2 = file->artboardDefault();
+    artboard2->audioEngine(engine);
+
+    REQUIRE(artboard != nullptr);
+
+    auto audioEvents = artboard->find<AudioEvent>();
+    REQUIRE(audioEvents.size() == 1);
+
+    auto audioEvent = audioEvents[0];
+    REQUIRE(audioEvent->asset() != nullptr);
+    REQUIRE(audioEvent->asset()->hasAudioSource());
+
+    audioEvent->play();
+    audioEvent->play();
+    REQUIRE(engine->playingSoundCount() == 2);
+    auto audioEvent2 = artboard2->find<AudioEvent>()[0];
+    audioEvent2->play();
+    REQUIRE(engine->playingSoundCount() == 3);
+    audioEvent->play();
+    REQUIRE(engine->playingSoundCount() == 4);
+
+    // The three playing sounds owned by the first artboard should now stop.
+    artboard = nullptr;
+
+    REQUIRE(engine->playingSoundCount() == 1);
+
+    // The last one belonging to artboard2 should now stop too.
+    artboard2 = nullptr;
+    REQUIRE(engine->playingSoundCount() == 0);
+}
+
+TEST_CASE("Artboard has audio", "[audio]")
+{
+    rcp<AudioEngine> engine = AudioEngine::Make(2, 44100);
+
+    auto file = ReadRiveFile("../../test/assets/sound2.riv");
+    auto artboard = file->artboardNamed("child");
+    artboard->audioEngine(engine);
+
+    REQUIRE(artboard != nullptr);
+
+    auto audioEvents = artboard->find<AudioEvent>();
+    REQUIRE(audioEvents.size() == 1);
+    REQUIRE(artboard->hasAudio() == true);
+}
+
+TEST_CASE("Artboard has audio in nested artboard", "[audio]")
+{
+    rcp<AudioEngine> engine = AudioEngine::Make(2, 44100);
+
+    auto file = ReadRiveFile("../../test/assets/sound2.riv");
+    auto artboard = file->artboardNamed("grand-parent");
+    artboard->audioEngine(engine);
+
+    REQUIRE(artboard != nullptr);
+
+    auto audioEvents = artboard->find<AudioEvent>();
+    REQUIRE(audioEvents.size() == 0);
+    REQUIRE(artboard->hasAudio() == true);
+}
+
+TEST_CASE("Artboard does not have audio", "[audio]")
+{
+    rcp<AudioEngine> engine = AudioEngine::Make(2, 44100);
+
+    auto file = ReadRiveFile("../../test/assets/sound2.riv");
+    auto artboard = file->artboardNamed("no-audio");
+    artboard->audioEngine(engine);
+
+    REQUIRE(artboard != nullptr);
+
+    auto audioEvents = artboard->find<AudioEvent>();
+    REQUIRE(audioEvents.size() == 0);
+    REQUIRE(artboard->hasAudio() == false);
+}
+
+// TODO check if sound->stop calls completed callback!!!
\ No newline at end of file
diff --git a/test/binary_reader_test.cpp b/test/binary_reader_test.cpp
new file mode 100644
index 0000000..da924fb
--- /dev/null
+++ b/test/binary_reader_test.cpp
@@ -0,0 +1,68 @@
+#include <catch.hpp>
+#include <rive/core/binary_reader.hpp>
+
+template <typename T> void checkFits()
+{
+    int64_t min = std::numeric_limits<T>::min();
+    int64_t max = std::numeric_limits<T>::max();
+    REQUIRE(rive::fitsIn<T>(max + 0));
+    REQUIRE(rive::fitsIn<T>(min - 0));
+    REQUIRE(!rive::fitsIn<T>(max + 1));
+    REQUIRE(!rive::fitsIn<T>(min - 1));
+}
+
+TEST_CASE("fitsIn checks", "[type_conversions]")
+{
+    checkFits<int8_t>();
+    checkFits<uint8_t>();
+
+    checkFits<int16_t>();
+    checkFits<uint16_t>();
+
+    checkFits<int32_t>();
+    checkFits<uint32_t>();
+}
+
+static uint8_t* packvarint(uint8_t array[], uint64_t value)
+{
+    while (value > 127)
+    {
+        *array++ = static_cast<uint8_t>(0x80 | (value & 0x7F));
+        value >>= 7;
+    }
+    *array++ = static_cast<uint8_t>(value);
+    return array;
+}
+
+template <typename T> bool checkAs(uint64_t value)
+{
+    uint8_t storage[16];
+    uint8_t* p = storage;
+
+    p = packvarint(storage, value);
+    rive::BinaryReader reader(rive::make_span(storage, p - storage));
+
+    auto newValue = reader.readVarUintAs<T>();
+
+    if (reader.hasError())
+    {
+        REQUIRE(newValue == 0);
+    }
+
+    return !reader.hasError() && value == newValue;
+}
+
+TEST_CASE("range checks", "[binary_reader]")
+{
+    REQUIRE(checkAs<uint8_t>(100));
+    REQUIRE(checkAs<uint16_t>(100));
+    REQUIRE(checkAs<uint32_t>(100));
+
+    REQUIRE(!checkAs<uint8_t>(1000));
+    REQUIRE(checkAs<uint16_t>(1000));
+    REQUIRE(checkAs<uint32_t>(1000));
+
+    REQUIRE(!checkAs<uint8_t>(100000));
+    REQUIRE(!checkAs<uint16_t>(100000));
+    REQUIRE(checkAs<uint32_t>(100000));
+}
diff --git a/test/bound_bones_test.cpp b/test/bound_bones_test.cpp
new file mode 100644
index 0000000..9362d38
--- /dev/null
+++ b/test/bound_bones_test.cpp
@@ -0,0 +1,37 @@
+#include <rive/bones/skin.hpp>
+#include <rive/bones/tendon.hpp>
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/shapes/clipping_shape.hpp>
+#include <rive/shapes/path_vertex.hpp>
+#include <rive/shapes/points_path.hpp>
+#include <rive/shapes/rectangle.hpp>
+#include <rive/shapes/shape.hpp>
+#include "utils/no_op_factory.hpp"
+#include "rive_file_reader.hpp"
+#include <catch.hpp>
+#include <cstdio>
+
+TEST_CASE("bound bones load correctly", "[bones]")
+{
+    auto file = ReadRiveFile("../../test/assets/off_road_car.riv");
+
+    auto node = file->artboard()->find("transmission_front_testing");
+    REQUIRE(node != nullptr);
+    REQUIRE(node->is<rive::Shape>());
+    REQUIRE(node->as<rive::Shape>()->paths().size() == 1);
+    auto path = node->as<rive::Shape>()->paths()[0];
+    REQUIRE(path->is<rive::PointsPath>());
+    auto pointsPath = path->as<rive::PointsPath>();
+    REQUIRE(pointsPath->skin() != nullptr);
+    REQUIRE(pointsPath->skin()->tendons().size() == 2);
+    REQUIRE(pointsPath->skin()->tendons()[0]->bone() != nullptr);
+    REQUIRE(pointsPath->skin()->tendons()[1]->bone() != nullptr);
+
+    for (auto vertex : path->vertices())
+    {
+        REQUIRE(vertex->weight() != nullptr);
+    }
+
+    // Ok seems like bones are set up ok.
+}
diff --git a/test/bounds_test.cpp b/test/bounds_test.cpp
new file mode 100644
index 0000000..99afa1e
--- /dev/null
+++ b/test/bounds_test.cpp
@@ -0,0 +1,48 @@
+#include "rive/file.hpp"
+#include "rive/node.hpp"
+#include "rive/shapes/shape.hpp"
+#include "rive/math/transform_components.hpp"
+#include "rive/text/text_value_run.hpp"
+#include "utils/no_op_renderer.hpp"
+#include "rive_file_reader.hpp"
+#include "rive_testing.hpp"
+#include <cstdio>
+
+TEST_CASE("compute bounds of background shape", "[bounds]")
+{
+    auto file = ReadRiveFile("../../test/assets/background_measure.riv");
+
+    auto artboard = file->artboard();
+
+    REQUIRE(artboard->find<rive::Shape>("background") != nullptr);
+    auto background = artboard->find<rive::Shape>("background");
+    REQUIRE(artboard->find<rive::TextValueRun>("nameRun") != nullptr);
+    auto name = artboard->find<rive::TextValueRun>("nameRun");
+    artboard->advance(0.0f);
+
+    auto bounds = background->computeWorldBounds();
+    CHECK(bounds.width() == Approx(42.010925f));
+    CHECK(bounds.height() == Approx(29.995453f));
+
+    // Change the text and verify the bounds extended further.
+    name->text("much much longer");
+    artboard->advance(0.0f);
+
+    bounds = background->computeWorldBounds();
+    CHECK(bounds.width() == Approx(138.01093f));
+    CHECK(bounds.height() == Approx(29.995453f));
+
+    // Apply a transform to the whole artboard.
+    rive::Mat2D& world = artboard->mutableWorldTransform();
+    world.scaleByValues(0.5f, 0.5f);
+    artboard->markWorldTransformDirty();
+    artboard->advance(0.0f);
+
+    bounds = background->computeWorldBounds();
+    CHECK(bounds.width() == Approx(138.01093f / 2.0f));
+    CHECK(bounds.height() == Approx(29.995453f / 2.0f));
+
+    bounds = background->computeLocalBounds();
+    CHECK(bounds.width() == Approx(138.01093f));
+    CHECK(bounds.height() == Approx(29.995453f));
+}
diff --git a/test/cdn_asset_test.cpp b/test/cdn_asset_test.cpp
new file mode 100644
index 0000000..60609a5
--- /dev/null
+++ b/test/cdn_asset_test.cpp
@@ -0,0 +1,53 @@
+#include <rive/bones/skin.hpp>
+#include <rive/bones/tendon.hpp>
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/shapes/clipping_shape.hpp>
+#include <rive/shapes/path_vertex.hpp>
+#include <rive/shapes/points_path.hpp>
+#include <rive/shapes/rectangle.hpp>
+#include <rive/shapes/shape.hpp>
+#include <rive/assets/file_asset.hpp>
+#include <rive/assets/image_asset.hpp>
+#include <rive/assets/font_asset.hpp>
+#include "utils/no_op_factory.hpp"
+#include "rive_file_reader.hpp"
+#include <catch.hpp>
+#include <cstdio>
+
+TEST_CASE("Image assets with cdn information loads correctly", "[cdn]")
+{
+    auto file = ReadRiveFile("../../test/assets/hosted_image_file.riv");
+
+    auto assets = file->assets();
+    REQUIRE(assets.size() == 1);
+    auto firstAsset = assets[0];
+    REQUIRE(firstAsset->is<rive::ImageAsset>());
+
+    // this is a 16 byte uuid, any good ideas on how to manage this test?
+    // we could convert it to a string for the getter...
+    REQUIRE(firstAsset->cdnUuid().size() == 16);
+    REQUIRE(strcmp("edcb1816-8405-4983-acd2-16db48d85df4", firstAsset->cdnUuidStr().c_str()) == 0);
+    REQUIRE(firstAsset->cdnBaseUrl() == "https://public.uat.rive.app/cdn/uuid");
+
+    REQUIRE(firstAsset->uniqueFilename() == "one-45008.png");
+    REQUIRE(firstAsset->fileExtension() == "png");
+}
+
+TEST_CASE("Font assets with cdn information loads correctly", "[cdn]")
+{
+    auto file = ReadRiveFile("../../test/assets/hosted_font_file.riv");
+
+    auto assets = file->assets();
+    REQUIRE(assets.size() == 1);
+    auto firstAsset = assets[0];
+    REQUIRE(firstAsset->is<rive::FontAsset>());
+
+    // this is a 16 byte uuid, any good ideas on how to manage this test?
+    // we could convert it to a string for the getter...
+    REQUIRE(firstAsset->cdnUuid().size() == 16);
+    REQUIRE(firstAsset->cdnBaseUrl() == "https://public.uat.rive.app/cdn/uuid");
+
+    REQUIRE(firstAsset->uniqueFilename() == "Inter-43276.ttf");
+    REQUIRE(firstAsset->fileExtension() == "ttf");
+}
diff --git a/test/clip_test.cpp b/test/clip_test.cpp
new file mode 100644
index 0000000..19ff144
--- /dev/null
+++ b/test/clip_test.cpp
@@ -0,0 +1,151 @@
+#include <rive/clip_result.hpp>
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/shapes/clipping_shape.hpp>
+#include <rive/shapes/rectangle.hpp>
+#include <rive/shapes/shape.hpp>
+#include <utils/no_op_renderer.hpp>
+#include "rive_file_reader.hpp"
+#include <catch.hpp>
+
+TEST_CASE("clipping loads correctly", "[clipping]")
+{
+    auto file = ReadRiveFile("../../test/assets/circle_clips.riv");
+
+    auto node = file->artboard()->find("TopEllipse");
+    REQUIRE(node != nullptr);
+    REQUIRE(node->is<rive::Shape>());
+
+    auto shape = node->as<rive::Shape>();
+    REQUIRE(shape->clippingShapes().size() == 2);
+    REQUIRE(shape->clippingShapes()[0]->source()->name() == "ClipRect2");
+    REQUIRE(shape->clippingShapes()[1]->source()->name() == "BabyEllipse");
+
+    file->artboard()->updateComponents();
+
+    rive::NoOpRenderer renderer;
+    file->artboard()->draw(&renderer);
+}
+
+class ClipTestRenderPath : public rive::RenderPath
+{
+public:
+    rive::RawPath rawPath;
+    ClipTestRenderPath(rive::RawPath& path) : rawPath(path) {}
+
+    void rewind() override {}
+
+    void fillRule(rive::FillRule value) override {}
+    void addPath(rive::CommandPath* path, const rive::Mat2D& transform) override {}
+    void addRenderPath(rive::RenderPath* path, const rive::Mat2D& transform) override {}
+
+    void moveTo(float x, float y) override {}
+    void lineTo(float x, float y) override {}
+    void cubicTo(float ox, float oy, float ix, float iy, float x, float y) override {}
+    void close() override {}
+};
+
+class ClippingFactory : public rive::NoOpFactory
+{
+    rive::rcp<rive::RenderPath> makeRenderPath(rive::RawPath& rawPath, rive::FillRule) override
+    {
+        return rive::make_rcp<ClipTestRenderPath>(rawPath);
+    }
+};
+
+TEST_CASE("artboard is clipped correctly", "[clipping]")
+{
+    ClippingFactory factory;
+    auto file = ReadRiveFile("../../test/assets/artboardclipping.riv", &factory);
+
+    auto artboard = file->artboard("Center");
+    REQUIRE(artboard != nullptr);
+    artboard->advance(0.0f);
+    REQUIRE(artboard->originX() == 0.5);
+    REQUIRE(artboard->originY() == 0.5);
+    {
+        auto clipPath = static_cast<ClipTestRenderPath*>(artboard->clipPath());
+        auto points = clipPath->rawPath.points();
+        REQUIRE(points.size() == 4);
+
+        REQUIRE(points[0] == rive::Vec2D(0.0f, 0.0f));
+        REQUIRE(points[1] == rive::Vec2D(500.0f, 0.0f));
+        REQUIRE(points[2] == rive::Vec2D(500.0f, 500.0f));
+        REQUIRE(points[3] == rive::Vec2D(0.0f, 500.0f));
+    }
+    // Now disable framing the origin so that the points are in origin space.
+    artboard->frameOrigin(false);
+    artboard->updateComponents();
+    {
+        auto clipPath = static_cast<ClipTestRenderPath*>(artboard->clipPath());
+        auto points = clipPath->rawPath.points();
+        REQUIRE(points.size() == 4);
+
+        REQUIRE(points[0] == rive::Vec2D(-250.0f, -250.0f));
+        REQUIRE(points[1] == rive::Vec2D(250.0f, -250.0f));
+        REQUIRE(points[2] == rive::Vec2D(250.0f, 250.0f));
+        REQUIRE(points[3] == rive::Vec2D(-250.0f, 250.0f));
+    }
+}
+
+TEST_CASE("Shape does not have any clipping paths visible", "[clipping]")
+{
+    ClippingFactory factory;
+    auto file = ReadRiveFile("../../test/assets/clip_tests.riv", &factory);
+
+    auto artboard = file->artboard("Empty-Shape");
+    REQUIRE(artboard != nullptr);
+    artboard->updateComponents();
+    auto node = artboard->find("Ellipse-clipper");
+    REQUIRE(node != nullptr);
+    REQUIRE(node->is<rive::Shape>());
+    rive::Shape* shape = static_cast<rive::Shape*>(node);
+    REQUIRE(shape->isEmpty() == true);
+    auto clippedNode = artboard->find("Rectangle-clipped");
+    REQUIRE(clippedNode != nullptr);
+    REQUIRE(clippedNode->is<rive::Shape>());
+    rive::Shape* clippedShape = static_cast<rive::Shape*>(clippedNode);
+    rive::NoOpRenderer renderer;
+    auto clipResult = clippedShape->applyClip(&renderer);
+    REQUIRE(clipResult == rive::ClipResult::emptyClip);
+}
+
+TEST_CASE("Shape has at least a clipping path visible", "[clipping]")
+{
+    ClippingFactory factory;
+    auto file = ReadRiveFile("../../test/assets/clip_tests.riv", &factory);
+
+    auto artboard = file->artboard("Hidden-Path-Visible-Path");
+    REQUIRE(artboard != nullptr);
+    artboard->updateComponents();
+    auto node = artboard->find("Ellipse-clipper");
+    REQUIRE(node != nullptr);
+    REQUIRE(node->is<rive::Shape>());
+    rive::Shape* shape = static_cast<rive::Shape*>(node);
+    REQUIRE(shape->isEmpty() == false);
+    auto clippedNode = artboard->find("Rectangle-clipped");
+    REQUIRE(clippedNode != nullptr);
+    REQUIRE(clippedNode->is<rive::Shape>());
+    rive::Shape* clippedShape = static_cast<rive::Shape*>(clippedNode);
+    rive::NoOpRenderer renderer;
+    auto clipResult = clippedShape->applyClip(&renderer);
+    REQUIRE(clipResult == rive::ClipResult::clip);
+}
+
+TEST_CASE("Shape returns an empty clip when one clipping shape is empty", "[clipping]")
+{
+    ClippingFactory factory;
+    auto file = ReadRiveFile("../../test/assets/clip_tests.riv", &factory);
+
+    auto artboard = file->artboard("One-Clipping-Shape-Visible-One-Hidden");
+    REQUIRE(artboard != nullptr);
+    artboard->updateComponents();
+    auto node = artboard->find("Rectangle-clipped");
+    REQUIRE(node != nullptr);
+    REQUIRE(node->is<rive::Shape>());
+    rive::Shape* shape = static_cast<rive::Shape*>(node);
+
+    rive::NoOpRenderer renderer;
+    auto clipResult = shape->applyClip(&renderer);
+    REQUIRE(clipResult == rive::ClipResult::emptyClip);
+}
diff --git a/test/color_test.cpp b/test/color_test.cpp
new file mode 100644
index 0000000..3105ec8
--- /dev/null
+++ b/test/color_test.cpp
@@ -0,0 +1,43 @@
+#include "rive/shapes/paint/color.hpp"
+
+#include <catch.hpp>
+
+TEST_CASE("unpacking", "[color]")
+{
+    rive::ColorInt color = 0x12345678;
+    uint8_t rgba[4];
+    rive::UnpackColorToRGBA8(color, rgba);
+    CHECK(rgba[0] == rive::colorRed(color));
+    CHECK(rgba[1] == rive::colorGreen(color));
+    CHECK(rgba[2] == rive::colorBlue(color));
+    CHECK(rgba[3] == rive::colorAlpha(color));
+
+    float color4f[4];
+    rive::UnpackColorToRGBA32F(color, color4f);
+    CHECK(color4f[0] == Approx(rive::colorRed(color) / 255.f));
+    CHECK(color4f[1] == Approx(rive::colorGreen(color) / 255.f));
+    CHECK(color4f[2] == Approx(rive::colorBlue(color) / 255.f));
+    CHECK(color4f[3] == Approx(rive::colorAlpha(color) / 255.f));
+
+    float color4fPremul[4];
+    rive::UnpackColorToRGBA32FPremul(color, color4fPremul);
+    CHECK(color4fPremul[0] == Approx(color4f[0] * color4f[3]));
+    CHECK(color4fPremul[1] == Approx(color4f[1] * color4f[3]));
+    CHECK(color4fPremul[2] == Approx(color4f[2] * color4f[3]));
+    CHECK(color4fPremul[3] == Approx(color4f[3]));
+}
+
+TEST_CASE("color lerp", "[color]")
+{
+    // Lerping this color with a mix value > 1 returns a negative value
+    // that overflows the unsigned int
+    // If this is not clamped correctly, the result would be off.
+    rive::ColorInt colorFrom = 0x90909090;
+    rive::ColorInt colorTo = 0x1E1E1E1E;
+    float mix = 1.3f;
+    rive::ColorInt colorLerped = rive::colorLerp(colorFrom, colorTo, mix);
+    CHECK(rive::colorRed(colorLerped) == 0);
+    CHECK(rive::colorGreen(colorLerped) == 0);
+    CHECK(rive::colorBlue(colorLerped) == 0);
+    CHECK(rive::colorAlpha(colorLerped) == 0);
+}
diff --git a/test/contour_measure_test.cpp b/test/contour_measure_test.cpp
new file mode 100644
index 0000000..3548366
--- /dev/null
+++ b/test/contour_measure_test.cpp
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include <rive/math/contour_measure.hpp>
+#include <rive/math/math_types.hpp>
+#include <rive/math/raw_path.hpp>
+#include <rive/math/vec2d.hpp>
+
+#include "rive_file_reader.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+
+#include <catch.hpp>
+#include <cstdio>
+
+using namespace rive;
+
+static bool nearly_eq(float a, float b, float tolerance)
+{
+    assert(tolerance >= 0);
+    const float diff = std::abs(a - b);
+    const float max = std::max(std::abs(a), std::abs(b));
+    const float allowed = tolerance * max;
+    if (diff > allowed)
+    {
+        printf("%g %g delta %g allowed %g\n", a, b, diff, allowed);
+        return false;
+    }
+    return true;
+}
+
+static bool nearly_eq(Vec2D a, Vec2D b, float tol)
+{
+    return nearly_eq(a.x, b.x, tol) && nearly_eq(a.y, b.y, tol);
+}
+
+TEST_CASE("contour-basics", "[contourmeasure]")
+{
+    const float tol = 0.000001f;
+
+    RawPath path;
+    ContourMeasureIter iter(&path);
+    REQUIRE(iter.next() == nullptr);
+
+    path.moveTo(1, 2);
+    iter.rewind(&path);
+    REQUIRE(iter.next() == nullptr);
+
+    path.lineTo(4, 6);
+    iter.rewind(&path);
+    auto cm = iter.next();
+    REQUIRE(cm);
+    REQUIRE(nearly_eq(cm->length(), 5, tol));
+    REQUIRE(iter.next() == nullptr);
+
+    // check the mid-points of a rectangle
+
+    path = RawPath();
+    const float w = 4, h = 6;
+    path.addRect({0, 0, w, h}, PathDirection::cw);
+    iter.rewind(&path);
+    cm = iter.next();
+    REQUIRE(cm);
+    REQUIRE(nearly_eq(cm->length(), 2 * (w + h), tol));
+    const float midDistances[] = {
+        w / 2,
+        w + h / 2,
+        w + h + w / 2,
+        w + h + w + h / 2,
+    };
+    const ContourMeasure::PosTan midPoints[] = {
+        {{w / 2, 0}, {1, 0}},
+        {{w, h / 2}, {0, 1}},
+        {{w / 2, h}, {-1, 0}},
+        {{0, h / 2}, {0, -1}},
+    };
+    for (int i = 0; i < 4; ++i)
+    {
+        auto rec = cm->getPosTan(midDistances[i]);
+        REQUIRE(nearly_eq(rec.pos, midPoints[i].pos, tol));
+        REQUIRE(nearly_eq(rec.tan, midPoints[i].tan, tol));
+    }
+    REQUIRE(iter.next() == nullptr);
+}
+
+TEST_CASE("multi-contours", "[contourmeasure]")
+{
+    const Vec2D pts[] = {
+        {0, 0},
+        {3, 0},
+        {3, 4},
+    };
+    auto span = make_span(pts, sizeof(pts) / sizeof(pts[0]));
+
+    // We expect 3 measurable contours out of this: 7, 16, 7
+    // the others should be skipped since they are empty (len == 0)
+
+    RawPath path;
+    path.addPoly(span, false); // len == 7
+
+    path.addPoly(span, true); // len == 12
+
+    // should be skipped (lengh == 0)
+    path.moveTo(0, 0);
+
+    // should be skipped (lengh == 0)
+    path.moveTo(0, 0);
+    path.close();
+
+    // should be skipped (lengh == 0)
+    path.moveTo(0, 0);
+    path.lineTo(0, 0);
+
+    // should be skipped (lengh == 0)
+    path.moveTo(0, 0);
+    path.lineTo(0, 0);
+    path.close();
+
+    path.addPoly(span, false); // len == 7
+
+    ContourMeasureIter iter(&path);
+    auto cm = iter.next();
+    REQUIRE(cm->length() == 7);
+    cm = iter.next();
+    REQUIRE(cm->length() == 12);
+    cm = iter.next();
+    REQUIRE(cm->length() == 7);
+    cm = iter.next();
+    REQUIRE(!cm);
+}
+
+TEST_CASE("contour-oval", "[contourmeasure]")
+{
+    const float tol = 0.0075f;
+
+    const float r = 10;
+    RawPath path;
+    path.addOval({-r, -r, r, r}, PathDirection::cw);
+    ContourMeasureIter iter(&path, tol);
+
+    auto cm = iter.next();
+    REQUIRE(nearly_eq(cm->length(), 2 * r * math::PI, tol));
+    REQUIRE(!iter.next());
+}
+
+TEST_CASE("bad contour", "[contourmeasure]")
+{
+    auto file = ReadRiveFile("../../test/assets/zombie_skins.riv");
+
+    auto artboard = file->artboard()->instance();
+    REQUIRE(artboard != nullptr);
+    auto machine = artboard->defaultStateMachine();
+    machine->advanceAndApply(0.0f);
+}
+
+// NaN paths don't return contours.
+TEST_CASE("nan-path", "[contourmeasure]")
+{
+    RawPath path;
+    path.lineTo(1, 2);
+    path.cubicTo(3, 4, 5, 6, 7, 8);
+    path.cubicTo(9, 10, 11, 12, 13, 14);
+    path.cubicTo(15, 16, 17, 18, 19, 20);
+
+    {
+        ContourMeasureIter iter(&path);
+        auto cm = iter.next();
+        CHECK(cm != nullptr);
+        CHECK(std::isfinite(cm->length()));
+        CHECK(iter.next() == nullptr);
+    }
+
+    {
+        auto nan = std::numeric_limits<float>::quiet_NaN();
+        RawPath path_ = path.transform(Mat2D(nan, nan, nan, nan, nan, nan));
+        ContourMeasureIter iter(&path_);
+        CHECK(iter.next() == nullptr);
+    }
+}
+
+// Regression test for a crash found by fuzzing.
+TEST_CASE("fuzz_issue_7295", "[MetricsPath]")
+{
+    NoOpFactory factory;
+
+    RawPath innerPath;
+    innerPath.moveTo(.0f, -20.5f);
+    innerPath.cubicTo(11.3218384f, -20.5f, 20.5f, -11.3218384f, 20.5f, .0f);
+    innerPath.cubicTo(20.5f, 11.3218384f, 11.3218384f, 20.5f, .0f, 20.5f);
+    innerPath.cubicTo(-11.3218384f, 20.5f, -20.5f, 11.3218384f, -20.5f, .0f);
+    innerPath.cubicTo(-20.5f, -11.3218384f, -11.3218384f, -20.5f, .0f, -20.5f);
+
+    RawPath outerPath;
+    Mat2D transform(1.f, .0f, .0f, 1.f, -134217728.f, -134217728.f);
+    outerPath.addPath(innerPath, &transform);
+
+    auto contour = ContourMeasureIter(&outerPath).next();
+    RawPath result;
+    contour->getSegment(.0f, 168.389008f, &result, true);
+    CHECK(math::nearly_equal(contour->length(), 168.389008f));
+}
diff --git a/test/cubic_value_test.cpp b/test/cubic_value_test.cpp
new file mode 100644
index 0000000..c36fd40
--- /dev/null
+++ b/test/cubic_value_test.cpp
@@ -0,0 +1,29 @@
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/animation/cubic_value_interpolator.hpp>
+#include "catch.hpp"
+#include "rive_file_reader.hpp"
+#include <cstdio>
+
+TEST_CASE("test cubic value load and interpolate properly", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/cubic_value_test.riv");
+
+    auto artboard = file->artboard();
+    REQUIRE(artboard != nullptr);
+
+    auto greyRect = artboard->find<rive::Node>("grey_rectangle");
+    REQUIRE(greyRect != nullptr);
+
+    REQUIRE(artboard->find<rive::CubicValueInterpolatorBase>().size() == 3);
+
+    auto animation = artboard->animation("Timeline 1");
+    REQUIRE(animation != nullptr);
+    // Go to frame 15.
+    animation->apply(artboard, 15.0f / animation->fps(), 1.0f);
+    REQUIRE(greyRect->x() == Approx(290.71f));
+
+    // Go to frame 11.
+    animation->apply(artboard, 11.0f / animation->fps(), 1.0f);
+    REQUIRE(greyRect->x() == Approx(363.01f));
+}
diff --git a/test/default_state_machine_test.cpp b/test/default_state_machine_test.cpp
new file mode 100644
index 0000000..1983b7c
--- /dev/null
+++ b/test/default_state_machine_test.cpp
@@ -0,0 +1,32 @@
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/shapes/clipping_shape.hpp>
+#include <rive/shapes/rectangle.hpp>
+#include <rive/shapes/shape.hpp>
+#include <rive/animation/state_machine_instance.hpp>
+#include "utils/no_op_factory.hpp"
+#include "utils/no_op_renderer.hpp"
+#include "rive_file_reader.hpp"
+#include <catch.hpp>
+#include <cstdio>
+
+TEST_CASE("default state machine is detected at load", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/entry.riv");
+
+    auto abi = file->artboardAt(0);
+    auto index = abi->defaultStateMachineIndex();
+
+    REQUIRE(index >= 0);
+    REQUIRE(abi->stateMachineNameAt(index) == "State Machine 1");
+
+    auto smi = abi->defaultStateMachine();
+
+    REQUIRE(smi != nullptr);
+    REQUIRE(smi->name() == "State Machine 1");
+
+    // default scene is the same as the default statemachine (when we have one)
+    auto scene = abi->defaultScene();
+    REQUIRE(scene != nullptr);
+    REQUIRE(scene->name() == smi->name());
+}
diff --git a/test/distance_constraint_test.cpp b/test/distance_constraint_test.cpp
new file mode 100644
index 0000000..62b97f5
--- /dev/null
+++ b/test/distance_constraint_test.cpp
@@ -0,0 +1,35 @@
+#include <rive/file.hpp>
+#include <rive/constraints/distance_constraint.hpp>
+#include <rive/node.hpp>
+#include <rive/math/vec2d.hpp>
+#include <rive/shapes/shape.hpp>
+#include "rive_file_reader.hpp"
+#include "rive_testing.hpp"
+#include <cstdio>
+
+TEST_CASE("distance constraints moves items as expected", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/distance_constraint.riv");
+
+    auto artboard = file->artboard();
+
+    REQUIRE(artboard->find<rive::Shape>("A") != nullptr);
+    auto a = artboard->find<rive::Shape>("A");
+
+    REQUIRE(artboard->find<rive::Shape>("B") != nullptr);
+    auto b = artboard->find<rive::Shape>("B");
+
+    REQUIRE(a->constraints().size() == 1);
+    REQUIRE(a->constraints()[0]->is<rive::DistanceConstraint>());
+
+    auto distanceConstraint = a->constraints()[0]->as<rive::DistanceConstraint>();
+    REQUIRE(distanceConstraint->modeValue() == 1);
+
+    b->x(259.31f);
+    b->y(137.87f);
+    artboard->advance(0.0f);
+
+    rive::Vec2D at = a->worldTranslation();
+    rive::Vec2D expectedTranslation(259.2808837890625f, 62.87000274658203f);
+    REQUIRE(rive::Vec2D::distance(at, expectedTranslation) < 0.001f);
+}
diff --git a/test/draw_order_test.cpp b/test/draw_order_test.cpp
new file mode 100644
index 0000000..c217f36
--- /dev/null
+++ b/test/draw_order_test.cpp
@@ -0,0 +1,39 @@
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/shapes/clipping_shape.hpp>
+#include <rive/shapes/rectangle.hpp>
+#include <rive/shapes/shape.hpp>
+#include <rive/animation/linear_animation_instance.hpp>
+#include "utils/no_op_renderer.hpp"
+#include "rive_file_reader.hpp"
+#include <catch.hpp>
+#include <cstdio>
+
+TEST_CASE("draw rules load and sort correctly", "[draw rules]")
+{
+    auto file = ReadRiveFile("../../test/assets/draw_rule_cycle.riv");
+
+    // auto file = reader.file();
+    std::unique_ptr<rive::ArtboardInstance> artboard = file->artboardDefault();
+    auto node = artboard->find<rive::Node>("Blue");
+    REQUIRE(node != nullptr);
+    REQUIRE(node->is<rive::Shape>());
+    // auto shape = node->as<rive::Shape>();
+
+    artboard->updateComponents();
+    REQUIRE(artboard->animationCount() == 1);
+
+    // Check that we can advance the ping-pong animation with 1 second duration
+    // without a hang.
+    std::unique_ptr<rive::LinearAnimationInstance> animation = artboard->animationAt(0);
+    // Advance and apply some frames.
+    int frames = 10;
+    float frameDuration = 1.0f;
+
+    for (int i = 0; i < frames; i++)
+    {
+        animation->advanceAndApply(frameDuration);
+        rive::NoOpRenderer renderer;
+        artboard->draw(&renderer);
+    }
+}
diff --git a/test/elastic_easing_test.cpp b/test/elastic_easing_test.cpp
new file mode 100644
index 0000000..4fa4a94
--- /dev/null
+++ b/test/elastic_easing_test.cpp
@@ -0,0 +1,35 @@
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/animation/elastic_interpolator.hpp>
+#include "rive/shapes/shape.hpp"
+#include "catch.hpp"
+#include "rive_file_reader.hpp"
+#include <cstdio>
+
+TEST_CASE("test elastic easing loads properly", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/test_elastic.riv");
+
+    auto artboard = file->artboard();
+    REQUIRE(artboard != nullptr);
+
+    REQUIRE(artboard->find<rive::ElasticInterpolator>().size() == 1);
+
+    auto interpolator = artboard->find<rive::ElasticInterpolator>()[0];
+    REQUIRE(interpolator->easing() == rive::Easing::easeOut);
+    REQUIRE(interpolator->amplitude() == 1.0f);
+    REQUIRE(interpolator->period() == 0.25f);
+
+    REQUIRE(artboard->find<rive::Shape>().size() == 1);
+
+    auto shape = artboard->find<rive::Shape>()[0];
+    REQUIRE(shape->x() == Approx(145.19f));
+    auto animation = artboard->animation("Timeline 1");
+    REQUIRE(animation != nullptr);
+    // Go to frame 15.
+    animation->apply(artboard, 7.0f / animation->fps(), 1.0f);
+    REQUIRE(shape->x() == Approx(423.98f));
+
+    animation->apply(artboard, 14.0f / animation->fps(), 1.0f);
+    REQUIRE(shape->x() == Approx(303.995f));
+}
diff --git a/test/enum_bitset_test.cpp b/test/enum_bitset_test.cpp
new file mode 100644
index 0000000..b1fbd9f
--- /dev/null
+++ b/test/enum_bitset_test.cpp
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2023 Rive
+ */
+
+#include <catch.hpp>
+
+#include "rive/enum_bitset.hpp"
+
+namespace rive
+{
+enum class Flags : uint8_t
+{
+    zero = 0,
+    one = 1 << 0,
+    two = 1 << 1,
+    four = 1 << 2,
+    eight = 1 << 3,
+};
+RIVE_MAKE_ENUM_BITSET(Flags);
+
+// Check rive::EnumBitset<> operators
+TEST_CASE("enum-operators", "[enum_bitset]")
+{
+    Flags flags;
+
+    flags = Flags::one | Flags::two;
+    CHECK(flags == (Flags)3);
+
+    flags &= ~Flags::two;
+    CHECK(flags == Flags::one);
+
+    flags = Flags::two | (Flags::four | Flags::eight);
+    CHECK(flags == (Flags)14);
+
+    flags = (Flags::two | Flags::four) & ~(Flags::one | Flags::two);
+    CHECK(flags == Flags::four);
+
+    flags = (Flags::two | Flags::four) & (Flags::one | Flags::two);
+    CHECK(flags == Flags::two);
+    CHECK(flags & Flags::two);
+    CHECK(!(flags & Flags::one));
+    CHECK(flags & (Flags::two | Flags::eight));
+    CHECK(!(flags & (Flags::four | Flags::eight)));
+
+    // All & overloads.
+    CHECK(!(Flags::one & Flags::two));
+    CHECK(Flags::four & Flags::four);
+    CHECK((Flags::four & Flags::four) == Flags::four);
+    CHECK((Flags::one & (Flags::one | Flags::two)) == Flags::one);
+    CHECK(!(~Flags::one & Flags::one));
+
+    // All | overloads.
+    CHECK(!(Flags::zero | Flags::zero));
+    CHECK(Flags::zero | Flags::one);
+    CHECK((Flags::one | Flags::two) == (Flags)3);
+    CHECK((Flags::one | (Flags::two | Flags::four)) == (Flags)7);
+    CHECK(((Flags::one | Flags::two) | Flags::four) == (Flags)7);
+    CHECK(((Flags::one | Flags::two) | (Flags::four | Flags::eight)) == (Flags)15);
+
+    // All ~ overloads.
+    CHECK(~Flags::two == (Flags)(255 ^ 2));                      // Flags is a uint8_t
+    CHECK(~(Flags::two | Flags::eight) == (Flags)(255 ^ 2 ^ 8)); // Flags is a uint8_t
+
+    // All &= overloads.
+    flags = Flags::eight | Flags::four | Flags::two | Flags::one;
+    CHECK(flags == (Flags)15);
+    Flags inverseEight = ~Flags::eight;
+    flags &= inverseEight;
+    CHECK(flags == (Flags)7);
+    flags &= ~(Flags::four | Flags::one);
+    CHECK(flags == Flags::two);
+    flags &= Flags::two;
+    CHECK(flags == Flags::two);
+    flags &= Flags::one;
+    CHECK(flags == Flags::zero);
+
+    // All |= overloads.
+    flags = Flags::zero;
+    CHECK(flags == Flags::zero);
+    flags |= Flags::eight;
+    CHECK(flags == Flags::eight);
+    flags |= ~Flags::eight;
+    CHECK(flags == (Flags)255); // Flags is a uint8_t
+}
+} // namespace rive
diff --git a/test/file_test.cpp b/test/file_test.cpp
new file mode 100644
index 0000000..69ec042
--- /dev/null
+++ b/test/file_test.cpp
@@ -0,0 +1,248 @@
+#include "rive/file.hpp"
+#include "rive/node.hpp"
+#include "rive/shapes/rectangle.hpp"
+#include "rive/shapes/shape.hpp"
+#include "rive/assets/image_asset.hpp"
+#include "rive/shapes/points_path.hpp"
+#include "rive/shapes/mesh.hpp"
+#include "utils/no_op_renderer.hpp"
+#include "rive_file_reader.hpp"
+#include <catch.hpp>
+#include <cstdio>
+#include <cstring>
+
+TEST_CASE("transform order is as expected", "[transform]")
+{
+    auto translation = rive::Mat2D::fromTranslate(10.0f, 20.0f);
+    auto rotation = rive::Mat2D::fromRotation(3.14f / 2.0f);
+    auto scale = rive::Mat2D::fromScale(2.0f, 3.0f);
+
+    auto xform = translation * rotation * scale;
+    auto xform2 = rive::Mat2D::fromRotation(3.14f / 2.0f);
+    xform2[0] *= 2.0f;
+    xform2[1] *= 2.0f;
+    xform2[2] *= 3.0f;
+    xform2[3] *= 3.0f;
+    xform2[4] = 10.0f;
+    xform2[5] = 20.0f;
+
+    REQUIRE(xform2 == xform);
+}
+
+TEST_CASE("file can be read", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/two_artboards.riv");
+
+    // Default artboard should be named Two.
+    REQUIRE(file->artboard()->name() == "Two");
+
+    // There should be a second artboard named One.
+    REQUIRE(file->artboard("One") != nullptr);
+}
+
+TEST_CASE("file with bad blend mode fails to load", "[file]")
+{
+    std::vector<uint8_t> bytes = ReadFile("../../test/assets/solar-system.riv");
+
+    rive::ImportResult result;
+    auto file = rive::File::import(bytes, &gNoOpFactory, &result, nullptr);
+    CHECK(result == rive::ImportResult::malformed);
+}
+
+TEST_CASE("file with animation can be read", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/juice.riv");
+
+    auto artboard = file->artboard();
+    REQUIRE(artboard->name() == "New Artboard");
+
+    auto shin = artboard->find("shin_right");
+    REQUIRE(shin != nullptr);
+    REQUIRE(shin->is<rive::Node>());
+
+    auto shinNode = shin->as<rive::Node>();
+    REQUIRE(shinNode->parent() != nullptr);
+    REQUIRE(shinNode->parent()->name() == "leg_right");
+    REQUIRE(shinNode->parent()->parent() != nullptr);
+    REQUIRE(shinNode->parent()->parent()->name() == "root");
+    REQUIRE(shinNode->parent()->parent() != nullptr);
+    REQUIRE(shinNode->parent()->parent()->parent() != nullptr);
+    REQUIRE(shinNode->parent()->parent()->parent() == artboard);
+
+    auto walkAnimation = artboard->animation("walk");
+    REQUIRE(walkAnimation != nullptr);
+    REQUIRE(walkAnimation->numKeyedObjects() == 22);
+}
+
+TEST_CASE("artboards can be counted and accessed via index or name", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/dependency_test.riv");
+
+    // The artboards caqn be counted
+    REQUIRE(file->artboardCount() == 1);
+
+    // Artboards can be access by index
+    REQUIRE(file->artboard(0) != nullptr);
+
+    // Artboards can be accessed by name
+    REQUIRE(file->artboard("Blue") != nullptr);
+}
+
+TEST_CASE("dependencies are as expected", "[file]")
+{
+    // ┌────┐
+    // │Blue│
+    // └────┘
+    //    │ ┌───┐
+    //    └▶│ A │
+    //      └───┘
+    //        │ ┌───┐
+    //        └▶│ B │
+    //          └───┘
+    //            │ ┌───┐
+    //            ├▶│ C │
+    //            │ └───┘
+    //            │ ┌─────────┐
+    //            └▶│Rectangle│
+    //              └─────────┘
+    //                   │ ┌──────────────┐
+    //                   └▶│Rectangle Path│
+    //                     └──────────────┘
+    auto file = ReadRiveFile("../../test/assets/dependency_test.riv");
+
+    auto artboard = file->artboard();
+    REQUIRE(artboard->name() == "Blue");
+
+    auto nodeA = artboard->find<rive::Node>("A");
+    auto nodeB = artboard->find<rive::Node>("B");
+    auto nodeC = artboard->find<rive::Node>("C");
+    auto shape = artboard->find<rive::Shape>("Rectangle");
+    auto path = artboard->find<rive::Path>("Rectangle Path");
+    REQUIRE(nodeA != nullptr);
+    REQUIRE(nodeB != nullptr);
+    REQUIRE(nodeC != nullptr);
+    REQUIRE(shape != nullptr);
+    REQUIRE(path != nullptr);
+
+    REQUIRE(nodeA->parent() == artboard);
+    REQUIRE(nodeB->parent() == nodeA);
+    REQUIRE(nodeC->parent() == nodeB);
+    REQUIRE(shape->parent() == nodeB);
+    REQUIRE(path->parent() == shape);
+
+    REQUIRE(nodeB->dependents().size() == 2);
+
+    REQUIRE(artboard->graphOrder() == 0);
+    REQUIRE(nodeA->graphOrder() > artboard->graphOrder());
+    REQUIRE(nodeB->graphOrder() > nodeA->graphOrder());
+    REQUIRE(nodeC->graphOrder() > nodeB->graphOrder());
+    REQUIRE(shape->graphOrder() > nodeB->graphOrder());
+    REQUIRE(path->graphOrder() > shape->graphOrder());
+
+    artboard->advance(0.0f);
+
+    auto world = shape->worldTransform();
+    REQUIRE(world[4] == 39.203125f);
+    REQUIRE(world[5] == 29.535156f);
+}
+
+TEST_CASE("long name in object is parsed correctly", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/long_name.riv");
+    auto artboard = file->artboard();
+
+    // Expect all object in file to be loaded, in this case 7
+    REQUIRE(artboard->objects().size() == 7);
+}
+
+TEST_CASE("file with in-band images can have the stripped", "[file]")
+{
+    FILE* fp = fopen("../../test/assets/jellyfish_test.riv", "rb");
+    REQUIRE(fp != nullptr);
+
+    fseek(fp, 0, SEEK_END);
+    const size_t length = ftell(fp);
+    fseek(fp, 0, SEEK_SET);
+    std::vector<uint8_t> bytes(length);
+    REQUIRE(fread(bytes.data(), 1, length, fp) == length);
+    fclose(fp);
+
+    rive::ImportResult result;
+    auto file = rive::File::import(bytes, &gNoOpFactory, &result);
+    REQUIRE(result == rive::ImportResult::success);
+    REQUIRE(file.get() != nullptr);
+    REQUIRE(file->artboard() != nullptr);
+
+    // Default artboard should be named Two.
+    REQUIRE(file->artboard()->name() == "Jellyfish");
+
+    // Strip nothing should result in the same file.
+    {
+        rive::ImportResult stripResult;
+        auto strippedBytes = rive::File::stripAssets(bytes, {}, &stripResult);
+        REQUIRE(stripResult == rive::ImportResult::success);
+        REQUIRE(bytes.size() == strippedBytes.size());
+        REQUIRE(std::memcmp(bytes.data(), strippedBytes.data(), bytes.size()) == 0);
+    }
+
+    // Strip image assets should result in a smaller file.
+    {
+        rive::ImportResult stripResult;
+        auto strippedBytes =
+            rive::File::stripAssets(bytes, {rive::ImageAsset::typeKey}, &stripResult);
+        REQUIRE(stripResult == rive::ImportResult::success);
+        REQUIRE(strippedBytes.size() < bytes.size());
+    }
+}
+
+TEST_CASE("file a bad skin (no parent skinnable) doesn't crash", "[file]")
+{
+    FILE* fp = fopen("../../test/assets/bad_skin.riv", "rb");
+    REQUIRE(fp != nullptr);
+
+    fseek(fp, 0, SEEK_END);
+    const size_t length = ftell(fp);
+    fseek(fp, 0, SEEK_SET);
+    std::vector<uint8_t> bytes(length);
+    REQUIRE(fread(bytes.data(), 1, length, fp) == length);
+    fclose(fp);
+
+    rive::ImportResult result;
+    auto file = rive::File::import(bytes, &gNoOpFactory, &result);
+    REQUIRE(result == rive::ImportResult::success);
+    REQUIRE(file.get() != nullptr);
+    REQUIRE(file->artboard() != nullptr);
+
+    REQUIRE(file->artboard()->name() == "Illustration WOman.svg");
+    auto artboard = file->artboardDefault();
+    artboard->updateComponents();
+    auto paths = artboard->find<rive::PointsPath>();
+    for (auto path : paths)
+    {
+        path->markPathDirty();
+    }
+    artboard->updateComponents();
+}
+
+// TODO:
+// ShapePaint (fill/stroke) needs to be implemented in WASM (jsFill/jsStroke) in
+// order to create Paint objects as necessary.
+
+// Mutators need to be implemented in WASM (solid/linear/radial) and get access
+// to their ShapePaint so they can mutate any extra objects they create on it
+// (like a paint object for skia).
+
+// Paths need to be implemented in WASM but not so much as a core path (like
+// parametric/pointspath, etc) but more as a general rendering path. Handed
+// their commands so they can generate/store a re-usable path. This would be a
+// Path2D in context2D and a SkPath in CanvasKit.
+
+// PathComposer is the factory for the Paths. But they do need to surive so they
+// can be reset/reused as available by the rendering lib.
+
+// PathComposer needs to be implemented in WASM to compose the paths together
+// and be accessible from the Shape (jsShape) which will need a call
+// setupFill/restoreFill and setupStroke/restoreStroke.
+
+// Draw will be called by C++ on the Shape, the Shape will call draw on the
+// fill/stroke (propagates to jsFill/jsStroke)
diff --git a/test/follow_path_constraint_test.cpp b/test/follow_path_constraint_test.cpp
new file mode 100644
index 0000000..faff9ec
--- /dev/null
+++ b/test/follow_path_constraint_test.cpp
@@ -0,0 +1,68 @@
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/shapes/shape.hpp>
+#include <rive/math/transform_components.hpp>
+#include <utils/no_op_renderer.hpp>
+#include "rive_file_reader.hpp"
+#include "rive_testing.hpp"
+#include <cstdio>
+
+TEST_CASE("follow path constraint updates world transform", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/follow_path.riv");
+
+    auto artboard = file->artboard();
+
+    REQUIRE(artboard->find<rive::TransformComponent>("target") != nullptr);
+    auto target = artboard->find<rive::TransformComponent>("target");
+
+    REQUIRE(artboard->find<rive::TransformComponent>("rect") != nullptr);
+    auto rectangle = artboard->find<rive::TransformComponent>("rect");
+
+    artboard->advance(0.0f);
+
+    auto targetComponents = target->worldTransform().decompose();
+    auto rectComponents = rectangle->worldTransform().decompose();
+    REQUIRE(targetComponents.x() == rectComponents.x());
+    REQUIRE(targetComponents.y() == rectComponents.y());
+}
+
+TEST_CASE("follow path with 0 opacity constraint updates world transform", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/follow_path_with_0_opacity.riv");
+
+    auto artboard = file->artboard();
+
+    REQUIRE(artboard->find<rive::TransformComponent>("target") != nullptr);
+    auto target = artboard->find<rive::TransformComponent>("target");
+
+    REQUIRE(artboard->find<rive::TransformComponent>("rect") != nullptr);
+    auto rectangle = artboard->find<rive::TransformComponent>("rect");
+
+    artboard->advance(0.0f);
+
+    auto targetComponents = target->worldTransform().decompose();
+    auto rectComponents = rectangle->worldTransform().decompose();
+    REQUIRE(targetComponents.x() == rectComponents.x());
+    REQUIRE(targetComponents.y() == rectComponents.y());
+}
+
+TEST_CASE("follow path constraint with path at 0 opacity updates world transform", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/follow_path_path_0_opacity.riv");
+
+    auto artboard = file->artboard();
+
+    REQUIRE(artboard->find<rive::TransformComponent>("target") != nullptr);
+    auto target = artboard->find<rive::TransformComponent>("target");
+
+    REQUIRE(artboard->find<rive::TransformComponent>("rect") != nullptr);
+    auto rectangle = artboard->find<rive::TransformComponent>("rect");
+
+    artboard->advance(0.0f);
+
+    auto targetComponents = target->worldTransform().decompose();
+    auto rectComponents = rectangle->worldTransform().decompose();
+    REQUIRE(targetComponents.x() == rectComponents.x());
+    REQUIRE(targetComponents.y() == rectComponents.y());
+}
diff --git a/test/font_test.cpp b/test/font_test.cpp
new file mode 100644
index 0000000..8c5ac0b
--- /dev/null
+++ b/test/font_test.cpp
@@ -0,0 +1,153 @@
+#include "rive/simple_array.hpp"
+#include "catch.hpp"
+#include "rive/text_engine.hpp"
+#include "rive/text/font_hb.hpp"
+#include "rive/text/utf.hpp"
+#include <string>
+
+using namespace rive;
+
+static rive::TextRun append(std::vector<rive::Unichar>* unichars,
+                            rive::rcp<rive::Font> font,
+                            float size,
+                            const char text[])
+{
+    const uint8_t* ptr = (const uint8_t*)text;
+    uint32_t n = 0;
+    while (*ptr)
+    {
+        unichars->push_back(rive::UTF::NextUTF8(&ptr));
+        n += 1;
+    }
+    return {std::move(font), size, -1.0f, 0.0f, n, 0};
+}
+
+static rcp<Font> loadFont(const char* filename)
+{
+    FILE* fp = fopen(filename, "rb");
+    REQUIRE(fp != nullptr);
+
+    fseek(fp, 0, SEEK_END);
+    const size_t length = ftell(fp);
+    fseek(fp, 0, SEEK_SET);
+    std::vector<uint8_t> bytes(length);
+    REQUIRE(fread(bytes.data(), 1, length, fp) == length);
+    fclose(fp);
+
+    return HBFont::Decode(bytes);
+}
+
+static std::vector<rive::rcp<rive::Font>> fallbackFonts;
+static rive::rcp<rive::Font> pickFallbackFont(rive::Span<const rive::Unichar> missing)
+{
+    size_t length = fallbackFonts.size();
+    for (size_t i = 0; i < length; i++)
+    {
+        HBFont* font = static_cast<HBFont*>(fallbackFonts[i].get());
+        if (font->hasGlyph(missing))
+        {
+            return fallbackFonts[i];
+        }
+    }
+    return nullptr;
+}
+
+TEST_CASE("fallback glyphs are found", "[text]")
+{
+    REQUIRE(fallbackFonts.empty());
+    auto font = loadFont("../../test/assets/RobotoFlex.ttf");
+    REQUIRE(font != nullptr);
+    auto fallbackFont = loadFont("../../test/assets/IBMPlexSansArabic-Regular.ttf");
+    REQUIRE(fallbackFont != nullptr);
+    fallbackFonts.push_back(fallbackFont);
+
+    Font::gFallbackProc = pickFallbackFont;
+
+    std::vector<rive::TextRun> truns;
+    std::vector<rive::Unichar> unichars;
+    truns.push_back(append(&unichars, font, 32.0f, "لمفاتيح ABC DEF"));
+
+    auto paragraphs = font->shapeText(unichars, truns);
+    REQUIRE(paragraphs.size() == 1);
+    paragraphs = SimpleArray<Paragraph>();
+    REQUIRE(paragraphs.size() == 0);
+    fallbackFonts.clear();
+    Font::gFallbackProc = nullptr;
+}
+
+TEST_CASE("variable axis values can be read", "[text]")
+{
+    REQUIRE(fallbackFonts.empty());
+    auto font = loadFont("../../test/assets/RobotoFlex.ttf");
+    REQUIRE(font != nullptr);
+
+    auto count = font->getAxisCount();
+
+    bool hasWeight = false;
+    for (uint16_t i = 0; i < count; i++)
+    {
+        auto axis = font->getAxis(i);
+        if (axis.tag == 2003265652)
+        {
+            REQUIRE(axis.def == 400.0f);
+            hasWeight = true;
+            break;
+        }
+    }
+
+    REQUIRE(hasWeight);
+
+    float value = font->getAxisValue(2003265652);
+    REQUIRE(value == 400.0f);
+
+    REQUIRE(font->getAxisValue(2003072104) == 100.0f);
+
+    rive::Font::Coord coord = {2003265652, 800.0f};
+    rive::rcp<rive::Font> vfont = font->makeAtCoords(rive::Span<HBFont::Coord>(&coord, 1));
+    REQUIRE(vfont->getAxisValue(2003265652) == 800.0f);
+
+    rive::Font::Coord coord2 = {2003072104, 122.0f};
+    rive::rcp<rive::Font> vfont2 = vfont->makeAtCoords(rive::Span<HBFont::Coord>(&coord2, 1));
+    REQUIRE(vfont2->getAxisValue(2003072104) == 122.0f);
+    // Should also still have the first axis value we set.
+    REQUIRE(vfont2->getAxisValue(2003265652) == 800.0f);
+}
+
+static std::string tagToString(uint32_t tag)
+{
+    std::string tag_name;
+    tag_name += ((char)((tag & 0xff000000) >> 24));
+    tag_name += ((char)((tag & 0x00ff0000) >> 16));
+    tag_name += ((char)((tag & 0x0000ff00) >> 8));
+    tag_name += ((char)((tag & 0x000000ff)));
+    return tag_name;
+}
+
+static bool hasTag(std::vector<std::string> featureStrings, std::string tag)
+{
+    return std::find(std::begin(featureStrings), std::end(featureStrings), tag) !=
+           std::end(featureStrings);
+}
+
+TEST_CASE("font features load as expected", "[text]")
+{
+    REQUIRE(fallbackFonts.empty());
+    auto font = loadFont("../../test/assets/RobotoFlex.ttf");
+    REQUIRE(font != nullptr);
+
+    rive::SimpleArray<uint32_t> features = font->features();
+    std::vector<std::string> featureStrings;
+    for (auto feature : features)
+    {
+        featureStrings.push_back(tagToString(feature));
+    }
+    REQUIRE(features.size() == 7);
+
+    REQUIRE(hasTag(featureStrings, "mkmk"));
+    REQUIRE(hasTag(featureStrings, "kern"));
+    REQUIRE(hasTag(featureStrings, "rvrn"));
+    REQUIRE(hasTag(featureStrings, "mark"));
+    REQUIRE(hasTag(featureStrings, "locl"));
+    REQUIRE(hasTag(featureStrings, "pnum"));
+    REQUIRE(hasTag(featureStrings, "liga"));
+}
diff --git a/test/hittest_test.cpp b/test/hittest_test.cpp
new file mode 100644
index 0000000..6c12c9f
--- /dev/null
+++ b/test/hittest_test.cpp
@@ -0,0 +1,386 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include <rive/math/aabb.hpp>
+#include <rive/math/hit_test.hpp>
+#include <rive/nested_artboard.hpp>
+#include <rive/animation/state_machine_instance.hpp>
+#include <rive/animation/state_machine_input_instance.hpp>
+#include <rive/animation/nested_state_machine.hpp>
+#include <rive/animation/animation_state.hpp>
+#include "rive_file_reader.hpp"
+
+#include <catch.hpp>
+#include <cstdio>
+
+using namespace rive;
+
+TEST_CASE("hittest-basics", "[hittest]")
+{
+    HitTester tester;
+    tester.reset({10, 10, 12, 12});
+    tester.move({0, 0});
+    tester.line({20, 0});
+    tester.line({20, 20});
+    tester.line({0, 20});
+    tester.close();
+    REQUIRE(tester.test());
+
+    IAABB area = {81, 156, 84, 159};
+
+    Vec2D pts[] = {
+        {29.9785f, 32.5261f},
+        {231.102f, 32.5261f},
+        {231.102f, 269.898f},
+        {29.9785f, 269.898f},
+    };
+    tester.reset(area);
+
+    tester.move(pts[0]);
+    for (int i = 1; i < 4; ++i)
+    {
+        tester.line(pts[i]);
+    }
+    tester.close();
+    REQUIRE(tester.test());
+}
+
+TEST_CASE("hittest-mesh", "[hittest]")
+{
+
+    const IAABB area{10, 10, 12, 12};
+
+    Vec2D verts[] = {
+        {0, 0},
+        {20, 10},
+        {0, 20},
+    };
+    uint16_t indices[] = {
+        0,
+        1,
+        2,
+    };
+    REQUIRE(HitTester::testMesh(area, make_span(verts, 3), make_span(indices, 3)));
+}
+
+TEST_CASE("hit test on opaque target", "[hittest]")
+{
+    // This artboard has two rects of size 200 x 200, "red-activate" at [0, 0, 200, 200]
+    // and "green-activate" at [0, 100, 200, 300]
+    // "red-activate" is above "green-activate" in drawing order
+    // Both targets are set as opaque for its listeners
+    // "red-activate" sets "toGreen" to false
+    // "green-activate" sets "toGreen" to true
+    // There is also a "gray-activate" above the other 2 that is not opaque so events should
+    // traverse through the other targets
+    auto file = ReadRiveFile("../../test/assets/opaque_hit_test.riv");
+
+    auto artboard = file->artboard("main");
+    auto artboardInstance = artboard->instance();
+    auto stateMachine = artboard->stateMachine("main-state-machine");
+
+    REQUIRE(artboardInstance != nullptr);
+    REQUIRE(artboardInstance->stateMachineCount() == 1);
+
+    REQUIRE(stateMachine != nullptr);
+
+    rive::StateMachineInstance* stateMachineInstance =
+        new rive::StateMachineInstance(stateMachine, artboardInstance.get());
+
+    stateMachineInstance->advance(0.0f);
+    artboardInstance->advance(0.0f);
+    REQUIRE(stateMachineInstance->needsAdvance() == true);
+    stateMachineInstance->advance(0.0f);
+
+    auto toGreenToggle = stateMachineInstance->getBool("toGreen");
+    REQUIRE(toGreenToggle != nullptr);
+    auto grayToggle = stateMachineInstance->getBool("grayToggle");
+    REQUIRE(grayToggle != nullptr);
+
+    stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 50.0f));
+    // "gray-activate" is clicked
+    REQUIRE(grayToggle->value() == true);
+    // Pointer only over "red-activate"
+    REQUIRE(toGreenToggle->value() == false);
+
+    stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 250.0f));
+    // "gray-activate" is clicked
+    REQUIRE(grayToggle->value() == false);
+    // Pointer over "green-activate"
+    REQUIRE(toGreenToggle->value() == true);
+
+    stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 110.0f));
+    // "gray-activate" is clicked
+    REQUIRE(grayToggle->value() == true);
+    // Pointer over "red-activate" and "green-activate", but "red-activate" is opaque and above
+    // so green activate does not trigger
+    REQUIRE(toGreenToggle->value() == false);
+    delete stateMachineInstance;
+}
+
+TEST_CASE("hit test on opaque nested artboard", "[hittest]")
+{
+    // This artboard (300x300) has a main rect at [0, 0, 300, 300]
+    // this rect has a listener that toggles "second-gray-toggle"
+    // and a nested artboard at [0, 0, 150, 150]
+    // the nested artboard and the rect have opaque targets
+    auto file = ReadRiveFile("../../test/assets/opaque_hit_test.riv");
+
+    auto artboard = file->artboard("second");
+    auto artboardInstance = artboard->instance();
+    auto stateMachine = artboard->stateMachine("second-state-machine");
+
+    REQUIRE(artboardInstance != nullptr);
+    REQUIRE(artboardInstance->stateMachineCount() == 1);
+
+    REQUIRE(stateMachine != nullptr);
+
+    rive::StateMachineInstance* stateMachineInstance =
+        new rive::StateMachineInstance(stateMachine, artboardInstance.get());
+
+    auto nestedArtboard =
+        stateMachineInstance->artboard()->find<rive::NestedArtboard>("second-nested");
+    REQUIRE(nestedArtboard != nullptr);
+    auto nestedArtboardStateMachine =
+        nestedArtboard->nestedAnimations()[0]->as<NestedStateMachine>();
+    REQUIRE(nestedArtboardStateMachine != nullptr);
+    auto nestedArtboardStateMachineInstance = nestedArtboardStateMachine->stateMachineInstance();
+
+    auto secondNestedBoolTarget = nestedArtboardStateMachineInstance->getBool("bool-target");
+    REQUIRE(secondNestedBoolTarget != nullptr);
+
+    artboardInstance->advance(0.0f);
+    stateMachineInstance->advanceAndApply(0.0f);
+
+    REQUIRE(secondNestedBoolTarget->value() == false);
+
+    auto secondGrayToggle = stateMachineInstance->getBool("second-gray-toggle");
+    REQUIRE(secondGrayToggle != nullptr);
+
+    stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 250.0f));
+    // toggle changes value because it is not under an opaque nested artboard
+    REQUIRE(secondGrayToggle->value() == true);
+
+    stateMachineInstance->pointerDown(rive::Vec2D(301.0f, 50.0f));
+    // toggle does not change because it is beyond the area of the square by 1 pixel
+    // And the 2px padding is unly used after the coarse grained test passes
+    REQUIRE(secondGrayToggle->value() == true);
+
+    stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 50.0f));
+    // toggle does not change because it is under an opaque nested artboard
+    REQUIRE(secondGrayToggle->value() == true);
+
+    // nested toggle changes because it's on top of shape
+    REQUIRE(secondNestedBoolTarget->value() == true);
+
+    // A timeline switches draw order and the nested artboard is now below the rect
+    stateMachineInstance->advanceAndApply(1.0f);
+    stateMachineInstance->advance(0.0f);
+
+    stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 50.0f));
+    // So now the pointer down is captured by the rect
+    REQUIRE(secondGrayToggle->value() == false);
+
+    // nested toggle does not change because it's below shape
+    REQUIRE(secondNestedBoolTarget->value() == true);
+    delete stateMachineInstance;
+}
+
+TEST_CASE("early out on listeners", "[hittest]")
+{
+    auto file = ReadRiveFile("../../test/assets/pointer_events.riv");
+
+    auto artboard = file->artboard("art-1");
+    auto artboardInstance = artboard->instance();
+    auto stateMachine = artboard->stateMachine("sm-1");
+
+    REQUIRE(artboardInstance != nullptr);
+    REQUIRE(artboardInstance->stateMachineCount() == 1);
+
+    REQUIRE(stateMachine != nullptr);
+
+    rive::StateMachineInstance* stateMachineInstance =
+        new rive::StateMachineInstance(stateMachine, artboardInstance.get());
+
+    stateMachineInstance->advance(0.0f);
+    artboardInstance->advance(0.0f);
+    REQUIRE(stateMachineInstance->needsAdvance() == true);
+    stateMachineInstance->advance(0.0f);
+    REQUIRE(stateMachineInstance->hitComponentsCount() == 4);
+    // Hit component with only pointer down and pointer up listeners
+    auto hitComponentWithEarlyOut = stateMachineInstance->hitComponent(0);
+    // Hit component that can't early out because it has a pointer enter event
+    auto hitComponentWithNoEarlyOut = stateMachineInstance->hitComponent(1);
+    // Hit component that can't early out because it is an opaque target
+    auto hitComponentOpaque = stateMachineInstance->hitComponent(2);
+    // Hit component that can early out on all and pointer up
+    auto hitComponentOnlyPointerDown = stateMachineInstance->hitComponent(3);
+    REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 0);
+    REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
+    REQUIRE(hitComponentOpaque->earlyOutCount == 0);
+    REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 0);
+    stateMachineInstance->pointerMove(rive::Vec2D(100.0f, 250.0f));
+    REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 1);
+    REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
+    REQUIRE(hitComponentOpaque->earlyOutCount == 0);
+    REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 1);
+    stateMachineInstance->pointerExit(rive::Vec2D(100.0f, 250.0f));
+    REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 2);
+    REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
+    REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 2);
+    REQUIRE(hitComponentOpaque->earlyOutCount == 0);
+    stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 250.0f));
+    REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 2);
+    REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
+    REQUIRE(hitComponentOpaque->earlyOutCount == 0);
+    REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 2);
+    stateMachineInstance->pointerUp(rive::Vec2D(100.0f, 250.0f));
+    REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 2);
+    REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
+    REQUIRE(hitComponentOpaque->earlyOutCount == 0);
+    REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 3);
+    stateMachineInstance->pointerMove(rive::Vec2D(105.0f, 205.0f));
+    REQUIRE(hitComponentWithEarlyOut->earlyOutCount == 3);
+    REQUIRE(hitComponentWithNoEarlyOut->earlyOutCount == 0);
+    REQUIRE(hitComponentOpaque->earlyOutCount == 0);
+    REQUIRE(hitComponentOnlyPointerDown->earlyOutCount == 4);
+
+    delete stateMachineInstance;
+}
+
+TEST_CASE("click event", "[hittest]")
+{
+    // This test has two rectangles of size [200, 200]
+    // positioned at [100,100] and [200, 200]
+    // they overlap between coordinates [100,100]-[200, 200]
+    // they are inside a group that has a listener attached to it
+    // that listener should fire an event on "Click"
+    auto file = ReadRiveFile("../../test/assets/click_event.riv");
+
+    auto artboard = file->artboard("art-1");
+    auto artboardInstance = artboard->instance();
+    auto stateMachine = artboard->stateMachine("sm-1");
+
+    REQUIRE(artboardInstance != nullptr);
+    REQUIRE(artboardInstance->stateMachineCount() == 1);
+
+    REQUIRE(stateMachine != nullptr);
+
+    rive::StateMachineInstance* stateMachineInstance =
+        new rive::StateMachineInstance(stateMachine, artboardInstance.get());
+
+    stateMachineInstance->advance(0.0f);
+    artboardInstance->advance(0.0f);
+    REQUIRE(stateMachineInstance->needsAdvance() == true);
+    stateMachineInstance->advance(0.0f);
+    // There is a single listener with two shapes in it
+    REQUIRE(stateMachineInstance->hitComponentsCount() == 2);
+    auto layerCount = stateMachine->layerCount();
+    REQUIRE(layerCount == 1);
+    REQUIRE(stateMachineInstance->reportedEventCount() == 0);
+    // Click in place should trigger a click event
+    stateMachineInstance->pointerDown(rive::Vec2D(75.0f, 75.0f));
+    stateMachineInstance->pointerUp(rive::Vec2D(75.0f, 75.0f));
+    REQUIRE(stateMachineInstance->reportedEventCount() == 1);
+    // Pointer down inside shape but Pointer up outside the shape
+    // should not trigger a click event
+    stateMachineInstance->pointerDown(rive::Vec2D(75.0f, 75.0f));
+    stateMachineInstance->pointerUp(rive::Vec2D(300.0f, 75.0f));
+    REQUIRE(stateMachineInstance->reportedEventCount() == 1);
+    // Pointer down outside shape but Pointer up inside the shape
+    // should not trigger a click event
+    stateMachineInstance->pointerDown(rive::Vec2D(300.0f, 75.0f));
+    stateMachineInstance->pointerUp(rive::Vec2D(75.0f, 75.0f));
+    REQUIRE(stateMachineInstance->reportedEventCount() == 1);
+    // Pointer down in shape 1 Pointer up in shape 2 of the same group
+    // should trigger a click event
+    stateMachineInstance->pointerDown(rive::Vec2D(75.0f, 75.0f));
+    stateMachineInstance->pointerUp(rive::Vec2D(225.0f, 225.0f));
+    REQUIRE(stateMachineInstance->reportedEventCount() == 2);
+    // Pointer down and up in area where both shapes overlap
+    // should trigger a single click event
+    stateMachineInstance->pointerDown(rive::Vec2D(150.0f, 150.0f));
+    stateMachineInstance->pointerUp(rive::Vec2D(150.0f, 150.0f));
+    REQUIRE(stateMachineInstance->reportedEventCount() == 3);
+
+    delete stateMachineInstance;
+}
+
+TEST_CASE("multiple shapes with mouse movement behavior", "[hittest]")
+{
+    // This test has two rectangles of size [200, 200]
+    // positioned at [100,100] and [100, 200]
+    // they overlap between coordinates [100,0]-[200, 200]
+    // they are inside a group that has a Pointer enter and a Pointer out
+    // listeners that toggle between two states (red and green)
+    // starting at "red"
+    auto file = ReadRiveFile("../../test/assets/click_event.riv");
+
+    auto artboard = file->artboard("art-2");
+    auto artboardInstance = artboard->instance();
+    auto stateMachine = artboard->stateMachine("sm-1");
+
+    REQUIRE(artboardInstance != nullptr);
+    REQUIRE(artboardInstance->stateMachineCount() == 1);
+
+    REQUIRE(stateMachine != nullptr);
+
+    rive::StateMachineInstance* stateMachineInstance =
+        new rive::StateMachineInstance(stateMachine, artboardInstance.get());
+
+    stateMachineInstance->advance(0.0f);
+    artboardInstance->advance(0.0f);
+    REQUIRE(stateMachineInstance->needsAdvance() == true);
+    stateMachineInstance->advance(0.0f);
+    // There is a single listener with two shapes in it
+    REQUIRE(stateMachineInstance->hitComponentsCount() == 2);
+    auto layerCount = stateMachine->layerCount();
+    REQUIRE(layerCount == 1);
+    // Move over the first shape
+    stateMachineInstance->pointerMove(rive::Vec2D(75.0f, 75.0f));
+    artboardInstance->advance(0.0f);
+    stateMachineInstance->advanceAndApply(0.0f);
+
+    {
+        auto state = stateMachineInstance->layerState(0);
+        REQUIRE(state->is<rive::AnimationState>());
+        auto animation = state->as<rive::AnimationState>()->animation();
+        REQUIRE(animation->name() == "green");
+    }
+    // Move over the second shape, nothing should change
+    stateMachineInstance->pointerMove(rive::Vec2D(200.0f, 75.0f));
+    artboardInstance->advance(0.0f);
+    stateMachineInstance->advanceAndApply(0.0f);
+
+    {
+        auto state = stateMachineInstance->layerState(0);
+        REQUIRE(state->is<rive::AnimationState>());
+        auto animation = state->as<rive::AnimationState>()->animation();
+        REQUIRE(animation->name() == "green");
+    }
+    // Move out of the second shape, should go back to red
+    stateMachineInstance->pointerMove(rive::Vec2D(400.0f, 75.0f));
+    artboardInstance->advance(0.0f);
+    stateMachineInstance->advanceAndApply(0.0f);
+
+    {
+        auto state = stateMachineInstance->layerState(0);
+        REQUIRE(state->is<rive::AnimationState>());
+        auto animation = state->as<rive::AnimationState>()->animation();
+        REQUIRE(animation->name() == "red");
+    }
+    // Move back into the second shape, should go to green
+    stateMachineInstance->pointerMove(rive::Vec2D(200.0f, 75.0f));
+    artboardInstance->advance(0.0f);
+    stateMachineInstance->advanceAndApply(0.0f);
+
+    {
+        auto state = stateMachineInstance->layerState(0);
+        REQUIRE(state->is<rive::AnimationState>());
+        auto animation = state->as<rive::AnimationState>()->animation();
+        REQUIRE(animation->name() == "green");
+    }
+
+    delete stateMachineInstance;
+}
\ No newline at end of file
diff --git a/test/ik_constraint_test.cpp b/test/ik_constraint_test.cpp
new file mode 100644
index 0000000..04ddebd
--- /dev/null
+++ b/test/ik_constraint_test.cpp
@@ -0,0 +1,38 @@
+#include <rive/file.hpp>
+#include <rive/constraints/ik_constraint.hpp>
+#include <rive/node.hpp>
+#include <rive/math/vec2d.hpp>
+#include <rive/shapes/shape.hpp>
+#include <rive/bones/skin.hpp>
+#include <rive/bones/bone.hpp>
+#include "rive_file_reader.hpp"
+#include "rive_testing.hpp"
+#include <cstdio>
+
+TEST_CASE("ik with skinned bones orders correctly", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/complex_ik_dependency.riv");
+
+    auto artboard = file->artboard();
+
+    REQUIRE(artboard->find<rive::Bone>("One") != nullptr);
+    auto one = artboard->find<rive::Bone>("One");
+
+    REQUIRE(artboard->find<rive::Bone>("Two") != nullptr);
+    auto two = artboard->find<rive::Bone>("Two");
+    rive::Skin* skin = nullptr;
+    for (auto object : artboard->objects())
+    {
+        if (object->is<rive::Skin>())
+        {
+            skin = object->as<rive::Skin>();
+            break;
+        }
+    }
+
+    REQUIRE(skin != nullptr);
+    REQUIRE(two->constraints()[0]->is<rive::IKConstraint>());
+
+    REQUIRE(skin->graphOrder() > one->graphOrder());
+    REQUIRE(skin->graphOrder() > two->graphOrder());
+}
diff --git a/test/ik_test.cpp b/test/ik_test.cpp
new file mode 100644
index 0000000..874a063
--- /dev/null
+++ b/test/ik_test.cpp
@@ -0,0 +1,156 @@
+#include <rive/node.hpp>
+#include <rive/bones/bone.hpp>
+#include <rive/shapes/shape.hpp>
+#include <utils/no_op_renderer.hpp>
+#include "rive_file_reader.hpp"
+#include "rive_testing.hpp"
+#include <cstdio>
+
+TEST_CASE("two bone ik places bones correctly", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/two_bone_ik.riv");
+    auto artboard = file->artboard();
+
+    REQUIRE(artboard->find<rive::Shape>("circle a") != nullptr);
+    auto circleA = artboard->find<rive::Shape>("circle a");
+
+    REQUIRE(artboard->find<rive::Shape>("circle b") != nullptr);
+    auto circleB = artboard->find<rive::Shape>("circle b");
+
+    REQUIRE(artboard->find<rive::Bone>("a") != nullptr);
+    auto boneA = artboard->find<rive::Bone>("a");
+
+    REQUIRE(artboard->find<rive::Bone>("b") != nullptr);
+    auto boneB = artboard->find<rive::Bone>("b");
+
+    REQUIRE(artboard->find<rive::Node>("target") != nullptr);
+    auto target = artboard->find<rive::Node>("target");
+
+    REQUIRE(artboard->animation("Animation 1") != nullptr);
+    auto animation = artboard->animation("Animation 1");
+
+    // Make sure dependency structure is correct. Important thing here is to
+    // ensure that circle a is dependent upon the tip of the ik chain (bone b).
+    // circle b is a child of bone b so it'll be there anyway, but may as well
+    // validate.
+    REQUIRE(std::find(boneB->dependents().begin(), boneB->dependents().end(), circleA) !=
+            boneB->dependents().end());
+    REQUIRE(std::find(boneB->dependents().begin(), boneB->dependents().end(), circleB) !=
+            boneB->dependents().end());
+
+    animation->apply(artboard, 0.0f, 1.0f);
+    artboard->advance(0.0f);
+    REQUIRE(target->x() == 296.0f);
+    REQUIRE(target->y() == 202.0f);
+    REQUIRE(aboutEqual(boneA->worldTransform(),
+                       rive::Mat2D(0.11632211506366729736328125f,
+                                   -0.993211567401885986328125f,
+                                   0.993211567401885986328125f,
+                                   0.11632211506366729736328125f,
+                                   26.015254974365234375f,
+                                   475.2149658203125f)));
+
+    REQUIRE(aboutEqual(boneB->worldTransform(),
+                       rive::Mat2D(0.974071562290191650390625f,
+                                   0.2262403070926666259765625f,
+                                   -0.2262403070926666259765625f,
+                                   0.974071562290191650390625f,
+                                   64.31568145751953125f,
+                                   148.1883544921875f)));
+
+    animation->apply(artboard, 1.0f, 1.0f);
+    artboard->advance(0.0f);
+    REQUIRE(target->x() == 450.0f);
+    REQUIRE(target->y() == 337.0f);
+    REQUIRE(aboutEqual(boneA->worldTransform(),
+                       rive::Mat2D(0.650279819965362548828125f,
+                                   -0.7596948146820068359375f,
+                                   0.7596948146820068359375f,
+                                   0.650279819965362548828125f,
+                                   26.015254974365234375f,
+                                   475.2149658203125f)));
+
+    REQUIRE(aboutEqual(boneB->worldTransform(),
+                       rive::Mat2D(0.8823678493499755859375f,
+                                   0.470560371875762939453125f,
+                                   -0.47056043148040771484375f,
+                                   0.882367908954620361328125f,
+                                   240.1275634765625f,
+                                   225.07647705078125f)));
+}
+
+TEST_CASE("ik keeps working after a lot of iterations", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/two_bone_ik.riv");
+    auto artboard = file->artboard();
+
+    REQUIRE(artboard->find<rive::Shape>("circle a") != nullptr);
+    auto circleA = artboard->find<rive::Shape>("circle a");
+
+    REQUIRE(artboard->find<rive::Shape>("circle b") != nullptr);
+    auto circleB = artboard->find<rive::Shape>("circle b");
+
+    REQUIRE(artboard->find<rive::Bone>("a") != nullptr);
+    auto boneA = artboard->find<rive::Bone>("a");
+
+    REQUIRE(artboard->find<rive::Bone>("b") != nullptr);
+    auto boneB = artboard->find<rive::Bone>("b");
+
+    REQUIRE(artboard->find<rive::Node>("target") != nullptr);
+    auto target = artboard->find<rive::Node>("target");
+
+    REQUIRE(artboard->animation("Animation 1") != nullptr);
+    auto animation = artboard->animation("Animation 1");
+
+    // Make sure dependency structure is correct. Important thing here is to
+    // ensure that circle a is dependent upon the tip of the ik chain (bone b).
+    // circle b is a child of bone b so it'll be there anyway, but may as well
+    // validate.
+    REQUIRE(std::find(boneB->dependents().begin(), boneB->dependents().end(), circleA) !=
+            boneB->dependents().end());
+    REQUIRE(std::find(boneB->dependents().begin(), boneB->dependents().end(), circleB) !=
+            boneB->dependents().end());
+
+    for (int i = 0; i < 1000; i++)
+    {
+        animation->apply(artboard, 0.0f, 1.0f);
+        artboard->advance(0.0f);
+        REQUIRE(target->x() == 296.0f);
+        REQUIRE(target->y() == 202.0f);
+        REQUIRE(aboutEqual(boneA->worldTransform(),
+                           rive::Mat2D(0.11632211506366729736328125f,
+                                       -0.993211567401885986328125f,
+                                       0.993211567401885986328125f,
+                                       0.11632211506366729736328125f,
+                                       26.015254974365234375f,
+                                       475.2149658203125f)));
+
+        REQUIRE(aboutEqual(boneB->worldTransform(),
+                           rive::Mat2D(0.974071562290191650390625f,
+                                       0.2262403070926666259765625f,
+                                       -0.2262403070926666259765625f,
+                                       0.974071562290191650390625f,
+                                       64.31568145751953125f,
+                                       148.1883544921875f)));
+
+        animation->apply(artboard, 1.0f, 1.0f);
+        artboard->advance(0.0f);
+        REQUIRE(target->x() == 450.0f);
+        REQUIRE(target->y() == 337.0f);
+        REQUIRE(aboutEqual(boneA->worldTransform(),
+                           rive::Mat2D(0.650279819965362548828125f,
+                                       -0.7596948146820068359375f,
+                                       0.7596948146820068359375f,
+                                       0.650279819965362548828125f,
+                                       26.015254974365234375f,
+                                       475.2149658203125f)));
+
+        REQUIRE(aboutEqual(boneB->worldTransform(),
+                           rive::Mat2D(0.8823678493499755859375f,
+                                       0.470560371875762939453125f,
+                                       -0.47056043148040771484375f,
+                                       0.882367908954620361328125f,
+                                       240.1275634765625f,
+                                       225.07647705078125f)));
+    }
+}
\ No newline at end of file
diff --git a/test/image_asset_test.cpp b/test/image_asset_test.cpp
new file mode 100644
index 0000000..5c86ef4
--- /dev/null
+++ b/test/image_asset_test.cpp
@@ -0,0 +1,79 @@
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/shapes/clipping_shape.hpp>
+#include <rive/shapes/rectangle.hpp>
+#include <rive/shapes/image.hpp>
+#include <rive/assets/image_asset.hpp>
+#include <rive/relative_local_asset_loader.hpp>
+#include <utils/no_op_factory.hpp>
+#include <utils/no_op_renderer.hpp>
+#include "rive_file_reader.hpp"
+#include <catch.hpp>
+#include <cstdio>
+
+TEST_CASE("image assets loads correctly", "[assets]")
+{
+    auto file = ReadRiveFile("../../test/assets/walle.riv");
+
+    auto node = file->artboard()->find("walle");
+    REQUIRE(node != nullptr);
+    REQUIRE(node->is<rive::Image>());
+    auto walle = node->as<rive::Image>();
+    REQUIRE(walle->imageAsset() != nullptr);
+    REQUIRE(walle->imageAsset()->decodedByteSize == 218873);
+
+    auto eve_left = file->artboard()->find("eve_left");
+    REQUIRE(eve_left != nullptr);
+    REQUIRE(eve_left->is<rive::Image>());
+    REQUIRE(eve_left->as<rive::Image>()->imageAsset() != nullptr);
+    REQUIRE(eve_left->as<rive::Image>()->imageAsset()->decodedByteSize == 246825);
+
+    auto eve_right = file->artboard()->find("eve_right");
+    REQUIRE(eve_right != nullptr);
+    REQUIRE(eve_right->is<rive::Image>());
+    REQUIRE(eve_right->as<rive::Image>()->imageAsset() != nullptr);
+    REQUIRE(eve_right->as<rive::Image>()->imageAsset() != walle->imageAsset());
+    REQUIRE(eve_right->as<rive::Image>()->imageAsset() ==
+            eve_left->as<rive::Image>()->imageAsset());
+
+    file->artboard()->updateComponents();
+
+    rive::NoOpRenderer renderer;
+    file->artboard()->draw(&renderer);
+}
+
+TEST_CASE("out of band image assets loads correctly", "[assets]")
+{
+    rive::NoOpFactory gEmptyFactory;
+
+    std::string filename = "../../test/assets/out_of_band/walle.riv";
+    rive::RelativeLocalAssetLoader loader(filename);
+
+    auto file = ReadRiveFile(filename.c_str(), &gEmptyFactory, &loader);
+
+    auto node = file->artboard()->find("walle");
+    REQUIRE(node != nullptr);
+    REQUIRE(node->is<rive::Image>());
+    auto walle = node->as<rive::Image>();
+    REQUIRE(walle->imageAsset() != nullptr);
+    REQUIRE(walle->imageAsset()->decodedByteSize == 218873);
+
+    auto eve_left = file->artboard()->find("eve_left");
+    REQUIRE(eve_left != nullptr);
+    REQUIRE(eve_left->is<rive::Image>());
+    REQUIRE(eve_left->as<rive::Image>()->imageAsset() != nullptr);
+    REQUIRE(eve_left->as<rive::Image>()->imageAsset()->decodedByteSize == 246825);
+
+    auto eve_right = file->artboard()->find("eve_right");
+    REQUIRE(eve_right != nullptr);
+    REQUIRE(eve_right->is<rive::Image>());
+    REQUIRE(eve_right->as<rive::Image>()->imageAsset() != nullptr);
+    REQUIRE(eve_right->as<rive::Image>()->imageAsset() != walle->imageAsset());
+    REQUIRE(eve_right->as<rive::Image>()->imageAsset() ==
+            eve_left->as<rive::Image>()->imageAsset());
+
+    file->artboard()->updateComponents();
+
+    rive::NoOpRenderer renderer;
+    file->artboard()->draw(&renderer);
+}
diff --git a/test/image_decoders_test.cpp b/test/image_decoders_test.cpp
new file mode 100644
index 0000000..024115b
--- /dev/null
+++ b/test/image_decoders_test.cpp
@@ -0,0 +1,79 @@
+#include "rive_file_reader.hpp"
+#include "rive_testing.hpp"
+#include "rive/decoders/bitmap_decoder.hpp"
+
+TEST_CASE("png file decodes correctly", "[image-decoder]")
+{
+    auto file = ReadFile("../../test/assets/placeholder.png");
+    REQUIRE(file.size() == 1096);
+
+    auto bitmap = Bitmap::decode(file.data(), file.size());
+
+    REQUIRE(bitmap != nullptr);
+
+    REQUIRE(bitmap->width() == 226);
+    REQUIRE(bitmap->height() == 128);
+}
+
+TEST_CASE("jpeg file decodes correctly", "[image-decoder]")
+{
+    auto file = ReadFile("../../test/assets/open_source.jpg");
+    REQUIRE(file.size() == 8880);
+
+    auto bitmap = Bitmap::decode(file.data(), file.size());
+
+    REQUIRE(bitmap != nullptr);
+
+    REQUIRE(bitmap->width() == 350);
+    REQUIRE(bitmap->height() == 200);
+}
+
+#ifndef __APPLE__
+// Loading this particular jpeg image in CG causes a memory leak CGImageSourceCreateImageAtIndex
+// calls IIOReadPlugin::createInfoPtr which leaks
+TEST_CASE("bad jpeg file doesn't cause an overflow", "[image-decoder]")
+{
+    auto file = ReadFile("../../test/assets/bad.jpg");
+    REQUIRE(file.size() == 88731);
+
+    auto bitmap = Bitmap::decode(file.data(), file.size());
+
+    REQUIRE(bitmap != nullptr);
+
+    REQUIRE(bitmap->width() == 24566);
+    REQUIRE(bitmap->height() == 58278);
+}
+#endif
+
+TEST_CASE("bad png file doesn't cause an overflow", "[image-decoder]")
+{
+    auto file = ReadFile("../../test/assets/bad.png");
+    REQUIRE(file.size() == 534283);
+
+    auto bitmap = Bitmap::decode(file.data(), file.size());
+
+#ifdef __APPLE__
+    // Loading this bad PNG file in CG actually works and we do get an image albiet black
+    REQUIRE(bitmap != nullptr);
+
+    REQUIRE(bitmap->width() == 58278);
+    REQUIRE(bitmap->height() == 24566);
+#else
+    // Our decoders return null as we have an invalid header with bogus resolution and we want to
+    // avoid a potential attack vector
+    REQUIRE(bitmap == nullptr);
+#endif
+}
+
+TEST_CASE("webp file decodes correctly", "[image-decoder]")
+{
+    auto file = ReadFile("../../test/assets/1.webp");
+    REQUIRE(file.size() == 30320);
+
+    auto bitmap = Bitmap::decode(file.data(), file.size());
+
+    REQUIRE(bitmap != nullptr);
+
+    REQUIRE(bitmap->width() == 550);
+    REQUIRE(bitmap->height() == 368);
+}
\ No newline at end of file
diff --git a/test/image_mesh_test.cpp b/test/image_mesh_test.cpp
new file mode 100644
index 0000000..5ba568e
--- /dev/null
+++ b/test/image_mesh_test.cpp
@@ -0,0 +1,64 @@
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/shapes/clipping_shape.hpp>
+#include <rive/shapes/rectangle.hpp>
+#include <rive/shapes/image.hpp>
+#include <rive/shapes/mesh.hpp>
+#include <rive/assets/image_asset.hpp>
+#include <rive/relative_local_asset_loader.hpp>
+#include <utils/no_op_renderer.hpp>
+#include "rive_file_reader.hpp"
+#include <catch.hpp>
+#include <cstdio>
+
+TEST_CASE("image with mesh loads correctly", "[mesh]")
+{
+    auto file = ReadRiveFile("../../test/assets/tape.riv");
+
+    auto node = file->artboard()->find("Tape body.png");
+    REQUIRE(node != nullptr);
+    REQUIRE(node->is<rive::Image>());
+    auto tape = node->as<rive::Image>();
+    REQUIRE(tape->imageAsset() != nullptr);
+    REQUIRE(tape->imageAsset()->decodedByteSize == 70903);
+    REQUIRE(tape->mesh() != nullptr);
+    REQUIRE(tape->mesh()->vertices().size() == 24);
+    REQUIRE(tape->mesh()->indices()->size() == 31 * 3); // Expect 31 triangles.
+}
+
+TEST_CASE("duplicating a mesh shares the indices", "[mesh]")
+{
+    auto file = ReadRiveFile("../../test/assets/tape.riv");
+
+    auto instance1 = file->artboardDefault();
+    auto instance2 = file->artboardDefault();
+    auto instance3 = file->artboardDefault();
+
+    auto node1 = instance1->find("Tape body.png");
+    auto node2 = instance2->find("Tape body.png");
+    auto node3 = instance3->find("Tape body.png");
+    REQUIRE(node1 != nullptr);
+    REQUIRE(node2 != nullptr);
+    REQUIRE(node3 != nullptr);
+    REQUIRE(node1->is<rive::Image>());
+    REQUIRE(node2->is<rive::Image>());
+    REQUIRE(node3->is<rive::Image>());
+
+    auto tape1 = node1->as<rive::Image>();
+    auto tape2 = node2->as<rive::Image>();
+    auto tape3 = node3->as<rive::Image>();
+    REQUIRE(tape1->imageAsset() != nullptr);
+    REQUIRE(tape1->mesh() != nullptr);
+    REQUIRE(tape2->imageAsset() != nullptr);
+    REQUIRE(tape2->mesh() != nullptr);
+    REQUIRE(tape3->imageAsset() != nullptr);
+    REQUIRE(tape3->mesh() != nullptr);
+
+    REQUIRE(tape1->mesh()->indices()->size() == 31 * 3);
+    REQUIRE(tape2->mesh()->indices()->size() == 31 * 3);
+    REQUIRE(tape3->mesh()->indices()->size() == 31 * 3);
+
+    // Important part, make sure they're all actually the same reference.
+    REQUIRE(tape1->mesh()->indices() == tape2->mesh()->indices());
+    REQUIRE(tape2->mesh()->indices() == tape3->mesh()->indices());
+}
diff --git a/test/in_band_asset_load_test.cpp b/test/in_band_asset_load_test.cpp
new file mode 100644
index 0000000..e0ea22e
--- /dev/null
+++ b/test/in_band_asset_load_test.cpp
@@ -0,0 +1,117 @@
+#include <rive/bones/skin.hpp>
+#include <rive/bones/tendon.hpp>
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/span.hpp>
+#include <rive/shapes/clipping_shape.hpp>
+#include <rive/shapes/path_vertex.hpp>
+#include <rive/shapes/points_path.hpp>
+#include <rive/shapes/rectangle.hpp>
+#include <rive/shapes/shape.hpp>
+#include <rive/assets/file_asset.hpp>
+#include <rive/assets/image_asset.hpp>
+#include <rive/assets/font_asset.hpp>
+#include <rive/assets/font_asset.hpp>
+#include "utils/no_op_factory.hpp"
+#include "rive_file_reader.hpp"
+#include <catch.hpp>
+#include <cstdio>
+
+class PretendAssetLoader : public rive ::FileAssetLoader
+{
+
+public:
+    rive::FileAsset* attemptedAsset;
+
+    bool loadContents(rive::FileAsset& asset,
+                      rive::Span<const uint8_t> inBandBytes,
+                      rive::Factory* factory) override
+    {
+        attemptedAsset = &asset;
+        return true;
+    }
+};
+
+class RejectAssetLoader : public rive ::FileAssetLoader
+{
+
+public:
+    rive::FileAsset* attemptedAsset;
+
+    bool loadContents(rive::FileAsset& asset,
+                      rive::Span<const uint8_t> inBandBytes,
+                      rive::Factory* factory) override
+    {
+        return false;
+    }
+};
+
+TEST_CASE("Load asset with in-band image", "[asset]")
+{
+    auto file = ReadRiveFile("../../test/assets/in_band_asset.riv");
+
+    auto assets = file->assets();
+    REQUIRE(assets.size() == 1);
+    auto firstAsset = assets[0];
+    REQUIRE(firstAsset->is<rive::ImageAsset>());
+
+    // in band asset, no cdn uuid set
+    REQUIRE(firstAsset->cdnUuid().size() == 0);
+
+    // default value
+    REQUIRE(firstAsset->cdnBaseUrl() == "https://public.rive.app/cdn/uuid");
+
+    REQUIRE(firstAsset->uniqueFilename() == "1x1-45022.png");
+    REQUIRE(firstAsset->fileExtension() == "png");
+
+    // we load in band assets, so the decoded size >0
+    REQUIRE(firstAsset->as<rive::ImageAsset>()->decodedByteSize == 308);
+}
+
+TEST_CASE("Load asset with in-band image, passing responsibility to loader", "[asset]")
+{
+    auto loader = PretendAssetLoader();
+
+    // our Loader has not attempted to load any asset.
+    REQUIRE(loader.attemptedAsset == nullptr);
+
+    auto file = ReadRiveFile("../../test/assets/in_band_asset.riv", nullptr, &loader);
+
+    auto assets = file->assets();
+    REQUIRE(assets.size() == 1);
+    auto firstAsset = assets[0];
+    REQUIRE(firstAsset->is<rive::ImageAsset>());
+
+    // in band asset, no cdn uuid set
+    REQUIRE(firstAsset->cdnUuid().size() == 0);
+
+    // default value
+    REQUIRE(firstAsset->cdnBaseUrl() == "https://public.rive.app/cdn/uuid");
+
+    REQUIRE(firstAsset->uniqueFilename() == "1x1-45022.png");
+    REQUIRE(firstAsset->fileExtension() == "png");
+
+    // we do not load in band assets, so the decoded size is still 0
+    REQUIRE(firstAsset->as<rive::ImageAsset>()->decodedByteSize == 0);
+
+    // however our FileAssetLoader had a chance to load this asset.
+    // in band asset, no cdn uuid set
+    REQUIRE(loader.attemptedAsset->cdnUuid().size() == 0);
+
+    // default value
+    REQUIRE(loader.attemptedAsset->cdnBaseUrl() == "https://public.rive.app/cdn/uuid");
+
+    REQUIRE(loader.attemptedAsset->uniqueFilename() == "1x1-45022.png");
+    REQUIRE(loader.attemptedAsset->fileExtension() == "png");
+}
+
+TEST_CASE("Load asset with in-band image, rejecting the loading responsiblity as loader", "[asset]")
+{
+    auto loader = RejectAssetLoader();
+    auto file = ReadRiveFile("../../test/assets/in_band_asset.riv", nullptr, &loader);
+    auto assets = file->assets();
+    auto firstAsset = assets[0];
+    REQUIRE(firstAsset->is<rive::ImageAsset>());
+    // our loader does not handle loading the asset, so we load the in band contents.
+    REQUIRE(firstAsset->as<rive::ImageAsset>()->decodedByteSize == 308);
+}
diff --git a/test/instancing_test.cpp b/test/instancing_test.cpp
new file mode 100644
index 0000000..4100951
--- /dev/null
+++ b/test/instancing_test.cpp
@@ -0,0 +1,68 @@
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/shapes/clipping_shape.hpp>
+#include <rive/shapes/rectangle.hpp>
+#include <rive/shapes/shape.hpp>
+#include <utils/no_op_factory.hpp>
+#include <utils/no_op_renderer.hpp>
+#include "rive_file_reader.hpp"
+#include <catch.hpp>
+#include <cstdio>
+
+TEST_CASE("cloning an ellipse works", "[instancing]")
+{
+    auto file = ReadRiveFile("../../test/assets/circle_clips.riv");
+
+    auto node = file->artboard()->find<rive::Shape>("TopEllipse");
+    REQUIRE(node != nullptr);
+
+    auto clonedNode = node->clone()->as<rive::Shape>();
+    REQUIRE(node->x() == clonedNode->x());
+    REQUIRE(node->y() == clonedNode->y());
+
+    delete clonedNode;
+}
+
+TEST_CASE("instancing artboard clones clipped properties", "[instancing]")
+{
+    auto file = ReadRiveFile("../../test/assets/circle_clips.riv");
+
+    REQUIRE(!file->artboard()->isInstance());
+
+    auto artboard = file->artboardDefault();
+
+    REQUIRE(artboard->isInstance());
+
+    auto node = artboard->find("TopEllipse");
+    REQUIRE(node != nullptr);
+    REQUIRE(node->is<rive::Shape>());
+
+    auto shape = node->as<rive::Shape>();
+    REQUIRE(shape->clippingShapes().size() == 2);
+    REQUIRE(shape->clippingShapes()[0]->source()->name() == "ClipRect2");
+    REQUIRE(shape->clippingShapes()[1]->source()->name() == "BabyEllipse");
+
+    artboard->updateComponents();
+
+    rive::NoOpRenderer renderer;
+    artboard->draw(&renderer);
+}
+
+TEST_CASE("instancing artboard doesn't clone animations", "[instancing]")
+{
+    auto file = ReadRiveFile("../../test/assets/juice.riv");
+
+    auto artboard = file->artboardDefault();
+
+    REQUIRE(file->artboard()->animationCount() == artboard->animationCount());
+    REQUIRE(file->artboard()->firstAnimation() == artboard->firstAnimation());
+
+    rive::LinearAnimation::deleteCount = 0;
+    // Make sure no animations were deleted by deleting the instance.
+    REQUIRE(rive::LinearAnimation::deleteCount == 0);
+
+    size_t numberOfAnimations = file->artboard()->animationCount();
+    file.reset(nullptr);
+    // Now the animations should've been deleted.
+    REQUIRE(rive::LinearAnimation::deleteCount == numberOfAnimations);
+}
diff --git a/test/joystick_flags_test.cpp b/test/joystick_flags_test.cpp
new file mode 100644
index 0000000..2e064bb
--- /dev/null
+++ b/test/joystick_flags_test.cpp
@@ -0,0 +1,54 @@
+#include "rive/joystick.hpp"
+#include "rive/shapes/shape.hpp"
+#include "rive_file_reader.hpp"
+#include "rive_testing.hpp"
+
+TEST_CASE("joystick flags load as expected", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/joystick_flag_test.riv");
+    auto artboard = file->artboard();
+
+    auto invertX = artboard->find<rive::Joystick>("Invert X Joystick");
+    REQUIRE(invertX->isJoystickFlagged(rive::JoystickFlags::invertX));
+    REQUIRE(!invertX->isJoystickFlagged(rive::JoystickFlags::invertY));
+    REQUIRE(!invertX->isJoystickFlagged(rive::JoystickFlags::worldSpace));
+
+    invertX->x(0.0f);
+    invertX->apply(artboard);
+    REQUIRE(artboard->find<rive::Shape>("invert_x_rect")->x() == 350.0f);
+
+    invertX->x(1.0f);
+    invertX->apply(artboard);
+    REQUIRE(artboard->find<rive::Shape>("invert_x_rect")->x() == 300.0f);
+
+    invertX->x(-1.0f);
+    invertX->apply(artboard);
+    REQUIRE(artboard->find<rive::Shape>("invert_x_rect")->x() == 400.0f);
+
+    auto invertY = artboard->find<rive::Joystick>("Invert Y Joystick");
+    REQUIRE(!invertY->isJoystickFlagged(rive::JoystickFlags::invertX));
+    REQUIRE(invertY->isJoystickFlagged(rive::JoystickFlags::invertY));
+    REQUIRE(!invertY->isJoystickFlagged(rive::JoystickFlags::worldSpace));
+
+    invertY->y(0.0f);
+    invertY->apply(artboard);
+    REQUIRE(artboard->find<rive::Shape>("invert_y_ellipse")->x() == 425.0f);
+
+    invertY->y(1.0f);
+    invertY->apply(artboard);
+    REQUIRE(artboard->find<rive::Shape>("invert_y_ellipse")->x() == 400.0f);
+
+    invertY->y(-1.0f);
+    invertY->apply(artboard);
+    REQUIRE(artboard->find<rive::Shape>("invert_y_ellipse")->x() == 450.0f);
+
+    auto world = artboard->find<rive::Joystick>("World Joystick");
+    REQUIRE(!world->isJoystickFlagged(rive::JoystickFlags::invertX));
+    REQUIRE(!world->isJoystickFlagged(rive::JoystickFlags::invertY));
+    REQUIRE(world->isJoystickFlagged(rive::JoystickFlags::worldSpace));
+
+    auto normal = artboard->find<rive::Joystick>("Normal Joystick");
+    REQUIRE(!normal->isJoystickFlagged(rive::JoystickFlags::invertX));
+    REQUIRE(!normal->isJoystickFlagged(rive::JoystickFlags::invertY));
+    REQUIRE(!normal->isJoystickFlagged(rive::JoystickFlags::worldSpace));
+}
\ No newline at end of file
diff --git a/test/layout_test.cpp b/test/layout_test.cpp
new file mode 100644
index 0000000..e7176a1
--- /dev/null
+++ b/test/layout_test.cpp
@@ -0,0 +1,143 @@
+#include "rive/math/transform_components.hpp"
+#include "rive/shapes/rectangle.hpp"
+#include "rive/text/text.hpp"
+#include "utils/no_op_factory.hpp"
+#include "rive_file_reader.hpp"
+#include "rive_testing.hpp"
+#include <catch.hpp>
+#include <cstdio>
+
+TEST_CASE("LayoutComponent FlexDirection row", "[layout]")
+{
+    auto file = ReadRiveFile("../../test/assets/layout/layout_horizontal.riv");
+
+    auto artboard = file->artboard();
+
+    REQUIRE(artboard->find<rive::LayoutComponent>("LayoutComponent1") != nullptr);
+    auto target1 = artboard->find<rive::LayoutComponent>("LayoutComponent1");
+
+    REQUIRE(artboard->find<rive::LayoutComponent>("LayoutComponent2") != nullptr);
+    auto target2 = artboard->find<rive::LayoutComponent>("LayoutComponent2");
+
+    REQUIRE(artboard->find<rive::LayoutComponent>("LayoutComponent3") != nullptr);
+    auto target3 = artboard->find<rive::LayoutComponent>("LayoutComponent3");
+
+    artboard->advance(0.0f);
+    auto target1Components = target1->worldTransform().decompose();
+    auto target2Components = target2->worldTransform().decompose();
+    auto target3Components = target3->worldTransform().decompose();
+
+    REQUIRE(target1Components.x() == 0);
+    REQUIRE(target2Components.x() == 100);
+    REQUIRE(target3Components.x() == 200);
+    REQUIRE(target1Components.y() == 0);
+    REQUIRE(target2Components.y() == 0);
+    REQUIRE(target3Components.y() == 0);
+}
+
+TEST_CASE("LayoutComponent FlexDirection column", "[layout]")
+{
+    auto file = ReadRiveFile("../../test/assets/layout/layout_vertical.riv");
+
+    auto artboard = file->artboard();
+
+    REQUIRE(artboard->find<rive::LayoutComponent>("LayoutComponent1") != nullptr);
+    auto target1 = artboard->find<rive::LayoutComponent>("LayoutComponent1");
+
+    REQUIRE(artboard->find<rive::LayoutComponent>("LayoutComponent2") != nullptr);
+    auto target2 = artboard->find<rive::LayoutComponent>("LayoutComponent2");
+
+    REQUIRE(artboard->find<rive::LayoutComponent>("LayoutComponent3") != nullptr);
+    auto target3 = artboard->find<rive::LayoutComponent>("LayoutComponent3");
+
+    artboard->advance(0.0f);
+    auto target1Components = target1->worldTransform().decompose();
+    auto target2Components = target2->worldTransform().decompose();
+    auto target3Components = target3->worldTransform().decompose();
+
+    REQUIRE(target1Components.x() == 0);
+    REQUIRE(target2Components.x() == 0);
+    REQUIRE(target3Components.x() == 0);
+    REQUIRE(target1Components.y() == 0);
+    REQUIRE(target2Components.y() == 100);
+    REQUIRE(target3Components.y() == 200);
+}
+
+TEST_CASE("LayoutComponent FlexDirection row with gap", "[layout]")
+{
+    auto file = ReadRiveFile("../../test/assets/layout/layout_horizontal_gaps.riv");
+
+    auto artboard = file->artboard();
+
+    REQUIRE(artboard->find<rive::LayoutComponent>("LayoutComponent1") != nullptr);
+    auto target1 = artboard->find<rive::LayoutComponent>("LayoutComponent1");
+
+    REQUIRE(artboard->find<rive::LayoutComponent>("LayoutComponent2") != nullptr);
+    auto target2 = artboard->find<rive::LayoutComponent>("LayoutComponent2");
+
+    REQUIRE(artboard->find<rive::LayoutComponent>("LayoutComponent3") != nullptr);
+    auto target3 = artboard->find<rive::LayoutComponent>("LayoutComponent3");
+
+    artboard->advance(0.0f);
+    auto target1Components = target1->worldTransform().decompose();
+    auto target2Components = target2->worldTransform().decompose();
+    auto target3Components = target3->worldTransform().decompose();
+
+    REQUIRE(target1Components.x() == 0);
+    REQUIRE(target2Components.x() == 110);
+    REQUIRE(target3Components.x() == 220);
+    REQUIRE(target1Components.y() == 0);
+    REQUIRE(target2Components.y() == 0);
+    REQUIRE(target3Components.y() == 0);
+}
+
+TEST_CASE("LayoutComponent FlexDirection row with wrap", "[layout]")
+{
+    auto file = ReadRiveFile("../../test/assets/layout/layout_horizontal_wrap.riv");
+
+    auto artboard = file->artboard();
+
+    REQUIRE(artboard->find<rive::LayoutComponent>("LayoutComponent6") != nullptr);
+    auto target = artboard->find<rive::LayoutComponent>("LayoutComponent6");
+
+    artboard->advance(0.0f);
+    auto targetComponents = target->worldTransform().decompose();
+
+    REQUIRE(targetComponents.x() == 0);
+    REQUIRE(targetComponents.y() == 100);
+}
+
+TEST_CASE("LayoutComponent Center using alignItems and justifyContent", "[layout]")
+{
+    auto file = ReadRiveFile("../../test/assets/layout/layout_center.riv");
+
+    auto artboard = file->artboard();
+
+    REQUIRE(artboard->find<rive::LayoutComponent>("LayoutComponent1") != nullptr);
+    auto target = artboard->find<rive::LayoutComponent>("LayoutComponent1");
+
+    artboard->advance(0.0f);
+    auto targetComponents = target->worldTransform().decompose();
+
+    REQUIRE(targetComponents.x() == 200);
+    REQUIRE(targetComponents.y() == 200);
+}
+
+TEST_CASE("LayoutComponent with intrinsic size gets measured correctly", "[layout]")
+{
+    auto file = ReadRiveFile("../../test/assets/layout/measure_tests.riv");
+
+    auto artboard = file->artboard("hi");
+
+    REQUIRE(artboard->find<rive::LayoutComponent>("TextLayout") != nullptr);
+    REQUIRE(artboard->find<rive::Text>("HiText") != nullptr);
+
+    artboard->advance(0.0f);
+
+    auto text = artboard->find<rive::Text>("HiText");
+    auto bounds = text->localBounds();
+    REQUIRE(bounds.left() == 0);
+    REQUIRE(bounds.top() == 0);
+    REQUIRE(bounds.width() == 62.48047f);
+    REQUIRE(bounds.height() == 72.62695f);
+}
diff --git a/test/line_break_test.cpp b/test/line_break_test.cpp
new file mode 100644
index 0000000..34dad5b
--- /dev/null
+++ b/test/line_break_test.cpp
@@ -0,0 +1,419 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include <rive/simple_array.hpp>
+#include <catch.hpp>
+#include <rive/text_engine.hpp>
+#include <rive/text/font_hb.hpp>
+#include "rive/text/utf.hpp"
+
+using namespace rive;
+
+static rive::TextRun append(std::vector<rive::Unichar>* unichars,
+                            rive::rcp<rive::Font> font,
+                            float size,
+                            const char text[])
+{
+    const uint8_t* ptr = (const uint8_t*)text;
+    uint32_t n = 0;
+    while (*ptr)
+    {
+        unichars->push_back(rive::UTF::NextUTF8(&ptr));
+        n += 1;
+    }
+    return {std::move(font), size, -1.0f, 0.0f, n, 0};
+}
+
+static rcp<Font> loadFont(const char* filename)
+{
+    FILE* fp = fopen("../../test/assets/RobotoFlex.ttf", "rb");
+    REQUIRE(fp != nullptr);
+
+    fseek(fp, 0, SEEK_END);
+    const size_t length = ftell(fp);
+    fseek(fp, 0, SEEK_SET);
+    std::vector<uint8_t> bytes(length);
+    REQUIRE(fread(bytes.data(), 1, length, fp) == length);
+    fclose(fp);
+
+    return HBFont::Decode(bytes);
+}
+
+TEST_CASE("line breaker separates words", "[line break]")
+{
+    auto font = loadFont("../../test/assets/RobotoFlex.ttf");
+    REQUIRE(font != nullptr);
+
+    // one two⏎ three
+    std::vector<rive::TextRun> truns;
+    std::vector<rive::Unichar> unichars;
+    truns.push_back(append(&unichars, font, 32.0f, "one two three"));
+
+    auto paragraphs = font->shapeText(unichars, truns);
+    REQUIRE(paragraphs.size() == 1);
+    const auto& paragraph = paragraphs.front();
+    REQUIRE(paragraph.runs.size() == 1);
+    const auto& run = paragraph.runs.front();
+    REQUIRE(run.breaks.size() == 6);
+    REQUIRE(run.breaks[0] == 0);
+    REQUIRE(run.breaks[1] == 3);
+    REQUIRE(run.breaks[2] == 4);
+    REQUIRE(run.breaks[3] == 7);
+    REQUIRE(run.breaks[4] == 8);
+    REQUIRE(run.breaks[5] == 13);
+}
+
+TEST_CASE("line breaker handles multiple runs", "[line break]")
+{
+    auto font = loadFont("../../test/assets/RobotoFlex.ttf");
+    REQUIRE(font != nullptr);
+
+    std::vector<rive::TextRun> truns;
+    std::vector<rive::Unichar> unichars;
+    truns.push_back(append(&unichars, font, 32.0f, "one two thr"));
+    truns.push_back(append(&unichars, font, 60.0f, "ee four"));
+
+    auto paragraphs = font->shapeText(unichars, truns);
+    REQUIRE(paragraphs.size() == 1);
+    const auto& paragraph = paragraphs.front();
+
+    REQUIRE(paragraph.runs.size() == 2);
+    {
+        const auto& run = paragraph.runs.front();
+        REQUIRE(run.breaks.size() == 5);
+        REQUIRE(run.breaks[0] == 0);
+        REQUIRE(run.breaks[1] == 3);
+        REQUIRE(run.breaks[2] == 4);
+        REQUIRE(run.breaks[3] == 7);
+        REQUIRE(run.breaks[4] == 8);
+    }
+    {
+        const auto& run = paragraph.runs.back();
+        REQUIRE(run.breaks.size() == 3);
+        REQUIRE(run.breaks[0] == 2);
+        REQUIRE(run.breaks[1] == 3);
+        REQUIRE(run.breaks[2] == 7);
+    }
+}
+
+TEST_CASE("line breaker handles returns", "[line break]")
+{
+    auto font = loadFont("../../test/assets/RobotoFlex.ttf");
+    REQUIRE(font != nullptr);
+
+    std::vector<rive::TextRun> truns;
+    std::vector<rive::Unichar> unichars;
+    truns.push_back(append(&unichars, font, 32.0f, "one two thr"));
+    truns.push_back(append(&unichars, font, 60.0f, u8"ee\u2028 four"));
+
+    auto paragraphs = font->shapeText(unichars, truns);
+    const auto& paragraph = paragraphs.front();
+    REQUIRE(paragraph.runs.size() == 2);
+    {
+        const auto& run = paragraph.runs.front();
+        REQUIRE(run.breaks.size() == 5);
+        REQUIRE(run.breaks[0] == 0);
+        REQUIRE(run.breaks[1] == 3);
+        REQUIRE(run.breaks[2] == 4);
+        REQUIRE(run.breaks[3] == 7);
+        REQUIRE(run.breaks[4] == 8);
+    }
+    {
+        const auto& run = paragraph.runs.back();
+        REQUIRE(run.breaks.size() == 5);
+        REQUIRE(run.breaks[0] == 2);
+        REQUIRE(run.breaks[1] == 2);
+        REQUIRE(run.breaks[2] == 2);
+        REQUIRE(run.breaks[3] == 4);
+        REQUIRE(run.breaks[4] == 8);
+    }
+}
+
+TEST_CASE("line breaker builds lines", "[line break]")
+{
+    auto font = loadFont("../../test/assets/RobotoFlex.ttf");
+    REQUIRE(font != nullptr);
+
+    // one two⏎ three
+    std::vector<rive::TextRun> truns;
+    std::vector<rive::Unichar> unichars;
+    truns.push_back(append(&unichars, font, 32.0f, "one two three"));
+
+    auto paragraphs = font->shapeText(unichars, truns);
+    REQUIRE(paragraphs.size() == 1);
+    const auto& paragraph = paragraphs.front();
+    REQUIRE(paragraph.runs.size() == 1);
+
+    // at 194 everything fits in one line
+    {
+        auto lines = GlyphLine::BreakLines(paragraph.runs, 194.0f);
+        REQUIRE(lines.size() == 1);
+        auto line = lines.back();
+        REQUIRE(line.startRunIndex == 0);
+        REQUIRE(line.startGlyphIndex == 0);
+        REQUIRE(line.endRunIndex == 0);
+        REQUIRE(line.endGlyphIndex == 13);
+    }
+    // at 191 "three" should pop to second line
+    {
+        auto lines = GlyphLine::BreakLines(paragraph.runs, 191.0f);
+        REQUIRE(lines.size() == 2);
+        {
+            auto line = lines.front();
+            REQUIRE(line.startRunIndex == 0);
+            REQUIRE(line.startGlyphIndex == 0);
+            REQUIRE(line.endRunIndex == 0);
+            REQUIRE(line.endGlyphIndex == 7);
+        }
+
+        {
+            auto line = lines.back();
+            REQUIRE(line.startRunIndex == 0);
+            REQUIRE(line.startGlyphIndex == 8);
+            REQUIRE(line.endRunIndex == 0);
+            REQUIRE(line.endGlyphIndex == 13);
+        }
+    }
+}
+
+TEST_CASE("line breaker deals with extremes", "[line break]")
+{
+    auto font = loadFont("../../test/assets/RobotoFlex.ttf");
+    REQUIRE(font != nullptr);
+
+    // one two⏎ three
+    std::vector<rive::TextRun> truns;
+    std::vector<rive::Unichar> unichars;
+    truns.push_back(append(&unichars, font, 32.0f, "ab"));
+
+    auto paragraphs = font->shapeText(unichars, truns);
+    REQUIRE(paragraphs.size() == 1);
+    const auto& paragraph = paragraphs.front();
+    REQUIRE(paragraph.runs.size() == 1);
+
+    {
+        auto lines = GlyphLine::BreakLines(paragraph.runs, 17.0f);
+        REQUIRE(lines.size() == 2);
+        {
+            auto line = lines.front();
+            REQUIRE(line.startRunIndex == 0);
+            REQUIRE(line.startGlyphIndex == 0);
+            REQUIRE(line.endRunIndex == 0);
+            REQUIRE(line.endGlyphIndex == 1);
+        }
+
+        {
+            auto line = lines.back();
+            REQUIRE(line.startRunIndex == 0);
+            REQUIRE(line.startGlyphIndex == 1);
+            REQUIRE(line.endRunIndex == 0);
+            REQUIRE(line.endGlyphIndex == 2);
+        }
+    }
+    // Test that it also handles 0 width.
+    {
+        auto lines = GlyphLine::BreakLines(paragraph.runs, 0.0f);
+        REQUIRE(lines.size() == 2);
+        {
+            auto line = lines.front();
+            REQUIRE(line.startRunIndex == 0);
+            REQUIRE(line.startGlyphIndex == 0);
+            REQUIRE(line.endRunIndex == 0);
+            REQUIRE(line.endGlyphIndex == 1);
+        }
+
+        {
+            auto line = lines.back();
+            REQUIRE(line.startRunIndex == 0);
+            REQUIRE(line.startGlyphIndex == 1);
+            REQUIRE(line.endRunIndex == 0);
+            REQUIRE(line.endGlyphIndex == 2);
+        }
+    }
+}
+
+TEST_CASE("line breaker breaks return characters", "[line break]")
+{
+    auto font = loadFont("../../test/assets/RobotoFlex.ttf");
+    REQUIRE(font != nullptr);
+
+    // one two⏎ three
+    std::vector<rive::TextRun> truns;
+    std::vector<rive::Unichar> unichars;
+    truns.push_back(append(&unichars, font, 32.0f, u8"hello look\u2028here"));
+
+    auto paragraphs = font->shapeText(unichars, truns);
+    REQUIRE(paragraphs.size() == 1);
+    const auto& paragraph = paragraphs.front();
+    REQUIRE(paragraph.runs.size() == 1);
+    {
+        auto lines = GlyphLine::BreakLines(paragraph.runs, 300.0f);
+        REQUIRE(lines.size() == 2);
+    }
+}
+
+TEST_CASE("shaper separates paragraphs", "[shaper]")
+{
+    auto font = loadFont("../../test/assets/RobotoFlex.ttf");
+    REQUIRE(font != nullptr);
+
+    // one two⏎ three
+    std::vector<rive::TextRun> truns;
+    std::vector<rive::Unichar> unichars;
+    truns.push_back(append(&unichars, font, 32.0f, u8"hello look\u2028here\nsecond paragraph"));
+
+    auto paragraphs = font->shapeText(unichars, truns);
+    REQUIRE(paragraphs.size() == 2);
+    {
+        const auto& paragraph = paragraphs.front();
+        REQUIRE(paragraph.runs.size() == 1);
+        REQUIRE(paragraph.baseDirection == rive::TextDirection::ltr);
+
+        auto lines = GlyphLine::BreakLines(paragraph.runs, 300.0f);
+        REQUIRE(lines.size() == 2);
+    }
+    {
+        const auto& paragraph = paragraphs.back();
+        REQUIRE(paragraph.runs.size() == 1);
+        REQUIRE(paragraph.baseDirection == rive::TextDirection::ltr);
+
+        auto lines = GlyphLine::BreakLines(paragraph.runs, 300.0f);
+        REQUIRE(lines.size() == 1);
+    }
+}
+
+TEST_CASE("shaper handles RTL", "[shaper]")
+{
+    auto font = loadFont("../../test/assets/IBMPlexSansArabic-Regular.ttf");
+    REQUIRE(font != nullptr);
+
+    // one two⏎ three
+    std::vector<rive::TextRun> truns;
+    std::vector<rive::Unichar> unichars;
+    truns.push_back(append(&unichars, font, 32.0f, "لمفاتيح ABC DEF"));
+
+    auto paragraphs = font->shapeText(unichars, truns);
+    REQUIRE(paragraphs.size() == 1);
+    const auto& paragraph = paragraphs.front();
+    REQUIRE(paragraph.baseDirection == rive::TextDirection::rtl);
+    {
+        auto lines = GlyphLine::BreakLines(paragraph.runs, 300.0f);
+        REQUIRE(lines.size() == 1);
+    }
+    {
+        auto lines = GlyphLine::BreakLines(paragraph.runs, 196.0f);
+        REQUIRE(lines.size() == 2);
+
+        // The second line should start with DEF as it's the first word to wrap.
+        const auto& line = lines.back();
+        const auto& run = paragraph.runs[line.startRunIndex];
+        auto index = run.textIndices[line.startGlyphIndex];
+        REQUIRE(unichars[index] == 'D');
+        REQUIRE(unichars[index + 1] == 'E');
+        REQUIRE(unichars[index + 2] == 'F');
+    }
+}
+
+TEST_CASE("shaper handles empty space", "[shaper]")
+{
+    auto font = loadFont("../../test/assets/IBMPlexSansArabic-Regular.ttf");
+    REQUIRE(font != nullptr);
+
+    std::vector<rive::TextRun> truns;
+    std::vector<rive::Unichar> unichars;
+    truns.push_back(append(&unichars, font, 32.0f, " "));
+
+    auto paragraphs = font->shapeText(unichars, truns);
+    REQUIRE(paragraphs.size() == 1);
+    const auto& paragraph = paragraphs.front();
+    REQUIRE(paragraph.baseDirection == rive::TextDirection::ltr);
+    {
+        auto lines = GlyphLine::BreakLines(paragraph.runs, 300.0f);
+        REQUIRE(lines.size() == 1);
+    }
+}
+
+TEST_CASE("line breaker deals with empty paragraphs", "[line break]")
+{
+    auto font = loadFont("../../test/assets/IBMPlexSansArabic-Regular.ttf");
+    REQUIRE(font != nullptr);
+
+    std::vector<rive::TextRun> truns;
+    std::vector<rive::Unichar> unichars;
+    truns.push_back(append(&unichars, font, 32.0f, "hi\n "));
+
+    auto paragraphs = font->shapeText(unichars, truns);
+    REQUIRE(paragraphs.size() == 2);
+    {
+        const auto& paragraph = paragraphs.front();
+        REQUIRE(paragraph.baseDirection == rive::TextDirection::ltr);
+        {
+            auto lines = GlyphLine::BreakLines(paragraph.runs, -1.0f);
+            REQUIRE(lines.size() == 1);
+        }
+    }
+    {
+        const auto& paragraph = paragraphs.back();
+        REQUIRE(paragraph.baseDirection == rive::TextDirection::ltr);
+        {
+            auto lines = GlyphLine::BreakLines(paragraph.runs, -1.0f);
+            REQUIRE(lines.size() == 1);
+            auto line = lines.front();
+            REQUIRE(line.startRunIndex == 0);
+            REQUIRE(paragraph.runs[line.startRunIndex].glyphs.size() == 1);
+            REQUIRE(paragraph.runs[line.startRunIndex].textIndices.size() == 1);
+            REQUIRE(paragraph.runs[line.startRunIndex].textIndices[0] == 3);
+        }
+    }
+}
+
+TEST_CASE("line breaker deals with space only lines", "[line break]")
+{
+    auto font = loadFont("../../test/assets/IBMPlexSansArabic-Regular.ttf");
+    REQUIRE(font != nullptr);
+
+    std::vector<rive::TextRun> truns;
+    std::vector<rive::Unichar> unichars;
+    truns.push_back(append(&unichars, font, 32.0f, u8"hi\u2028 "));
+
+    auto paragraphs = font->shapeText(unichars, truns);
+    REQUIRE(paragraphs.size() == 1);
+    {
+        const auto& paragraph = paragraphs.front();
+        REQUIRE(paragraph.baseDirection == rive::TextDirection::ltr);
+        {
+            auto lines = GlyphLine::BreakLines(paragraph.runs, -1.0f);
+            REQUIRE(lines.size() == 2);
+        }
+    }
+}
+
+TEST_CASE("line breaker deals with empty lines", "[line break]")
+{
+    auto font = loadFont("../../test/assets/IBMPlexSansArabic-Regular.ttf");
+    REQUIRE(font != nullptr);
+
+    std::vector<rive::TextRun> truns;
+    std::vector<rive::Unichar> unichars;
+    truns.push_back(append(&unichars, font, 32.0f, "hi\n"));
+
+    auto paragraphs = font->shapeText(unichars, truns);
+    REQUIRE(paragraphs.size() == 1);
+    {
+        const auto& paragraph = paragraphs.front();
+        REQUIRE(paragraph.baseDirection == rive::TextDirection::ltr);
+        {
+            auto lines = GlyphLine::BreakLines(paragraph.runs, -1.0f);
+            REQUIRE(lines.size() == 1);
+            {
+                auto line = lines.front();
+                REQUIRE(line.startRunIndex == 0);
+                REQUIRE(line.startGlyphIndex == 0);
+                REQUIRE(paragraph.runs[line.startRunIndex].glyphs.size() == 3);
+                REQUIRE(paragraph.runs[line.startRunIndex].textIndices.size() == 3);
+                REQUIRE(paragraph.runs[line.startRunIndex].textIndices[0] == 0);
+            }
+        }
+    }
+}
diff --git a/test/linear_animation_instance_test.cpp b/test/linear_animation_instance_test.cpp
new file mode 100644
index 0000000..0e8a8a6
--- /dev/null
+++ b/test/linear_animation_instance_test.cpp
@@ -0,0 +1,410 @@
+#include <rive/animation/loop.hpp>
+#include <rive/animation/linear_animation.hpp>
+#include <rive/animation/linear_animation_instance.hpp>
+#include "utils/no_op_factory.hpp"
+#include <catch.hpp>
+#include <cstdio>
+
+TEST_CASE("LinearAnimationInstance oneShot", "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    // For each of these tests, we cons up a dummy artboard/instance
+    // just to make the animations happy.
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 5
+    linearAnimation->duration(10);
+    linearAnimation->fps(2);
+    linearAnimation->loopValue(static_cast<int>(rive::Loop::oneShot));
+
+    rive::LinearAnimationInstance* linearAnimationInstance =
+        new rive::LinearAnimationInstance(linearAnimation, abi.get());
+
+    // play from beginning.
+    bool continuePlaying = linearAnimationInstance->advance(2.0);
+    REQUIRE(continuePlaying == true);
+    REQUIRE(linearAnimationInstance->time() == 2.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 2.0);
+    REQUIRE(linearAnimationInstance->didLoop() == false);
+
+    // get stuck at end
+    continuePlaying = linearAnimationInstance->advance(10.0);
+    REQUIRE(linearAnimationInstance->time() == 5.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 12.0);
+    REQUIRE(linearAnimationInstance->didLoop() == true);
+    REQUIRE(continuePlaying == false);
+
+    delete linearAnimationInstance;
+    delete linearAnimation;
+}
+
+TEST_CASE("LinearAnimationInstance speed", "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    // For each of these tests, we cons up a dummy artboard/instance
+    // just to make the animations happy.
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 5
+    linearAnimation->duration(10);
+    linearAnimation->fps(2);
+    linearAnimation->speed(.5);
+
+    rive::LinearAnimationInstance* linearAnimationInstance =
+        new rive::LinearAnimationInstance(linearAnimation, abi.get());
+
+    // play from beginning.
+    bool continuePlaying = linearAnimationInstance->advance(2.0);
+    REQUIRE(continuePlaying == true);
+    REQUIRE(linearAnimationInstance->time() == 1.0f);
+    REQUIRE(linearAnimationInstance->totalTime() == 1.0f);
+
+    delete linearAnimationInstance;
+    delete linearAnimation;
+}
+
+TEST_CASE("LinearAnimationInstance negative advance adds absolute total time ", "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    // For each of these tests, we cons up a dummy artboard/instance
+    // just to make the animations happy.
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 5
+
+    linearAnimation->duration(10);
+    linearAnimation->fps(2);
+    linearAnimation->loopValue(static_cast<int>(rive::Loop::loop));
+
+    rive::LinearAnimationInstance* linearAnimationInstance =
+        new rive::LinearAnimationInstance(linearAnimation, abi.get());
+
+    // play from beginning.
+    bool continuePlaying = linearAnimationInstance->advance(-2.0);
+    REQUIRE(continuePlaying == true);
+    REQUIRE(linearAnimationInstance->time() == 3.0f);
+    REQUIRE(linearAnimationInstance->totalTime() == 2.0f);
+    REQUIRE(linearAnimationInstance->didLoop() == true);
+
+    delete linearAnimationInstance;
+    delete linearAnimation;
+}
+
+TEST_CASE("LinearAnimationInstance oneShot <-", "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 5
+    linearAnimation->duration(10);
+    linearAnimation->fps(2);
+    linearAnimation->loopValue(static_cast<int>(rive::Loop::oneShot));
+
+    rive::LinearAnimationInstance* linearAnimationInstance =
+        new rive::LinearAnimationInstance(linearAnimation, abi.get());
+    linearAnimationInstance->direction(-1);
+    REQUIRE(linearAnimationInstance->time() == 0.0);
+
+    // Advancing 2 seconds backwards will keep the "animation" time at 0
+    bool continuePlaying = linearAnimationInstance->advance(2.0);
+    REQUIRE(continuePlaying == false);
+    REQUIRE(linearAnimationInstance->time() == 0.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 2.0);
+    REQUIRE(linearAnimationInstance->didLoop() == true);
+
+    // set time to end..
+    // TODO: this also "resets" the total time. is that sensible?
+    linearAnimationInstance->time(5.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 5.0);
+    // TODO: get rid if we stop killing m_Direction
+    linearAnimationInstance->direction(-1);
+
+    // play from end to beginning
+    continuePlaying = linearAnimationInstance->advance(2.0);
+    REQUIRE(continuePlaying == true);
+    REQUIRE(linearAnimationInstance->time() == 3.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 7.0);
+    REQUIRE(linearAnimationInstance->didLoop() == false);
+
+    // get stuck at beginning
+    continuePlaying = linearAnimationInstance->advance(4.0);
+    REQUIRE(continuePlaying == false);
+    REQUIRE(linearAnimationInstance->time() == 0.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 11.0);
+    REQUIRE(linearAnimationInstance->didLoop() == true);
+
+    delete linearAnimationInstance;
+    delete linearAnimation;
+}
+
+TEST_CASE("LinearAnimationInstance loop ->", "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 5
+    linearAnimation->duration(10);
+    linearAnimation->fps(2);
+    linearAnimation->loopValue(static_cast<int>(rive::Loop::loop));
+
+    rive::LinearAnimationInstance* linearAnimationInstance =
+        new rive::LinearAnimationInstance(linearAnimation, abi.get());
+
+    // play from beginning.
+    bool continuePlaying = linearAnimationInstance->advance(2.0);
+    REQUIRE(continuePlaying == true);
+    REQUIRE(linearAnimationInstance->time() == 2.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 2.0);
+    REQUIRE(linearAnimationInstance->didLoop() == false);
+
+    // loop around a couple of times, back to the same spot
+    continuePlaying = linearAnimationInstance->advance(10.0);
+    REQUIRE(linearAnimationInstance->time() == 2.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 12.0);
+    REQUIRE(linearAnimationInstance->didLoop() == true);
+    REQUIRE(continuePlaying == true);
+
+    delete linearAnimationInstance;
+    delete linearAnimation;
+}
+
+TEST_CASE("LinearAnimationInstance loop <-", "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 5
+    linearAnimation->duration(10);
+    linearAnimation->fps(2);
+    linearAnimation->loopValue(static_cast<int>(rive::Loop::loop));
+
+    rive::LinearAnimationInstance* linearAnimationInstance =
+        new rive::LinearAnimationInstance(linearAnimation, abi.get());
+    linearAnimationInstance->direction(-1);
+    REQUIRE(linearAnimationInstance->time() == 0.0);
+
+    // Advancing 2 seconds backwards will get the "animation" time to 3
+    bool continuePlaying = linearAnimationInstance->advance(2.0);
+    REQUIRE(continuePlaying == true);
+    REQUIRE(linearAnimationInstance->direction() == -1);
+    REQUIRE(linearAnimationInstance->time() == 3.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 2.0);
+    REQUIRE(linearAnimationInstance->didLoop() == true);
+
+    // play without looping past the beginning.
+    continuePlaying = linearAnimationInstance->advance(2.0);
+    REQUIRE(continuePlaying == true);
+    REQUIRE(linearAnimationInstance->time() == 1.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 4.0);
+    REQUIRE(linearAnimationInstance->didLoop() == false);
+
+    // loop past beginning again
+    continuePlaying = linearAnimationInstance->advance(4.0);
+    REQUIRE(continuePlaying == true);
+    REQUIRE(linearAnimationInstance->time() == 2.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 8.0);
+    REQUIRE(linearAnimationInstance->didLoop() == true);
+
+    delete linearAnimationInstance;
+    delete linearAnimation;
+}
+
+TEST_CASE("LinearAnimationInstance loop <- work area", "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 50
+    linearAnimation->workStart(4);
+    linearAnimation->enableWorkArea(true);
+    linearAnimation->workEnd(10);
+    linearAnimation->duration(100);
+    linearAnimation->fps(2);
+    linearAnimation->loopValue(static_cast<int>(rive::Loop::loop));
+
+    rive::LinearAnimationInstance* linearAnimationInstance =
+        new rive::LinearAnimationInstance(linearAnimation, abi.get());
+    linearAnimationInstance->direction(-1);
+    REQUIRE(linearAnimationInstance->time() == 2.0);
+
+    // advancing by 0 will not change anything
+    // is expected to return continue playing true regardless
+    bool continuePlaying = linearAnimationInstance->advance(0.0);
+
+    REQUIRE(continuePlaying == true);
+    REQUIRE(linearAnimationInstance->direction() == -1);
+    REQUIRE(linearAnimationInstance->time() == 2.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 0.0);
+    REQUIRE(linearAnimationInstance->didLoop() == false);
+
+    // 2 more secs , 5s -> 3s
+    continuePlaying = linearAnimationInstance->advance(2.0);
+    REQUIRE(continuePlaying == true);
+    REQUIRE(linearAnimationInstance->direction() == -1);
+    REQUIRE(linearAnimationInstance->time() == 3.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 2.0);
+    REQUIRE(linearAnimationInstance->didLoop() == true);
+
+    // 2 more secs , 3s -> 1s, thats before start, so loops to 4s
+    continuePlaying = linearAnimationInstance->advance(2.0);
+    REQUIRE(continuePlaying == true);
+    REQUIRE(linearAnimationInstance->direction() == -1);
+    REQUIRE(linearAnimationInstance->time() == 4.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 4.0);
+    REQUIRE(linearAnimationInstance->didLoop() == true);
+
+    // another hit, 4->2s, thats at the start, loops to 5s
+    continuePlaying = linearAnimationInstance->advance(2.0);
+    REQUIRE(continuePlaying == true);
+    REQUIRE(linearAnimationInstance->time() == 5.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 6.0);
+    REQUIRE(linearAnimationInstance->didLoop() == true);
+
+    delete linearAnimationInstance;
+    delete linearAnimation;
+}
+
+TEST_CASE("LinearAnimationInstance pingpong ->", "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 5
+    linearAnimation->duration(10);
+    linearAnimation->fps(2);
+    linearAnimation->loopValue(static_cast<int>(rive::Loop::pingPong));
+
+    rive::LinearAnimationInstance* linearAnimationInstance =
+        new rive::LinearAnimationInstance(linearAnimation, abi.get());
+
+    // play from beginning.
+    bool continuePlaying = linearAnimationInstance->advance(2.0);
+    REQUIRE(continuePlaying == true);
+    REQUIRE(linearAnimationInstance->time() == 2.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 2.0);
+    REQUIRE(linearAnimationInstance->didLoop() == false);
+
+    // pingpong at the end and come back to 3.
+    continuePlaying = linearAnimationInstance->advance(5.0);
+    REQUIRE(linearAnimationInstance->time() == 3.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 7.0);
+    REQUIRE(linearAnimationInstance->direction() == -1);
+    REQUIRE(linearAnimationInstance->didLoop() == true);
+    REQUIRE(continuePlaying == true);
+    // pingpong at the start and come back to 6.
+    continuePlaying = linearAnimationInstance->advance(9.0);
+    REQUIRE(linearAnimationInstance->time() == 4.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 16.0);
+    REQUIRE(linearAnimationInstance->direction() == -1);
+    REQUIRE(linearAnimationInstance->didLoop() == true);
+    REQUIRE(continuePlaying == true);
+
+    // pingpong back around to 2
+    continuePlaying = linearAnimationInstance->advance(6.0);
+    REQUIRE(linearAnimationInstance->time() == 2.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 22.0);
+    REQUIRE(linearAnimationInstance->direction() == 1);
+    REQUIRE(linearAnimationInstance->didLoop() == true);
+    REQUIRE(continuePlaying == true);
+
+    // Loop around twice in a frame.
+    continuePlaying = linearAnimationInstance->advance(20.0);
+    REQUIRE(linearAnimationInstance->time() == 2.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 42.0);
+    REQUIRE(linearAnimationInstance->direction() == 1);
+    REQUIRE(linearAnimationInstance->didLoop() == true);
+    REQUIRE(continuePlaying == true);
+
+    delete linearAnimationInstance;
+    delete linearAnimation;
+}
+
+TEST_CASE("LinearAnimationInstance pingpong <-", "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 5
+    linearAnimation->duration(10);
+    linearAnimation->fps(2);
+    linearAnimation->loopValue(static_cast<int>(rive::Loop::pingPong));
+
+    rive::LinearAnimationInstance* linearAnimationInstance =
+        new rive::LinearAnimationInstance(linearAnimation, abi.get());
+    linearAnimationInstance->direction(-1);
+    REQUIRE(linearAnimationInstance->time() == 0.0);
+
+    // Advancing 2 seconds backwards, pongs immediately (and is acutally just
+    // like playing forwards)
+    bool continuePlaying = linearAnimationInstance->advance(2.0);
+    REQUIRE(continuePlaying == true);
+    REQUIRE(linearAnimationInstance->direction() == 1);
+    REQUIRE(linearAnimationInstance->time() == 2.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 2.0);
+    REQUIRE(linearAnimationInstance->didLoop() == true);
+
+    // pingpong at the end
+    continuePlaying = linearAnimationInstance->advance(4.0);
+    REQUIRE(continuePlaying == true);
+    REQUIRE(linearAnimationInstance->direction() == -1);
+    REQUIRE(linearAnimationInstance->time() == 4.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 6.0);
+    REQUIRE(linearAnimationInstance->didLoop() == true);
+
+    // just a normal advance, no loop
+    continuePlaying = linearAnimationInstance->advance(2.0);
+    REQUIRE(continuePlaying == true);
+    REQUIRE(linearAnimationInstance->direction() == -1);
+    REQUIRE(linearAnimationInstance->time() == 2.0);
+    REQUIRE(linearAnimationInstance->totalTime() == 8.0);
+    REQUIRE(linearAnimationInstance->didLoop() == false);
+
+    delete linearAnimationInstance;
+    delete linearAnimation;
+}
+
+TEST_CASE("LinearAnimationInstance override loop", "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 5
+    linearAnimation->duration(10);
+    linearAnimation->fps(2);
+    linearAnimation->loopValue(static_cast<int>(rive::Loop::oneShot));
+
+    rive::LinearAnimationInstance* linearAnimationInstance =
+        new rive::LinearAnimationInstance(linearAnimation, abi.get());
+
+    // Check the loop value is same as the animation's
+    REQUIRE(linearAnimationInstance->loopValue() == linearAnimation->loopValue());
+
+    // Override the loop type
+    linearAnimationInstance->loopValue(static_cast<int>(rive::Loop::pingPong));
+    REQUIRE(linearAnimationInstance->loopValue() != linearAnimation->loopValue());
+    REQUIRE(linearAnimationInstance->loopValue() == static_cast<int>(rive::Loop::pingPong));
+    REQUIRE(linearAnimationInstance->loop() == rive::Loop::pingPong);
+
+    delete linearAnimationInstance;
+    delete linearAnimation;
+}
diff --git a/test/linear_animation_test.cpp b/test/linear_animation_test.cpp
new file mode 100644
index 0000000..ef23235
--- /dev/null
+++ b/test/linear_animation_test.cpp
@@ -0,0 +1,164 @@
+#include "rive/artboard.hpp"
+#include "rive/animation/linear_animation.hpp"
+#include "rive/animation/linear_animation_instance.hpp"
+#include "rive/animation/keyed_callback_reporter.hpp"
+#include "utils/no_op_factory.hpp"
+#include "rive_file_reader.hpp"
+#include "rive_testing.hpp"
+#include "rive/shapes/shape.hpp"
+#include <catch.hpp>
+#include <cstdio>
+
+TEST_CASE("LinearAnimation with positive speed have normal start and end seconds", "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    // For each of these tests, we cons up a dummy artboard/instance
+    // just to make the animations happy.
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 5
+    linearAnimation->duration(10);
+    linearAnimation->fps(2);
+    linearAnimation->speed(1);
+
+    REQUIRE(linearAnimation->startSeconds() == 0.0);
+    REQUIRE(linearAnimation->endSeconds() == 5.0);
+
+    REQUIRE(linearAnimation->startTime() == 0.0);
+    REQUIRE(linearAnimation->endTime() == 5.0);
+    REQUIRE(linearAnimation->durationSeconds() == 5.0);
+
+    delete linearAnimation;
+}
+
+TEST_CASE("LinearAnimation with negative speed have reversed start and end seconds", "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    // For each of these tests, we cons up a dummy artboard/instance
+    // just to make the animations happy.
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    // duration in seconds is 5
+    linearAnimation->duration(10);
+    linearAnimation->fps(2);
+    linearAnimation->speed(-1);
+
+    REQUIRE(linearAnimation->startSeconds() == 0.0);
+    REQUIRE(linearAnimation->endSeconds() == 5.0);
+    REQUIRE(linearAnimation->startTime() == 5.0);
+    REQUIRE(linearAnimation->endTime() == 0.0);
+
+    REQUIRE(linearAnimation->durationSeconds() == 5.0);
+
+    delete linearAnimation;
+}
+
+TEST_CASE("quantize goes to whole frames", "[animation]")
+{
+    auto file = ReadRiveFile("../../test/assets/quantize_test.riv");
+    auto artboard = file->artboard();
+    auto animation = artboard->animation(0);
+    REQUIRE(animation->quantize());
+
+    auto shapes = artboard->find<rive::Shape>();
+    REQUIRE(shapes.size() == 1);
+    auto ellipse = shapes[0];
+
+    animation->apply(artboard, 0.0f);
+    REQUIRE(ellipse->x() == 0.0f);
+
+    // Animation has 5 frames and lasts 1 second. So going to 0.5 seconds should
+    // show frame 2.5 which should floor to frame 2's value of 160 on x. Without
+    // quantize this would be 200.
+    animation->apply(artboard, 0.5f);
+    REQUIRE(ellipse->x() == 160.0f);
+
+    // Make sure our assumption with quantize off is true.
+    animation->quantize(false);
+    animation->apply(artboard, 0.5f);
+    REQUIRE(ellipse->x() == 200.0f);
+}
+
+TEST_CASE("LinearAnimation reports when to keep going correctly", "[animation]")
+{
+    rive::NoOpFactory emptyFactory;
+    // For each of these tests, we cons up a dummy artboard/instance
+    // just to make the animations happy.
+    rive::Artboard ab(&emptyFactory);
+    auto abi = ab.instance();
+
+    rive::LinearAnimation* linearAnimation = new rive::LinearAnimation();
+    linearAnimation->duration(60);
+    linearAnimation->fps(60);
+    linearAnimation->speed(1);
+    linearAnimation->enableWorkArea(true);
+    linearAnimation->workStart(30);
+    linearAnimation->workEnd(42);
+
+    auto animationInstance = rive::LinearAnimationInstance(linearAnimation, abi.get());
+
+    REQUIRE(animationInstance.advance(0.0f));
+    REQUIRE(animationInstance.time() == 0.5f);
+    REQUIRE(animationInstance.advance(0.1f));
+    REQUIRE(animationInstance.time() == 0.6f);
+    REQUIRE(!animationInstance.advance(0.2f));
+    REQUIRE(animationInstance.time() == 0.7f);
+
+    delete linearAnimation;
+}
+
+class TestReporter : public rive::KeyedCallbackReporter
+{
+private:
+    std::vector<uint32_t> m_reportedObjects;
+
+public:
+    void reportKeyedCallback(uint32_t objectId, uint32_t propertyKey, float elapsedSeconds) override
+    {
+        m_reportedObjects.push_back(objectId);
+    }
+
+    size_t count() { return m_reportedObjects.size(); }
+};
+
+TEST_CASE("Looping timeline events load correctly and report", "[events]")
+{
+    auto file = ReadRiveFile("../../test/assets/looping_timeline_events.riv");
+
+    auto artboard = file->artboard()->instance();
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->animationCount() == 1);
+
+    auto animationInstance = artboard->animationAt(0);
+    REQUIRE(animationInstance != nullptr);
+
+    TestReporter reporter;
+    // Report (at frame 0) fires from 0-0.1f
+    animationInstance->advance(0.1f, &reporter);
+    REQUIRE(animationInstance->time() == 0.1f);
+    REQUIRE(reporter.count() == 1);
+
+    // Report (at frame 25) fires
+    animationInstance->advance(0.32f, &reporter);
+    REQUIRE(animationInstance->time() == 0.42f);
+    REQUIRE(reporter.count() == 2);
+
+    // No report from .42 to .72 seconds.
+    animationInstance->advance(0.3f, &reporter);
+    REQUIRE(animationInstance->time() == 0.72f);
+    REQUIRE(reporter.count() == 2);
+
+    // Report at 1s fires from .72 to 1.0 seconds.
+    animationInstance->advance(0.28f, &reporter);
+    REQUIRE(animationInstance->time() == 0.0f);
+    REQUIRE(reporter.count() == 3);
+
+    // All 3 events fire (and first one again too).
+    animationInstance->advance(1.01f, &reporter);
+    REQUIRE(animationInstance->time() == Approx(0.01f));
+    REQUIRE(reporter.count() == 7);
+}
\ No newline at end of file
diff --git a/test/listener_align_target_test.cpp b/test/listener_align_target_test.cpp
new file mode 100644
index 0000000..f41dbab
--- /dev/null
+++ b/test/listener_align_target_test.cpp
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include <rive/math/aabb.hpp>
+#include <rive/animation/state_machine_instance.hpp>
+#include <rive/animation/state_machine_input_instance.hpp>
+#include <rive/animation/nested_state_machine.hpp>
+#include <rive/shapes/ellipse.hpp>
+#include <rive/shapes/shape.hpp>
+#include "rive_file_reader.hpp"
+
+#include <catch.hpp>
+#include <cstdio>
+
+using namespace rive;
+
+TEST_CASE("align target with preserve offset off test", "[listener_align]")
+{
+    // The circle starts at coords 100, 100
+    // Once the pointer move has acted, the new coords should be 100, 51
+    auto file = ReadRiveFile("../../test/assets/align_target.riv");
+
+    auto artboard = file->artboard("preserve-inactive");
+    auto artboardInstance = artboard->instance();
+    auto stateMachine = artboard->stateMachine("align-state-machine");
+
+    REQUIRE(artboardInstance != nullptr);
+    REQUIRE(artboardInstance->stateMachineCount() == 1);
+
+    REQUIRE(stateMachine != nullptr);
+
+    rive::StateMachineInstance* stateMachineInstance =
+        new rive::StateMachineInstance(stateMachine, artboardInstance.get());
+
+    artboardInstance->advance(0.0f);
+    stateMachineInstance->advanceAndApply(0.0f);
+    REQUIRE(stateMachineInstance->needsAdvance() == true);
+    stateMachineInstance->advance(0.0f);
+    auto circle = stateMachineInstance->artboard()->find<rive::Shape>("circle");
+    REQUIRE(circle != nullptr);
+    stateMachineInstance->pointerMove(rive::Vec2D(100.0f, 50.0f));
+    stateMachineInstance->pointerMove(rive::Vec2D(100.0f, 51.0f));
+    stateMachineInstance->advanceAndApply(1.0f);
+    stateMachineInstance->advance(0.0f);
+    REQUIRE(circle->x() == 100.0f);
+    REQUIRE(circle->y() == 51.0f);
+    delete stateMachineInstance;
+}
+
+TEST_CASE("align target preserve offset test", "[listener_align]")
+{
+    // The circle starts at coords 100, 100
+    // Once the pointer move has acted, the new coords should be 100, 101
+    auto file = ReadRiveFile("../../test/assets/align_target.riv");
+
+    auto artboard = file->artboard("preserve-active");
+    auto artboardInstance = artboard->instance();
+    auto stateMachine = artboard->stateMachine("align-state-machine");
+
+    REQUIRE(artboardInstance != nullptr);
+    REQUIRE(artboardInstance->stateMachineCount() == 1);
+
+    REQUIRE(stateMachine != nullptr);
+
+    rive::StateMachineInstance* stateMachineInstance =
+        new rive::StateMachineInstance(stateMachine, artboardInstance.get());
+
+    artboardInstance->advance(0.0f);
+    stateMachineInstance->advanceAndApply(0.0f);
+    REQUIRE(stateMachineInstance->needsAdvance() == true);
+    stateMachineInstance->advance(0.0f);
+    auto circle = stateMachineInstance->artboard()->find<rive::Shape>("circle");
+    REQUIRE(circle != nullptr);
+    stateMachineInstance->pointerMove(rive::Vec2D(100.0f, 50.0f));
+    stateMachineInstance->pointerMove(rive::Vec2D(100.0f, 51.0f));
+    stateMachineInstance->advanceAndApply(1.0f);
+    stateMachineInstance->advance(0.0f);
+    REQUIRE(circle->x() == 100.0f);
+    REQUIRE(circle->y() == 101.0f);
+    delete stateMachineInstance;
+}
\ No newline at end of file
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/test/main_test.cpp b/test/main_test.cpp
new file mode 100644
index 0000000..37eab93
--- /dev/null
+++ b/test/main_test.cpp
@@ -0,0 +1,6 @@
+// The only purpose of this file is to DEFINE the catch config so it can include
+// main()
+
+#define CATCH_CONFIG_MAIN // This tells Catch to provide a main() - only do this
+                          // in one cpp file
+#include <catch.hpp>
\ No newline at end of file
diff --git a/test/mat2d_test.cpp b/test/mat2d_test.cpp
new file mode 100644
index 0000000..cc80713
--- /dev/null
+++ b/test/mat2d_test.cpp
@@ -0,0 +1,258 @@
+#include <catch.hpp>
+#include "rive/math/mat2d.hpp"
+#include "rive/math/math_types.hpp"
+
+namespace rive
+{
+TEST_CASE("mapPoints", "[Mat2D]")
+{
+    std::vector<Vec2D> testPts{{0, 0}, {1, 0}, {0, 1}, {-1, 0}, {0, -1}};
+    for (int i = 0; i < 100; ++i)
+    {
+        testPts.push_back(
+            {static_cast<float>(rand() % 201 - 100), static_cast<float>(rand() % 201 - 100)});
+    }
+    size_t n = testPts.size();
+    std::vector<Vec2D> dstPts(n);
+    std::vector<Vec2D> expectedPts(n);
+
+    auto checkMatrix = [&](Mat2D m) {
+        // Map a single point.
+        m.mapPoints(dstPts.data(), testPts.data() + 2, 1);
+        expectedPts[0] = m * testPts[2];
+        CHECK(dstPts[0] == expectedPts[0]);
+
+        // Map n - 1 points (ensures we test one even-length and one odd-length array).
+        m.mapPoints(dstPts.data(), testPts.data() + 1, n - 1);
+        for (size_t i = 0; i < n - 1; ++i)
+            expectedPts[i] = m * testPts[i + 1];
+        CHECK(std::equal(dstPts.begin(), dstPts.end() - 1, expectedPts.begin()));
+
+        // Map n points.
+        m.mapPoints(dstPts.data(), testPts.data(), n);
+        for (size_t i = 0; i < n; ++i)
+            expectedPts[i] = m * testPts[i];
+        CHECK(dstPts == expectedPts);
+    };
+    checkMatrix(Mat2D());
+    checkMatrix(Mat2D(1, 0, 0, 1, 2, -3));
+    checkMatrix(Mat2D(4, 0, 0, -5, 0, 0));
+    checkMatrix(Mat2D(4, 0, 0, 5, -6, 7));
+    checkMatrix(Mat2D(0, 8, 9, 0, 10, 11));
+    checkMatrix(Mat2D(-12, -13, -14, -15, -16, -17));
+    checkMatrix(Mat2D(18, 19, 20, 21, 22, 23));
+    checkMatrix(Mat2D(-25, 26, 27, -28, 29, -30));
+}
+
+// Check Mat2D::findMaxScale.
+TEST_CASE("findMaxScale", "[Mat2D]")
+{
+    Mat2D identity;
+    // CHECK(1 == identity.getMinScale());
+    CHECK(1 == identity.findMaxScale());
+    // success = identity.getMinMaxScales(scales);
+    // CHECK(success && 1 == scales[0] && 1 == scales[1]);
+
+    Mat2D scale = Mat2D::fromScale(2, 4);
+    // CHECK(2 == scale.getMinScale());
+    CHECK(4 == scale.findMaxScale());
+    // success = scale.getMinMaxScales(scales);
+    // CHECK(success && 2 == scales[0] && 4 == scales[1]);
+
+    Mat2D transpose = Mat2D(0,
+                            3,
+                            6,
+                            0,
+                            std::numeric_limits<float>::quiet_NaN(),
+                            std::numeric_limits<float>::infinity());
+    CHECK(transpose.findMaxScale() == 6);
+
+    Mat2D rot90Scale = Mat2D::fromRotation(math::PI / 2);
+    rot90Scale = Mat2D::fromScale(1.f / 4, 1.f / 2) * rot90Scale;
+    // CHECK(1.f / 4 == rot90Scale.getMinScale());
+    CHECK(1.f / 2 == rot90Scale.findMaxScale());
+    // success = rot90Scale.getMinMaxScales(scales);
+    // CHECK(success && 1.f / 4 == scales[0] && 1.f / 2 == scales[1]);
+
+    Mat2D rotate = Mat2D::fromRotation(128 * math::PI / 180);
+    // CHECK(math::nearly_equal(1, rotate.getMinScale(), math::EPSILON));
+    CHECK(math::nearly_equal(1, rotate.findMaxScale(), math::EPSILON));
+    // success = rotate.getMinMaxScales(scales);
+    // CHECK(success);
+    // CHECK(math::nearly_equal(1, scales[0], math::EPSILON));
+    // CHECK(math::nearly_equal(1, scales[1], math::EPSILON));
+
+    Mat2D translate = Mat2D::fromTranslate(10, -5);
+    // CHECK(1 == translate.getMinScale());
+    CHECK(1 == translate.findMaxScale());
+    // success = translate.getMinMaxScales(scales);
+    // CHECK(success && 1 == scales[0] && 1 == scales[1]);
+
+    // skbug.com/4718
+    Mat2D big(2.39394089e+36f,
+              3.9159619e+36f,
+              8.85347779e+36f,
+              1.44823453e+37f,
+              9.26526204e+36f,
+              1.51559342e+37f);
+    CHECK(big.findMaxScale() == 0);
+
+    // // skbug.com/4718
+    // Mat2D givingNegativeNearlyZeros(0.00436534f,
+    //                                 0.00358857f,
+    //                                 0.114138f,
+    //                                 0.0936228f,
+    //                                 0.37141f,
+    //                                 -0.0174198f);
+    // success = givingNegativeNearlyZeros.getMinMaxScales(scales);
+    // CHECK(success && 0 == scales[0]);
+
+    constexpr int kNumBaseMats = 4;
+    Mat2D baseMats[kNumBaseMats] = {scale, rot90Scale, rotate, translate};
+    constexpr int kNumMats = 2 * kNumBaseMats;
+    Mat2D mats[kNumMats];
+    for (size_t i = 0; i < kNumBaseMats; ++i)
+    {
+        mats[i] = baseMats[i];
+        bool invertible = mats[i].invert(&mats[i + kNumBaseMats]);
+        REQUIRE(invertible);
+    }
+    srand(0);
+    for (int m = 0; m < 1000; ++m)
+    {
+        Mat2D mat;
+        for (int i = 0; i < 4; ++i)
+        {
+            int x = rand() % kNumMats;
+            mat = mats[x] * mat;
+        }
+
+        // float minScale = mat.findMinScale();
+        float maxScale = mat.findMaxScale();
+        // REQUIRE(minScale >= 0);
+        REQUIRE(maxScale >= 0);
+
+        // test a bunch of vectors. All should be scaled by between minScale and maxScale
+        // (modulo some error) and we should find a vector that is scaled by almost each.
+        static const float gVectorScaleTol = (105 * 1.f) / 100;
+        static const float gCloseScaleTol = (97 * 1.f) / 100;
+        float max = 0, min = std::numeric_limits<float>::max();
+        constexpr int kNumVectors = 1000;
+        Vec2D vectors[kNumVectors];
+        for (size_t i = 0; i < kNumVectors; ++i)
+        {
+            vectors[i].x = rand() * 2.f / static_cast<float>(RAND_MAX) - 1;
+            vectors[i].y = rand() * 2.f / static_cast<float>(RAND_MAX) - 1;
+            vectors[i] = vectors[i].normalized();
+            vectors[i] = {mat[0] * vectors[i].x + mat[2] * vectors[i].y,
+                          mat[1] * vectors[i].x + mat[3] * vectors[i].y};
+        }
+        for (size_t i = 0; i < kNumVectors; ++i)
+        {
+            float d = vectors[i].length();
+            REQUIRE(d / maxScale < gVectorScaleTol);
+            // REQUIRE(minScale / d < gVectorScaleTol);
+            if (max < d)
+            {
+                max = d;
+            }
+            if (min > d)
+            {
+                min = d;
+            }
+        }
+        REQUIRE(max / maxScale >= gCloseScaleTol);
+        // REQUIRE(minScale / min >= gCloseScaleTol);
+    }
+}
+
+TEST_CASE("mapBoundingBox", "[Mat2D]")
+{
+    const std::vector<Vec2D> testPts{{0, 0}, {1, 0}, {0, 1}, {-1, 0}, {0, -1}, {1, 1}, {-1, -1}};
+    std::vector<Vec2D> mappedPts(testPts.size());
+    auto checkMatrix = [&](Mat2D m) {
+        // Check zero points.
+        AABB mappedBbox = m.mapBoundingBox(nullptr, 0);
+        CHECK(mappedBbox.left() == 0);
+        CHECK(mappedBbox.top() == 0);
+        CHECK(mappedBbox.right() == 0);
+        CHECK(mappedBbox.bottom() == 0);
+
+        // Find a single point boinding box.
+        for (const Vec2D pt : testPts)
+        {
+            mappedBbox = m.mapBoundingBox(&pt, 1);
+            Vec2D mappedPt;
+            m.mapPoints(&mappedPt, &pt, 1);
+            CHECK(mappedBbox.left() == Approx(mappedPt.x));
+            CHECK(mappedBbox.top() == Approx(mappedPt.y));
+            CHECK(mappedBbox.right() == Approx(mappedPt.x));
+            CHECK(mappedBbox.bottom() == Approx(mappedPt.y));
+        }
+
+        // Check n - 1 points (ensures we test one even-length and one odd-length array).
+        m.mapPoints(mappedPts.data(), testPts.data() + 1, testPts.size() - 1);
+        mappedBbox = m.mapBoundingBox(testPts.data() + 1, testPts.size() - 1);
+        AABB testBbox = {1e9f, 1e9f, -1e9f, -1e9f};
+        for (size_t i = 0; i < testPts.size() - 1; ++i)
+        {
+            AABB::expandTo(testBbox, mappedPts[i]);
+        }
+        CHECK(mappedBbox.left() == Approx(testBbox.left()));
+        CHECK(mappedBbox.top() == Approx(testBbox.top()));
+        CHECK(mappedBbox.right() == Approx(testBbox.right()));
+        CHECK(mappedBbox.bottom() == Approx(testBbox.bottom()));
+
+        // Check n points.
+        m.mapPoints(mappedPts.data(), testPts.data(), testPts.size());
+        mappedBbox = m.mapBoundingBox(testPts.data(), testPts.size());
+        testBbox = {1e9f, 1e9f, -1e9f, -1e9f};
+        for (size_t i = 0; i < testPts.size(); ++i)
+        {
+            AABB::expandTo(testBbox, mappedPts[i]);
+        }
+        CHECK(mappedBbox.left() == Approx(testBbox.left()));
+        CHECK(mappedBbox.top() == Approx(testBbox.top()));
+        CHECK(mappedBbox.right() == Approx(testBbox.right()));
+        CHECK(mappedBbox.bottom() == Approx(testBbox.bottom()));
+
+        // Check mapping of a bounding box.
+        Vec2D bboxPts[4] = {{0, 0}, {0, 1}, {1, 1}, {1, 0}};
+        AABB mappedBboxFromPts = m.mapBoundingBox(bboxPts, 4);
+        AABB mappedBboxFromAABB = m.mapBoundingBox(AABB{0, 0, 1, 1});
+        CHECK(mappedBboxFromPts.left() == Approx(mappedBboxFromAABB.left()));
+        CHECK(mappedBboxFromPts.top() == Approx(mappedBboxFromAABB.top()));
+        CHECK(mappedBboxFromPts.right() == Approx(mappedBboxFromAABB.right()));
+        CHECK(mappedBboxFromPts.bottom() == Approx(mappedBboxFromAABB.bottom()));
+    };
+    checkMatrix(Mat2D());
+    checkMatrix(Mat2D(1, 0, 0, 1, 2, -3));
+    checkMatrix(Mat2D(4, 0, 0, -5, 0, 0));
+    checkMatrix(Mat2D(4, 0, 0, 5, -6, 7));
+    checkMatrix(Mat2D(0, 8, 9, 0, 10, 11));
+    checkMatrix(Mat2D(-12, -13, -14, -15, -16, -17));
+    checkMatrix(Mat2D(18, 19, 20, 21, 22, 23));
+    checkMatrix(Mat2D(-25, 26, 27, -28, 29, -30));
+
+    // Mapping empty or NaN points returns 0.
+    CHECK(Mat2D().mapBoundingBox(nullptr, 0) == AABB());
+    auto nan = std::numeric_limits<float>::quiet_NaN();
+    CHECK(Mat2D().mapBoundingBox(AABB{nan, nan, nan, nan}) == AABB());
+
+    // NaN values are otherwise ignored.
+    CHECK(Mat2D().mapBoundingBox(AABB{-1, -1, 1, 1}) == AABB{-1, -1, 1, 1});
+    CHECK(Mat2D().mapBoundingBox(AABB{nan, -1, 1, 1}) == AABB{1, -1, 1, 1});
+    CHECK(Mat2D().mapBoundingBox(AABB{-1, nan, 1, 1}) == AABB{-1, 1, 1, 1});
+    CHECK(Mat2D().mapBoundingBox(AABB{-1, -1, nan, 1}) == AABB{-1, -1, -1, 1});
+    CHECK(Mat2D().mapBoundingBox(AABB{-1, -1, 1, nan}) == AABB{-1, -1, 1, -1});
+
+    // When AABB::height() inf - inf, the result is nan.
+    auto inf = std::numeric_limits<float>::infinity();
+    CHECK(Mat2D().mapBoundingBox(AABB{0, inf, 0, nan}).height() == 0);
+    CHECK(Mat2D().mapBoundingBox(AABB{0, -inf, 0, nan}).height() == 0);
+    CHECK(Mat2D().mapBoundingBox(AABB{inf, 0, nan, 0}).width() == 0);
+    CHECK(Mat2D().mapBoundingBox(AABB{-inf, 0, nan, 0}).width() == 0);
+    CHECK(Mat2D().mapBoundingBox(AABB{inf, 0, inf, 0}).width() == 0);
+    CHECK(Mat2D().mapBoundingBox(AABB{0, -inf, 0, -inf}).height() == 0);
+}
+} // namespace rive
diff --git a/test/math_test.cpp b/test/math_test.cpp
new file mode 100644
index 0000000..31e4a81
--- /dev/null
+++ b/test/math_test.cpp
@@ -0,0 +1,126 @@
+#include <catch.hpp>
+#include <rive/math/math_types.hpp>
+
+using namespace rive;
+
+constexpr float kInf = std::numeric_limits<float>::infinity();
+constexpr float kNaN = std::numeric_limits<float>::quiet_NaN();
+
+// Check math::ieee_float_divide.
+TEST_CASE("ieee_float_divide", "[math]")
+{
+    // Make sure we're doing division.
+    CHECK(math::ieee_float_divide(100, 10) == 10);
+
+    // Returns +/-Inf if b == 0.
+    CHECK(math::ieee_float_divide(5, 0) == kInf);
+    CHECK(math::ieee_float_divide(5, -0) == kInf);
+    CHECK(math::ieee_float_divide(-3, 0) == -kInf);
+    CHECK(math::ieee_float_divide(-3, -0) == -kInf);
+    CHECK(math::ieee_float_divide(kInf, 0) == kInf);
+    CHECK(math::ieee_float_divide(-kInf, 0) == -kInf);
+    CHECK(math::ieee_float_divide(kInf, -0) == kInf);
+    CHECK(math::ieee_float_divide(-kInf, -0) == -kInf);
+
+    // Returns 0 if b == +/-Inf.
+    CHECK(math::ieee_float_divide(1, kInf) == 0);
+    CHECK(math::ieee_float_divide(-100, kInf) == 0);
+    CHECK(math::ieee_float_divide(std::numeric_limits<float>::max(), kInf) == 0);
+    CHECK(math::ieee_float_divide(std::numeric_limits<float>::max(), -kInf) == 0);
+    CHECK(math::ieee_float_divide(-std::numeric_limits<float>::max(), -kInf) == 0);
+    CHECK(math::ieee_float_divide(-std::numeric_limits<float>::max(), kInf) == 0);
+    CHECK(math::ieee_float_divide(0, kInf) == 0);
+    CHECK(math::ieee_float_divide(0, -kInf) == 0);
+    CHECK(math::ieee_float_divide(-0, -kInf) == 0);
+    CHECK(math::ieee_float_divide(-0, kInf) == 0);
+
+    // Returns NaN if a and b are both zero.
+    CHECK(std::isnan(math::ieee_float_divide(0, 0)));
+    CHECK(std::isnan(math::ieee_float_divide(0, -0)));
+    CHECK(std::isnan(math::ieee_float_divide(-0, 0)));
+    CHECK(std::isnan(math::ieee_float_divide(-0, -0)));
+
+    // Returns NaN if b and are both infinite.
+    CHECK(std::isnan(math::ieee_float_divide(kInf, kInf)));
+    CHECK(std::isnan(math::ieee_float_divide(kInf, -kInf)));
+    CHECK(std::isnan(math::ieee_float_divide(-kInf, kInf)));
+    CHECK(std::isnan(math::ieee_float_divide(kInf, -kInf)));
+
+    // Returns NaN a or b is NaN.
+    CHECK(std::isnan(math::ieee_float_divide(kNaN, 1)));
+    CHECK(std::isnan(math::ieee_float_divide(kInf, kNaN)));
+}
+
+// Check math::bit_cast.
+TEST_CASE("bit_cast", "[math]")
+{
+    CHECK(math::bit_cast<float>(0x3f800000) == 1);
+    CHECK(math::bit_cast<float>((1u << 31) | 0x3f800000) == -1);
+    CHECK(math::bit_cast<float>(0x7f800000) == kInf);
+    CHECK(math::bit_cast<float>((1u << 31) | 0x7f800000) == -kInf);
+    CHECK(std::isnan(math::bit_cast<float>(0x7fc00000)));
+}
+
+// Check math::clz*
+TEST_CASE("clz", "[math]")
+{
+    CHECK(math::clz32(1) == 31);
+    CHECK(math::clz32(-1) == 0);
+    for (int i = 0; i < 32; ++i)
+    {
+        CHECK(math::clz32(1 << i) == 31 - i);
+        CHECK(math::clz32((1 << i) | (rand() & ((1 << i) - 1))) == 31 - i);
+    }
+
+    CHECK(math::clz64(1) == 63);
+    CHECK(math::clz64(-1) == 0);
+    for (int i = 0; i < 64; ++i)
+    {
+        CHECK(math::clz64(1ll << i) == 63 - i);
+        CHECK(math::clz64((1ll << i) | (rand() & ((1ll << i) - 1))) == 63 - i);
+    }
+}
+
+// Check math::rotateleft32
+TEST_CASE("rotateleft32", "[math]")
+{
+    CHECK(math::rotateleft32(0xabcdef01, 24) == 0x01abcdef);
+    CHECK(math::rotateleft32(0xffff0000, 16) == 0x0000ffff);
+}
+
+// Check math::msb.
+TEST_CASE("msb", "[math]")
+{
+    CHECK(math::msb(0) == 0);
+    CHECK(math::msb(1) == 1);
+    CHECK(math::msb(2) == 2);
+    CHECK(math::msb(3) == 2);
+    CHECK(math::msb(4) == 3);
+    CHECK(math::msb(5) == 3);
+    CHECK(math::msb(6) == 3);
+    CHECK(math::msb(7) == 3);
+    CHECK(math::msb(8) == 4);
+    CHECK(math::msb(9) == 4);
+    for (int i = 0; i < 29; ++i)
+    {
+        CHECK(math::msb(10 << i) == 4 + i);
+    }
+    CHECK(math::msb(0xffffffff) == 32);
+}
+
+// Check math::round_up_to_multiple_of.
+TEST_CASE("round_up_to_multiple_of", "[math]")
+{
+    CHECK(math::round_up_to_multiple_of<4>(0) == 0);
+    CHECK(math::round_up_to_multiple_of<4>(3) == 4);
+    CHECK(math::round_up_to_multiple_of<8>(16) == 16);
+    CHECK(math::round_up_to_multiple_of<8>(24) == 24);
+    CHECK(math::round_up_to_multiple_of<8>(25) == 32);
+    CHECK(math::round_up_to_multiple_of<8>(31) == 32);
+    CHECK(math::round_up_to_multiple_of<8>(32) == 32);
+    for (size_t i = 0; i < 10; ++i)
+    {
+        CHECK(math::round_up_to_multiple_of<1>(i) == i);
+    }
+    CHECK(math::round_up_to_multiple_of<2>(~size_t(0)) == 0);
+}
diff --git a/test/nested_artboard.cpp b/test/nested_artboard.cpp
new file mode 100644
index 0000000..fa88626
--- /dev/null
+++ b/test/nested_artboard.cpp
@@ -0,0 +1,66 @@
+#include <rive/solo.hpp>
+#include <rive/nested_artboard.hpp>
+#include <rive/shapes/rectangle.hpp>
+#include <rive/shapes/shape.hpp>
+#include <rive/animation/state_machine_instance.hpp>
+#include <rive/animation/nested_linear_animation.hpp>
+#include <rive/animation/nested_state_machine.hpp>
+#include "rive_file_reader.hpp"
+#include "rive_testing.hpp"
+#include <catch.hpp>
+#include <cstdio>
+#include <iostream>
+
+TEST_CASE("collapsed nested artboards do not advance", "[solo]")
+{
+    auto file = ReadRiveFile("../../test/assets/solos_with_nested_artboards.riv");
+
+    auto artboard = file->artboard("main-artboard")->instance();
+    artboard->advance(0.0f);
+    auto solos = artboard->find<rive::Solo>();
+    REQUIRE(solos.size() == 1);
+    auto stateMachine = artboard->stateMachineAt(0);
+    stateMachine->advanceAndApply(0.0f);
+    REQUIRE(stateMachine->needsAdvance() == true);
+    artboard->advance(0.75f);
+    // Testing whether the squares have moved in the artboard is an indirect way
+    // if checking whether the time of each artboard has advanced
+    // Unfortunately there is no way of accessing the time of the animations directly
+    auto redNestedArtboard = stateMachine->artboard()->find<rive::NestedArtboard>("red-artboard");
+    auto redNestedArtboardArtboard = redNestedArtboard->artboardInstance();
+    auto movingShapes = redNestedArtboardArtboard->find<rive::Shape>();
+    auto redRect = movingShapes.at(0);
+    REQUIRE(redRect->x() > 50);
+    auto greenNestedArtboard =
+        stateMachine->artboard()->find<rive::NestedArtboard>("green-artboard");
+    auto greenNestedArtboardArtboard = greenNestedArtboard->artboardInstance();
+    auto greenMovingShapes = greenNestedArtboardArtboard->find<rive::Shape>();
+    auto greenRect = greenMovingShapes.at(0);
+    REQUIRE(greenRect->x() == 50);
+}
+
+TEST_CASE("nested artboards with looping animations will keep main advanceAndApply advancing",
+          "[nested]")
+{
+    auto file = ReadRiveFile("../../test/assets/ball_test.riv");
+    auto artboard = file->artboard("Artboard")->instance();
+    artboard->advance(0.0f);
+    auto stateMachine = artboard->stateMachineAt(0);
+    REQUIRE(stateMachine->advanceAndApply(0.0f) == true);
+    REQUIRE(stateMachine->advanceAndApply(1.0f) == true);
+    REQUIRE(stateMachine->advanceAndApply(1.0f) == true);
+}
+TEST_CASE("nested artboards with one shot animations will not main advanceAndApply advancing",
+          "[nested]")
+{
+
+    auto file = ReadRiveFile("../../test/assets/ball_test.riv");
+    auto artboard = file->artboard("Artboard 2")->instance();
+    artboard->advance(0.0f);
+    auto stateMachine = artboard->stateMachineAt(0);
+    REQUIRE(stateMachine->advanceAndApply(0.0f) == true);
+    REQUIRE(stateMachine->advanceAndApply(0.9f) == true);
+    REQUIRE(stateMachine->advanceAndApply(0.1f) == true);
+    // nested artboards animation is 1s long
+    REQUIRE(stateMachine->advanceAndApply(0.1f) == false);
+}
\ No newline at end of file
diff --git a/test/nested_artboard_opacity_test.cpp b/test/nested_artboard_opacity_test.cpp
new file mode 100644
index 0000000..209f209
--- /dev/null
+++ b/test/nested_artboard_opacity_test.cpp
@@ -0,0 +1,27 @@
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/nested_artboard.hpp>
+#include <rive/shapes/paint/shape_paint.hpp>
+#include "rive_file_reader.hpp"
+
+TEST_CASE("Nested artboard background renders with opacity", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/nested_artboard_opacity.riv");
+
+    auto mainArtboard = file->artboard()->instance();
+    REQUIRE(mainArtboard->find("Parent Artboard") != nullptr);
+    auto artboard = mainArtboard->find<rive::Artboard>("Parent Artboard");
+    artboard->updateComponents();
+    REQUIRE(artboard->is<rive::Artboard>());
+    REQUIRE(artboard->find("Nested artboard container") != nullptr);
+    auto nestedArtboardContainer =
+        artboard->find<rive::NestedArtboard>("Nested artboard container");
+    REQUIRE(nestedArtboardContainer->artboardInstance() != nullptr);
+    auto nestedArtboard = nestedArtboardContainer->artboardInstance();
+    nestedArtboard->updateComponents();
+    auto paints = nestedArtboard->shapePaints();
+    REQUIRE(paints.size() == 1);
+    auto paint = paints[0];
+    REQUIRE(paint->is<rive::ShapePaint>());
+    REQUIRE(paint->renderOpacity() == 0.3275f);
+}
diff --git a/test/nested_input_test.cpp b/test/nested_input_test.cpp
new file mode 100644
index 0000000..a40b58c
--- /dev/null
+++ b/test/nested_input_test.cpp
@@ -0,0 +1,139 @@
+#include "rive/core/binary_reader.hpp"
+#include "rive/file.hpp"
+#include "rive/nested_artboard.hpp"
+#include "rive/animation/nested_bool.hpp"
+#include "rive/animation/nested_input.hpp"
+#include "rive/animation/nested_number.hpp"
+#include "rive/animation/nested_trigger.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+#include "catch.hpp"
+#include "rive_file_reader.hpp"
+#include <cstdio>
+
+TEST_CASE("validate nested boolean get/set", "[nestedInput]")
+{
+    auto file = ReadRiveFile("../../test/assets/runtime_nested_inputs.riv");
+
+    auto artboard = file->artboard("MainArtboard")->instance();
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->stateMachineCount() == 1);
+
+    // Test getting/setting boolean SMIInput via nested artboard path
+    auto boolInput = artboard->getBool("CircleOuterState", "CircleOuter");
+    auto smiInput = artboard->input("CircleOuterState", "CircleOuter");
+    auto smiBoolInput = static_cast<rive::SMIBool*>(smiInput);
+    auto nestedArtboard = artboard->nestedArtboard("CircleOuter");
+    auto nestedInput = nestedArtboard->input("CircleOuterState")->as<rive::NestedBool>();
+    REQUIRE(boolInput->value() == false);
+    REQUIRE(smiBoolInput->value() == false);
+    REQUIRE(nestedInput->nestedValue() == false);
+
+    boolInput->value(true);
+    REQUIRE(boolInput->value() == true);
+    REQUIRE(smiBoolInput->value() == true);
+    REQUIRE(nestedInput->nestedValue() == true);
+
+    smiBoolInput->value(false);
+    REQUIRE(boolInput->value() == false);
+    REQUIRE(smiBoolInput->value() == false);
+    REQUIRE(nestedInput->nestedValue() == false);
+
+    nestedInput->nestedValue(true);
+    REQUIRE(boolInput->value() == true);
+    REQUIRE(smiBoolInput->value() == true);
+    REQUIRE(nestedInput->nestedValue() == true);
+}
+
+TEST_CASE("validate nested number get/set", "[nestedInput]")
+{
+    auto file = ReadRiveFile("../../test/assets/runtime_nested_inputs.riv");
+
+    auto artboard = file->artboard("MainArtboard")->instance();
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->stateMachineCount() == 1);
+
+    // Test getting/setting number SMIInput via nested artboard path
+    auto numInput = artboard->getNumber("CircleOuterNumber", "CircleOuter");
+    auto smiInput = artboard->input("CircleOuterNumber", "CircleOuter");
+    auto smiNumInput = static_cast<rive::SMINumber*>(smiInput);
+    auto nestedArtboard = artboard->nestedArtboardAtPath("CircleOuter");
+    auto nestedInput = nestedArtboard->input("CircleOuterNumber")->as<rive::NestedNumber>();
+    REQUIRE(numInput->value() == 0);
+    REQUIRE(smiNumInput->value() == 0);
+    REQUIRE(nestedInput->nestedValue() == 0);
+
+    numInput->value(10);
+    REQUIRE(numInput->value() == 10);
+    REQUIRE(smiNumInput->value() == 10);
+    REQUIRE(nestedInput->nestedValue() == 10);
+
+    smiNumInput->value(5);
+    REQUIRE(numInput->value() == 5);
+    REQUIRE(smiNumInput->value() == 5);
+    REQUIRE(nestedInput->nestedValue() == 5);
+
+    nestedInput->nestedValue(99);
+    REQUIRE(numInput->value() == 99);
+    REQUIRE(smiNumInput->value() == 99);
+    REQUIRE(nestedInput->nestedValue() == 99);
+}
+
+TEST_CASE("validate nested trigger fire", "[nestedInput]")
+{
+    auto file = ReadRiveFile("../../test/assets/runtime_nested_inputs.riv");
+
+    auto artboard = file->artboard("MainArtboard")->instance();
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->stateMachineCount() == 1);
+
+    // Test getting/setting number SMIInput via nested artboard path
+    auto tInput = artboard->getTrigger("CircleOuterTrigger", "CircleOuter");
+    auto smiInput = artboard->input("CircleOuterTrigger", "CircleOuter");
+    auto smiTInput = static_cast<rive::SMITrigger*>(smiInput);
+    auto nestedArtboard = artboard->nestedArtboardAtPath("CircleOuter");
+    auto nestedInput = nestedArtboard->input("CircleOuterTrigger")->as<rive::NestedTrigger>();
+    auto nestedSMI = static_cast<rive::SMITrigger*>(nestedInput->input());
+    REQUIRE(tInput->didFire() == false);
+    REQUIRE(smiTInput->didFire() == false);
+    REQUIRE(nestedSMI->didFire() == false);
+
+    tInput->fire();
+    REQUIRE(tInput->didFire() == true);
+    REQUIRE(smiTInput->didFire() == true);
+    REQUIRE(nestedSMI->didFire() == true);
+}
+
+TEST_CASE("validate nested boolean get/set multiple nested artboards deep", "[nestedInput]")
+{
+    auto file = ReadRiveFile("../../test/assets/runtime_nested_inputs.riv");
+
+    auto artboard = file->artboard("MainArtboard")->instance();
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->stateMachineCount() == 1);
+
+    // Test getting/setting boolean SMIInput via nested artboard path
+    auto boolInput = artboard->getBool("CircleInnerState", "CircleOuter/CircleInner");
+    auto smiInput = artboard->input("CircleInnerState", "CircleOuter/CircleInner");
+    auto smiBoolInput = static_cast<rive::SMIBool*>(smiInput);
+    auto nestedArtboard = artboard->nestedArtboardAtPath("CircleOuter/CircleInner");
+    auto nestedInput = nestedArtboard->input("CircleInnerState")->as<rive::NestedBool>();
+    REQUIRE(boolInput->value() == false);
+    REQUIRE(smiBoolInput->value() == false);
+    REQUIRE(nestedInput->nestedValue() == false);
+
+    boolInput->value(true);
+    REQUIRE(boolInput->value() == true);
+    REQUIRE(smiBoolInput->value() == true);
+    REQUIRE(nestedInput->nestedValue() == true);
+
+    smiBoolInput->value(false);
+    REQUIRE(boolInput->value() == false);
+    REQUIRE(smiBoolInput->value() == false);
+    REQUIRE(nestedInput->nestedValue() == false);
+
+    nestedInput->nestedValue(true);
+    REQUIRE(boolInput->value() == true);
+    REQUIRE(smiBoolInput->value() == true);
+    REQUIRE(nestedInput->nestedValue() == true);
+}
\ No newline at end of file
diff --git a/test/nested_text_run_test.cpp b/test/nested_text_run_test.cpp
new file mode 100644
index 0000000..f79a6e6
--- /dev/null
+++ b/test/nested_text_run_test.cpp
@@ -0,0 +1,61 @@
+#include "rive/core/binary_reader.hpp"
+#include "rive/file.hpp"
+#include "rive/nested_artboard.hpp"
+#include "rive/text/text_value_run.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+#include "catch.hpp"
+#include "rive_file_reader.hpp"
+#include <cstdio>
+
+TEST_CASE("validate nested text get/set", "[nestedText]")
+{
+    auto file = ReadRiveFile("../../test/assets/runtime_nested_text_runs.riv");
+
+    auto artboard = file->artboard("ArtboardA")->instance();
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->stateMachineCount() == 1);
+
+    // Test getting/setting TextValueRun view nested artboard path one level deep
+
+    auto textRunB1 = artboard->getTextRun("ArtboardBRun", "ArtboardB-1");
+    REQUIRE(textRunB1->is<rive::TextValueRun>());
+    REQUIRE(textRunB1->text() == "Artboard B Run");
+
+    auto textRunB2 = artboard->getTextRun("ArtboardBRun", "ArtboardB-2");
+    REQUIRE(textRunB2->is<rive::TextValueRun>());
+    REQUIRE(textRunB2->text() == "Artboard B Run");
+
+    // Test getting/setting TextValueRun view nested artboard path two level deep
+
+    auto textRunB1C1 = artboard->getTextRun("ArtboardCRun", "ArtboardB-1/ArtboardC-1");
+    REQUIRE(textRunB1C1->is<rive::TextValueRun>());
+    REQUIRE(textRunB1C1->text() == "Artboard C Run");
+
+    auto textRunB1C2 = artboard->getTextRun("ArtboardCRun", "ArtboardB-1/ArtboardC-2");
+    REQUIRE(textRunB1C2->is<rive::TextValueRun>());
+    REQUIRE(textRunB1C2->text() == "Artboard C Run");
+
+    auto textRunB2C1 = artboard->getTextRun("ArtboardCRun", "ArtboardB-2/ArtboardC-1");
+    REQUIRE(textRunB2C1->is<rive::TextValueRun>());
+    REQUIRE(textRunB2C1->text() == "Artboard C Run");
+
+    auto textRunB2C2 = artboard->getTextRun("ArtboardCRun", "ArtboardB-2/ArtboardC-2");
+    REQUIRE(textRunB2C2->is<rive::TextValueRun>());
+    REQUIRE(textRunB2C2->text() == "Artboard C Run");
+
+    // Validate that text run values can be set
+
+    textRunB1->text("Artboard B1 Run Updated");
+    textRunB2->text("Artboard B2 Run Updated");
+    textRunB1C1->text("Artboard B1C1 Run Updated");
+    textRunB1C2->text("Artboard B1C2 Run Updated");
+    textRunB2C1->text("Artboard B2C1 Run Updated");
+    textRunB2C2->text("Artboard B2C2 Run Updated");
+    REQUIRE(textRunB1->text() == "Artboard B1 Run Updated");
+    REQUIRE(textRunB2->text() == "Artboard B2 Run Updated");
+    REQUIRE(textRunB1C1->text() == "Artboard B1C1 Run Updated");
+    REQUIRE(textRunB1C2->text() == "Artboard B1C2 Run Updated");
+    REQUIRE(textRunB2C1->text() == "Artboard B2C1 Run Updated");
+    REQUIRE(textRunB2C2->text() == "Artboard B2C2 Run Updated");
+}
\ No newline at end of file
diff --git a/test/node_test.cpp b/test/node_test.cpp
new file mode 100644
index 0000000..0fecbad
--- /dev/null
+++ b/test/node_test.cpp
@@ -0,0 +1,13 @@
+#include <catch.hpp>
+#include <rive/node.hpp>
+
+TEST_CASE("Node instances", "[core]") { REQUIRE(rive::Node().x() == 0.0f); }
+
+TEST_CASE("nodeX function return x value", "[node]")
+{
+    rive::Node* node = new rive::Node();
+    REQUIRE(node->x() == 0.0f);
+    node->x(2.0f);
+    REQUIRE(node->x() == 2.0f);
+    delete node;
+}
\ No newline at end of file
diff --git a/test/path_test.cpp b/test/path_test.cpp
new file mode 100644
index 0000000..a664075
--- /dev/null
+++ b/test/path_test.cpp
@@ -0,0 +1,431 @@
+#include <rive/artboard.hpp>
+#include <rive/file.hpp>
+#include <rive/math/circle_constant.hpp>
+#include <rive/node.hpp>
+#include <rive/shapes/ellipse.hpp>
+#include <rive/shapes/path_composer.hpp>
+#include <rive/shapes/rectangle.hpp>
+#include <rive/shapes/shape.hpp>
+#include <rive/shapes/clipping_shape.hpp>
+#include <utils/no_op_factory.hpp>
+#include <utils/no_op_renderer.hpp>
+#include <rive/solo.hpp>
+#include <rive/animation/linear_animation_instance.hpp>
+#include "rive_file_reader.hpp"
+#include "rive/math/path_types.hpp"
+#include <catch.hpp>
+#include <cstdio>
+
+// Need a specialized "noop" factory that does make an inspectable Path
+
+namespace
+{
+enum class TestPathCommandType
+{
+    MoveTo,
+    LineTo,
+    CubicTo,
+    Reset,
+    Close,
+    AddPath,
+};
+
+struct TestPathCommand
+{
+    TestPathCommandType command;
+    float x;
+    float y;
+    float inX;
+    float inY;
+    float outX;
+    float outY;
+};
+
+class TestRenderPath : public rive::RenderPath
+{
+public:
+    std::vector<TestPathCommand> commands;
+    void rewind() override
+    {
+        commands.emplace_back(
+            TestPathCommand{TestPathCommandType::Reset, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f});
+    }
+
+    void fillRule(rive::FillRule value) override {}
+    void addPath(rive::CommandPath* path, const rive::Mat2D& transform) override
+    {
+        commands.emplace_back(
+            TestPathCommand{TestPathCommandType::AddPath, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f});
+    }
+    void addRenderPath(rive::RenderPath* path, const rive::Mat2D& transform) override {}
+
+    void moveTo(float x, float y) override
+    {
+        commands.emplace_back(
+            TestPathCommand{TestPathCommandType::MoveTo, x, y, 0.0f, 0.0f, 0.0f, 0.0f});
+    }
+    void lineTo(float x, float y) override
+    {
+        commands.emplace_back(
+            TestPathCommand{TestPathCommandType::LineTo, x, y, 0.0f, 0.0f, 0.0f, 0.0f});
+    }
+    void cubicTo(float ox, float oy, float ix, float iy, float x, float y) override
+    {
+        commands.emplace_back(TestPathCommand{TestPathCommandType::CubicTo, x, y, ix, iy, ox, oy});
+    }
+    void close() override
+    {
+        commands.emplace_back(
+            TestPathCommand{TestPathCommandType::Close, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f});
+    }
+};
+
+class TestNoOpFactory : public rive::NoOpFactory
+{
+public:
+    rive::rcp<rive::RenderPath> makeEmptyRenderPath() override
+    {
+        return rive::make_rcp<TestRenderPath>();
+    }
+};
+} // namespace
+
+TEST_CASE("rectangle path builds expected commands", "[path]")
+{
+    TestNoOpFactory emptyFactory;
+    rive::Artboard artboard(&emptyFactory);
+    rive::Shape* shape = new rive::Shape();
+    rive::Rectangle* rectangle = new rive::Rectangle();
+
+    rectangle->x(0.0f);
+    rectangle->y(0.0f);
+    rectangle->width(100.0f);
+    rectangle->height(200.0f);
+    rectangle->cornerRadiusTL(0.0f);
+
+    artboard.addObject(&artboard);
+    artboard.addObject(shape);
+    artboard.addObject(rectangle);
+    rectangle->parentId(1);
+
+    REQUIRE(artboard.initialize() == rive::StatusCode::Ok);
+
+    artboard.advance(0.0f);
+
+    auto rawPath = rectangle->rawPath();
+
+    auto verbs = rawPath.verbs();
+    REQUIRE(verbs.size() == 6);
+    REQUIRE(verbs[0] == rive::PathVerb::move);
+    REQUIRE(verbs[1] == rive::PathVerb::line);
+    REQUIRE(verbs[2] == rive::PathVerb::line);
+    REQUIRE(verbs[3] == rive::PathVerb::line);
+    REQUIRE(verbs[4] == rive::PathVerb::line);
+    REQUIRE(verbs[5] == rive::PathVerb::close);
+}
+
+TEST_CASE("rounded rectangle path builds expected commands", "[path]")
+{
+    TestNoOpFactory emptyFactory;
+    rive::Artboard artboard(&emptyFactory);
+    rive::Shape* shape = new rive::Shape();
+    rive::Rectangle* rectangle = new rive::Rectangle();
+
+    rectangle->x(0.0f);
+    rectangle->y(0.0f);
+    rectangle->width(100.0f);
+    rectangle->height(200.0f);
+    rectangle->cornerRadiusTL(20.0f);
+    rectangle->linkCornerRadius(true);
+
+    artboard.addObject(&artboard);
+    artboard.addObject(shape);
+    artboard.addObject(rectangle);
+    rectangle->parentId(1);
+
+    artboard.initialize();
+
+    artboard.advance(0.0f);
+
+    auto rawPath = rectangle->rawPath();
+
+    // rewind
+    // moveTo
+    // cubic - for 1st corner
+
+    // lineTo, cubicTo for 2nd corner
+    // lineTo, cubicTo for 3rd corner
+    // lineTo, cubicTo for 4th corner
+
+    // close
+    auto verbs = rawPath.verbs();
+    REQUIRE(verbs.size() == 10);
+
+    // Init
+    REQUIRE(verbs[0] == rive::PathVerb::move);
+
+    // 1st
+    REQUIRE(verbs[1] == rive::PathVerb::cubic);
+
+    // 2nd
+    REQUIRE(verbs[2] == rive::PathVerb::line);
+    REQUIRE(verbs[3] == rive::PathVerb::cubic);
+
+    // 3rd
+    REQUIRE(verbs[4] == rive::PathVerb::line);
+    REQUIRE(verbs[5] == rive::PathVerb::cubic);
+
+    // 4th
+    REQUIRE(verbs[6] == rive::PathVerb::line);
+    REQUIRE(verbs[7] == rive::PathVerb::cubic);
+
+    REQUIRE(verbs[8] == rive::PathVerb::line);
+
+    REQUIRE(verbs[9] == rive::PathVerb::close);
+}
+
+TEST_CASE("ellipse path builds expected commands", "[path]")
+{
+    TestNoOpFactory emptyFactory;
+    rive::Artboard artboard(&emptyFactory);
+    rive::Ellipse* ellipse = new rive::Ellipse();
+    rive::Shape* shape = new rive::Shape();
+
+    ellipse->x(0.0f);
+    ellipse->y(0.0f);
+    ellipse->width(100.0f);
+    ellipse->height(200.0f);
+
+    artboard.addObject(&artboard);
+    artboard.addObject(shape);
+    artboard.addObject(ellipse);
+    ellipse->parentId(1);
+
+    artboard.initialize();
+
+    artboard.advance(0.0f);
+
+    auto path = ellipse->rawPath();
+
+    // rewind
+    // moveTo
+    // cubic - for 1st corner
+
+    // lineTo, cubicTo for 2nd corner
+    // lineTo, cubicTo for 3rd corner
+    // lineTo, cubicTo for 4th corner
+
+    // close
+
+    auto verbs = path.verbs();
+    auto points = path.points();
+    REQUIRE(verbs.size() == 6);
+
+    // Init
+    REQUIRE(verbs[0] == rive::PathVerb::move);
+    REQUIRE(points[0].x == 0.0f);
+    REQUIRE(points[0].y == -100.0f);
+
+    // 1st
+    REQUIRE(verbs[1] == rive::PathVerb::cubic);
+    REQUIRE(points[1].x == 50.0f * rive::circleConstant);
+    REQUIRE(points[1].y == -100.0f);
+    REQUIRE(points[2].x == 50.0f);
+    REQUIRE(points[2].y == -100.0f * rive::circleConstant);
+    REQUIRE(points[3].x == 50.0f);
+    REQUIRE(points[3].y == 0.0f);
+
+    // 2nd
+    REQUIRE(verbs[2] == rive::PathVerb::cubic);
+    REQUIRE(points[4].x == 50.0f);
+    REQUIRE(points[4].y == 100.0f * rive::circleConstant);
+    REQUIRE(points[5].x == 50.0f * rive::circleConstant);
+    REQUIRE(points[5].y == 100.0f);
+    REQUIRE(points[6].x == 0.0f);
+    REQUIRE(points[6].y == 100.0f);
+
+    // 3rd
+    REQUIRE(verbs[3] == rive::PathVerb::cubic);
+    REQUIRE(points[7].x == -50.0f * rive::circleConstant);
+    REQUIRE(points[7].y == 100.0f);
+    REQUIRE(points[8].x == -50.0f);
+    REQUIRE(points[8].y == 100.0f * rive::circleConstant);
+    REQUIRE(points[9].x == -50.0f);
+    REQUIRE(points[9].y == 0.0f);
+
+    // 4th
+    REQUIRE(verbs[4] == rive::PathVerb::cubic);
+    REQUIRE(points[10].x == -50.0f);
+    REQUIRE(points[10].y == -100.0f * rive::circleConstant);
+    REQUIRE(points[11].x == -50.0f * rive::circleConstant);
+    REQUIRE(points[11].y == -100.0f);
+    REQUIRE(points[12].x == 0.0f);
+    REQUIRE(points[12].y == -100.0f);
+
+    REQUIRE(verbs[5] == rive::PathVerb::close);
+}
+
+TEST_CASE("nested solo with shape expanded and path collapsed", "[path]")
+{
+    TestNoOpFactory emptyFactory;
+    auto file = ReadRiveFile("../../test/assets/solos_collapse_tests.riv", &emptyFactory);
+
+    auto artboard = file->artboard("test-1-shape-with-shape-and-path")->instance();
+    // Root-Shape
+    artboard->advance(0.0f);
+    auto rootShape = artboard->children()[0]->as<rive::Shape>();
+    REQUIRE(rootShape != nullptr);
+    REQUIRE(rootShape->name() == "Root-Shape");
+    auto s1 = artboard->find<rive::Solo>("Solo-1");
+    REQUIRE(s1 != nullptr);
+    auto solos = artboard->find<rive::Solo>();
+    REQUIRE(solos.size() == 1);
+    auto solo = solos[0];
+    REQUIRE(solo->children().size() == 2);
+    REQUIRE(solo->children()[0]->is<rive::Shape>());
+    REQUIRE(solo->children()[0]->name() == "Rectangle-shape");
+    REQUIRE(solo->children()[1]->is<rive::Path>());
+    REQUIRE(solo->children()[1]->name() == "Path-2");
+    auto rectangleShape = solo->children()[0]->as<rive::Shape>();
+    auto path = solo->children()[1]->as<rive::Path>();
+    REQUIRE(rectangleShape->isCollapsed() == false);
+    REQUIRE(path->isCollapsed() == true);
+
+    auto pathComposer = rootShape->pathComposer();
+    auto pathComposerPath = static_cast<TestRenderPath*>(pathComposer->localPath());
+    // Path is skipped and the nested shape forms its own drawable, so size is 0
+    REQUIRE(pathComposerPath->commands.size() == 0);
+}
+
+TEST_CASE("nested solo clipping with shape collapsed and path expanded", "[path]")
+{
+    TestNoOpFactory emptyFactory;
+    auto file = ReadRiveFile("../../test/assets/solos_collapse_tests.riv", &emptyFactory);
+
+    auto artboard = file->artboard("test-2-clip-with-shape-and-path")->instance();
+    // Root-Shape
+    artboard->advance(0.0f);
+    auto rectangleClip = artboard->find<rive::Shape>("Rectangle-clipped");
+    REQUIRE(rectangleClip != nullptr);
+    REQUIRE(rectangleClip->name() == "Rectangle-clipped");
+    auto s1 = artboard->find<rive::Solo>("Solo-name");
+    REQUIRE(s1 != nullptr);
+    auto solos = artboard->find<rive::Solo>();
+    REQUIRE(solos.size() == 1);
+    auto solo = solos[0];
+    REQUIRE(solo->children().size() == 2);
+    REQUIRE(solo->children()[0]->is<rive::Shape>());
+    REQUIRE(solo->children()[0]->name() == "Rectangle-shape");
+    REQUIRE(solo->children()[1]->is<rive::Path>());
+    REQUIRE(solo->children()[1]->name() == "Path-2");
+    auto rectangleShape = solo->children()[0]->as<rive::Shape>();
+    auto path = solo->children()[1]->as<rive::Path>();
+    REQUIRE(rectangleShape->isCollapsed() == true);
+    REQUIRE(path->isCollapsed() == false);
+
+    auto clippingShape = rectangleClip->clippingShapes()[0];
+    REQUIRE(clippingShape != nullptr);
+    auto clippingPath = static_cast<TestRenderPath*>(clippingShape->renderPath());
+    // One path is skipped, otherwise size would be 3
+    REQUIRE(clippingPath->commands.size() == 2);
+}
+
+TEST_CASE("nested solo clipping with animation", "[path]")
+{
+    TestNoOpFactory emptyFactory;
+    auto file = ReadRiveFile("../../test/assets/solos_collapse_tests.riv", &emptyFactory);
+
+    auto artboard = file->artboard("test-5-clip-with-group-and-path-and-shape")->instance();
+    artboard->advance(0.0f);
+    auto rectangleClip = artboard->find<rive::Shape>("Rectangle-clipped");
+    REQUIRE(rectangleClip != nullptr);
+    REQUIRE(rectangleClip->name() == "Rectangle-clipped");
+    auto clippingShape = rectangleClip->clippingShapes()[0];
+    REQUIRE(clippingShape != nullptr);
+    auto clippingPath = static_cast<TestRenderPath*>(clippingShape->renderPath());
+    REQUIRE(clippingPath != nullptr);
+    std::unique_ptr<rive::LinearAnimationInstance> animation = artboard->animationAt(0);
+    // First a single shape is drawn as part of the solo
+    REQUIRE(clippingPath->commands[0].command == TestPathCommandType::Reset);
+    REQUIRE(clippingPath->commands[1].command == TestPathCommandType::AddPath);
+    REQUIRE(clippingPath->commands.size() == 2);
+    animation->advanceAndApply(2.5f);
+    // Then a different shape is drawn as part of the solo
+    REQUIRE(clippingPath->commands[2].command == TestPathCommandType::Reset);
+    REQUIRE(clippingPath->commands[3].command == TestPathCommandType::AddPath);
+    REQUIRE(clippingPath->commands.size() == 4);
+    animation->advanceAndApply(2.5f);
+    // Then an empty group is focused, so there is no path drawn
+    REQUIRE(clippingPath->commands[4].command == TestPathCommandType::Reset);
+    REQUIRE(clippingPath->commands.size() == 5);
+    animation->advanceAndApply(2.5f);
+    // Then an empty group is focused, so there is no path drawn
+    REQUIRE(clippingPath->commands[5].command == TestPathCommandType::Reset);
+    REQUIRE(clippingPath->commands[6].command == TestPathCommandType::AddPath);
+    REQUIRE(clippingPath->commands.size() == 7);
+    animation->advanceAndApply(1.0f);
+    // Then back to the group so no path is added
+    REQUIRE(clippingPath->commands[7].command == TestPathCommandType::Reset);
+    REQUIRE(clippingPath->commands.size() == 8);
+    animation->advanceAndApply(2.5f);
+    // Finally a new shape is added
+    REQUIRE(clippingPath->commands[8].command == TestPathCommandType::Reset);
+    REQUIRE(clippingPath->commands[9].command == TestPathCommandType::AddPath);
+    REQUIRE(clippingPath->commands.size() == 10);
+}
+
+TEST_CASE("double nested solos clipping with animation", "[path]")
+{
+    TestNoOpFactory emptyFactory;
+    auto file = ReadRiveFile("../../test/assets/solos_collapse_tests.riv", &emptyFactory);
+
+    auto artboard = file->artboard("test-6-clip-with-nested-solos")->instance();
+    artboard->advance(0.0f);
+    auto rectangleClip = artboard->find<rive::Shape>("Rectangle-clipped");
+    REQUIRE(rectangleClip != nullptr);
+    REQUIRE(rectangleClip->name() == "Rectangle-clipped");
+    auto clippingShape = rectangleClip->clippingShapes()[0];
+    REQUIRE(clippingShape != nullptr);
+    auto clippingPath = static_cast<TestRenderPath*>(clippingShape->renderPath());
+    REQUIRE(clippingPath != nullptr);
+    std::unique_ptr<rive::LinearAnimationInstance> animation = artboard->animationAt(0);
+    // First a single shape is drawn as part of the solo
+    REQUIRE(clippingPath->commands[0].command == TestPathCommandType::Reset);
+    REQUIRE(clippingPath->commands[1].command == TestPathCommandType::AddPath);
+    REQUIRE(clippingPath->commands.size() == 2);
+    animation->advanceAndApply(1.5f); // 1.5s in timeline
+    // Changed nested group but path does not change.
+    // Reset and AddPath are called anyway because it is marked as dirty
+    REQUIRE(clippingPath->commands[2].command == TestPathCommandType::Reset);
+    REQUIRE(clippingPath->commands[3].command == TestPathCommandType::AddPath);
+    REQUIRE(clippingPath->commands.size() == 4);
+    animation->advanceAndApply(1.0f); // 2.5s in timeline
+    // Both solos are pointing to empty groups
+    REQUIRE(clippingPath->commands[4].command == TestPathCommandType::Reset);
+    REQUIRE(clippingPath->commands.size() == 5);
+    animation->advanceAndApply(1.0f); // 3.5s in timeline
+    // Nothing changes, it hasn't reached any new keyframe
+    REQUIRE(clippingPath->commands.size() == 5);
+    animation->advanceAndApply(1.0f); // 4.5s in timeline
+    // Outer solo is pointing to inner solo, but inner solo is pointing to empty group
+    REQUIRE(clippingPath->commands.size() == 5);
+    animation->advanceAndApply(1.0f); // 5.5s in timeline
+    // Outer solo is pointing to inner solo, inner solo is pointing to shape
+    REQUIRE(clippingPath->commands[5].command == TestPathCommandType::Reset);
+    REQUIRE(clippingPath->commands[6].command == TestPathCommandType::AddPath);
+    REQUIRE(clippingPath->commands.size() == 7);
+    animation->advanceAndApply(1.0f); // 6.5s in timeline
+    // Outer solo pointing to empty group
+    REQUIRE(clippingPath->commands[7].command == TestPathCommandType::Reset);
+    REQUIRE(clippingPath->commands.size() == 8);
+    animation->advanceAndApply(2.0f); // 8.5s in timeline
+    // Outer solo pointing to inner solo. Inner solo pointing to rect shape
+    REQUIRE(clippingPath->commands[8].command == TestPathCommandType::Reset);
+    REQUIRE(clippingPath->commands[9].command == TestPathCommandType::AddPath);
+    REQUIRE(clippingPath->commands.size() == 10);
+    animation->advanceAndApply(2.0f); // 10.5s in timeline
+    // Outer solo pointing to inner solo. Inner solo pointing to path
+    REQUIRE(clippingPath->commands[10].command == TestPathCommandType::Reset);
+    REQUIRE(clippingPath->commands[11].command == TestPathCommandType::AddPath);
+    REQUIRE(clippingPath->commands.size() == 12);
+}
diff --git a/test/raw_path_test.cpp b/test/raw_path_test.cpp
new file mode 100644
index 0000000..b804831
--- /dev/null
+++ b/test/raw_path_test.cpp
@@ -0,0 +1,454 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include <rive/math/aabb.hpp>
+#include <rive/math/raw_path.hpp>
+
+#include <catch.hpp>
+#include <cstdio>
+#include <limits>
+#include <tuple>
+
+using namespace rive;
+
+TEST_CASE("rawpath-basics", "[rawpath]")
+{
+    RawPath path;
+
+    REQUIRE(path.empty());
+    REQUIRE(path.bounds() == AABB{0, 0, 0, 0});
+
+    path.move({1, 2});
+    REQUIRE(!path.empty());
+    REQUIRE(path.bounds() == AABB{1, 2, 1, 2});
+
+    path = RawPath();
+    REQUIRE(path.empty());
+    REQUIRE(path.bounds() == AABB{0, 0, 0, 0});
+
+    path.move({1, -2});
+    path.line({3, 4});
+    path.line({-1, 5});
+    REQUIRE(!path.empty());
+    REQUIRE(path.bounds() == AABB{-1, -2, 3, 5});
+}
+
+TEST_CASE("rawpath-add-helpers", "[rawpath]")
+{
+    RawPath path;
+
+    path.addRect({1, 1, 5, 6});
+    REQUIRE(!path.empty());
+    REQUIRE(path.bounds() == AABB{1, 1, 5, 6});
+    REQUIRE(path.points().size() == 4);
+    REQUIRE(path.verbs().size() == 5); // move, line, line, line, close
+
+    path = RawPath();
+    path.addOval({0, 0, 3, 6});
+    REQUIRE(!path.empty());
+    REQUIRE(path.bounds() == AABB{0, 0, 3, 6});
+    REQUIRE(path.points().size() == 13);
+    REQUIRE(path.verbs().size() == 6); // move, cubic, cubic, cubic, cubic, close
+
+    const Vec2D pts[] = {
+        {1, 2},
+        {4, 5},
+        {3, 2},
+        {100, -100},
+    };
+    constexpr auto size = sizeof(pts) / sizeof(pts[0]);
+
+    for (auto isClosed : {false, true})
+    {
+        path = RawPath();
+        path.addPoly({pts, size}, isClosed);
+        REQUIRE(path.bounds() == AABB{1, -100, 100, 5});
+        REQUIRE(path.points().size() == size);
+        REQUIRE(path.verbs().size() == size + isClosed);
+
+        for (size_t i = 0; i < size; ++i)
+        {
+            REQUIRE(path.points()[i] == pts[i]);
+        }
+        REQUIRE(path.verbs()[0] == PathVerb::move);
+        for (size_t i = 1; i < size; ++i)
+        {
+            REQUIRE(path.verbs()[i] == PathVerb::line);
+        }
+        if (isClosed)
+        {
+            REQUIRE(path.verbs()[size] == PathVerb::close);
+        }
+    }
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+static void check_iter(RawPath::Iter& iter,
+                       const RawPath::Iter& end,
+                       PathVerb expectedVerb,
+                       std::vector<Vec2D> expectedPts)
+{
+    REQUIRE(iter != end);
+    PathVerb verb = iter.verb();
+    const Vec2D* pts = iter.pts();
+    REQUIRE(verb == expectedVerb);
+    switch (verb)
+    {
+        case PathVerb::move:
+            CHECK(expectedPts.size() == 1);
+            CHECK(pts[0] == iter.movePt());
+            break;
+        case PathVerb::line:
+            CHECK(expectedPts.size() == 2);
+            CHECK(pts == iter.linePts());
+            break;
+        case PathVerb::quad:
+            CHECK(expectedPts.size() == 3);
+            CHECK(pts == iter.quadPts());
+            break;
+        case PathVerb::cubic:
+            CHECK(expectedPts.size() == 4);
+            CHECK(pts == iter.cubicPts());
+            break;
+        case PathVerb::close:
+            CHECK(expectedPts.size() == 0);
+            CHECK(pts == iter.rawPtsPtr() - 1);
+            break;
+    }
+    for (size_t i = 0; i < expectedPts.size(); ++i)
+    {
+        CHECK(pts[i] == expectedPts[i]);
+    }
+    ++iter;
+}
+
+TEST_CASE("rawpath-iter", "[rawpath]")
+{
+    {
+        RawPath rp;
+        REQUIRE(rp.begin() == rp.end());
+    }
+    {
+        RawPath rp;
+        rp.moveTo(1, 2);
+        rp.lineTo(3, 4);
+        rp.quadTo(5, 6, 7, 8);
+        rp.cubicTo(9, 10, 11, 12, 13, 14);
+        rp.close();
+        auto iter = rp.begin();
+        auto end = rp.end();
+        check_iter(iter, end, PathVerb::move, {{1, 2}});
+        check_iter(iter, end, PathVerb::line, {{1, 2}, {3, 4}});
+        check_iter(iter, end, PathVerb::quad, {{3, 4}, {5, 6}, {7, 8}});
+        check_iter(iter, end, PathVerb::cubic, {{7, 8}, {9, 10}, {11, 12}, {13, 14}});
+        check_iter(iter, end, PathVerb::close, {});
+        REQUIRE(iter == end);
+
+        // Moves are never discarded.
+        rp.reset();
+        rp.moveTo(1, 2);
+        rp.moveTo(3, 4);
+        rp.moveTo(5, 6);
+        rp.close();
+        std::tie(iter, end) = std::make_tuple(rp.begin(), rp.end());
+        check_iter(iter, end, PathVerb::move, {{1, 2}});
+        check_iter(iter, end, PathVerb::move, {{3, 4}});
+        check_iter(iter, end, PathVerb::move, {{5, 6}});
+        check_iter(iter, end, PathVerb::close, {});
+        REQUIRE(iter == end);
+
+        // lineTo, quadTo, and cubicTo can inject implicit moveTos.
+        rp.rewind();
+        rp.close();                   // discarded
+        rp.close();                   // discarded
+        rp.close();                   // discarded
+        rp.close();                   // discarded
+        rp.lineTo(1, 2);              // injects moveTo(0, 0)
+        rp.close();                   // kept
+        rp.close();                   // discarded
+        rp.cubicTo(3, 4, 5, 6, 7, 8); // injects moveTo(0, 0)
+        rp.moveTo(9, 10);
+        rp.moveTo(11, 12);
+        rp.quadTo(13, 14, 15, 16);
+        rp.close();        // kept
+        rp.lineTo(17, 18); // injects moveTo(11, 12)
+        std::tie(iter, end) = std::make_tuple(rp.begin(), rp.end());
+        check_iter(iter, end, PathVerb::move, {{0, 0}});
+        check_iter(iter, end, PathVerb::line, {{0, 0}, {1, 2}});
+        check_iter(iter, end, PathVerb::close, {});
+        check_iter(iter, end, PathVerb::move, {{0, 0}});
+        check_iter(iter, end, PathVerb::cubic, {{0, 0}, {3, 4}, {5, 6}, {7, 8}});
+        check_iter(iter, end, PathVerb::move, {{9, 10}});
+        check_iter(iter, end, PathVerb::move, {{11, 12}});
+        check_iter(iter, end, PathVerb::quad, {{11, 12}, {13, 14}, {15, 16}});
+        check_iter(iter, end, PathVerb::close, {});
+        check_iter(iter, end, PathVerb::move, {{11, 12}});
+        check_iter(iter, end, PathVerb::line, {{11, 12}, {17, 18}});
+        REQUIRE(iter == end);
+    }
+}
+
+TEST_CASE("addPath", "[rawpath]")
+{
+    using PathMaker = void (*)(RawPath * sink);
+
+    const PathMaker makers[] = {
+        [](RawPath* sink) {},
+        [](RawPath* sink) {
+            sink->moveTo(1, 2);
+            sink->lineTo(3, 4);
+        },
+        [](RawPath* sink) {
+            sink->moveTo(1, 2);
+            sink->lineTo(3, 4);
+            sink->close();
+        },
+        [](RawPath* sink) {
+            sink->moveTo(1, 2);
+            sink->lineTo(3, 4);
+            sink->quadTo(5, 6, 7, 8);
+            sink->cubicTo(9, 10, 11, 12, 13, 14);
+            sink->close();
+        },
+    };
+    constexpr size_t N = sizeof(makers) / sizeof(makers[0]);
+
+    auto direct = [](PathMaker m0, PathMaker m1, const Mat2D* mx) {
+        RawPath p;
+        m0(&p);
+        m1(&p);
+        if (mx)
+        {
+            p.transformInPlace(*mx);
+        }
+        return p;
+    };
+    auto useadd = [](PathMaker m0, PathMaker m1, const Mat2D* mx) {
+        RawPath p;
+
+        RawPath tmp;
+        m0(&tmp);
+        p.addPath(tmp, mx);
+
+        tmp.reset();
+        m1(&tmp);
+        p.addPath(tmp, mx);
+        return p;
+    };
+
+    for (auto i = 0; i < N; ++i)
+    {
+        for (auto j = 0; j < N; ++j)
+        {
+            RawPath p0, p1;
+
+            p0 = direct(makers[i], makers[j], nullptr);
+            p1 = useadd(makers[i], makers[j], nullptr);
+            REQUIRE(p0 == p1);
+
+            auto mx = Mat2D::fromScale(2, 3);
+            p0 = direct(makers[i], makers[j], &mx);
+            p1 = useadd(makers[i], makers[j], &mx);
+            REQUIRE(p0 == p1);
+        }
+    }
+}
+
+TEST_CASE("bounds", "[rawpath]")
+{
+    RawPath path;
+    AABB bounds;
+    srand(0);
+    const auto randPt = [&] {
+        Vec2D pt = Vec2D(float(rand()), float(rand())) / (float(RAND_MAX) * .5f) - Vec2D(1, 1);
+        bounds.minX = std::min(bounds.minX, pt.x);
+        bounds.minY = std::min(bounds.minY, pt.y);
+        bounds.maxX = std::max(bounds.maxX, pt.x);
+        bounds.maxY = std::max(bounds.maxY, pt.y);
+        return pt;
+    };
+    for (int numVerbs = 1; numVerbs < 1 << 16; numVerbs <<= 1)
+    {
+        path.rewind();
+        bounds.minX = bounds.minY = std::numeric_limits<float>::infinity();
+        bounds.maxX = bounds.maxY = -std::numeric_limits<float>::infinity();
+        for (int i = 0; i < numVerbs; ++i)
+        {
+            switch (rand() % 5)
+            {
+                case 0:
+                    path.move(randPt());
+                    break;
+                case 1:
+                    if (path.empty())
+                    { // Account for the implicit moveTo(0).
+                        bounds = {};
+                    }
+                    path.line(randPt());
+                    break;
+                case 2:
+                    if (path.empty())
+                    { // Account for the implicit moveTo(0).
+                        bounds = {};
+                    }
+                    path.quad(randPt(), randPt());
+                    break;
+                case 3:
+                    if (path.empty())
+                    { // Account for the implicit moveTo(0).
+                        bounds = {};
+                    }
+                    path.cubic(randPt(), randPt(), randPt());
+                    break;
+                case 4:
+                    path.close();
+                    break;
+            }
+        }
+        AABB pathBounds = path.bounds();
+        REQUIRE(pathBounds.minX == bounds.minX);
+        REQUIRE(pathBounds.minY == bounds.minY);
+        REQUIRE(pathBounds.maxX == bounds.maxX);
+        REQUIRE(pathBounds.maxY == bounds.maxY);
+    }
+}
+
+TEST_CASE("prune-empty-segments", "[rawpath]")
+{
+    {
+        RawPath p;
+        p.pruneEmptySegments();
+        CHECK(p.begin() == p.end());
+    }
+
+    {
+        RawPath p;
+        p.lineTo(0, 0);
+        p.pruneEmptySegments();
+        auto iter = p.begin();
+        auto end = p.end();
+        check_iter(iter, end, PathVerb::move, {{0, 0}});
+        CHECK(iter == end);
+    }
+
+    {
+        RawPath p;
+        p.quadTo(0, 0, 0, 0);
+        p.pruneEmptySegments();
+        auto iter = p.begin();
+        auto end = p.end();
+        check_iter(iter, end, PathVerb::move, {{0, 0}});
+        CHECK(iter == end);
+    }
+
+    {
+        RawPath p;
+        p.cubicTo(0, 0, 0, 0, 0, 0);
+        p.pruneEmptySegments();
+        auto iter = p.begin();
+        auto end = p.end();
+        check_iter(iter, end, PathVerb::move, {{0, 0}});
+        CHECK(iter == end);
+    }
+
+    {
+        RawPath p;
+        p.moveTo(1, 2);
+        p.lineTo(3, 4);
+        p.lineTo(3, 4);
+        p.quadTo(5, 6, 7, 8);
+        p.quadTo(7, 8, 7, 8);
+        p.quadTo(7, 8, 7, 9);
+        p.quadTo(7, 9, 7, 9);
+        p.quadTo(7, 9, 7, 8);
+        p.quadTo(7, 8, 7, 8);
+        p.cubicTo(9, 10, 11, 12, 13, 14);
+        p.cubicTo(13, 14, 13, 14, 13, 14);
+        p.cubicTo(13, 14, 13, 14, 13, 15);
+        p.cubicTo(13, 15, 13, 15, 13, 15);
+        p.cubicTo(13, 16, 13, 15, 13, 15);
+        p.cubicTo(13, 15, 13, 15, 13, 15);
+        p.cubicTo(13, 15, 13, 16, 13, 15);
+        p.cubicTo(13, 15, 13, 15, 13, 15);
+        p.cubicTo(13, 15, 13, 15, 13, 16);
+        p.close();
+        p.pruneEmptySegments();
+        auto iter = p.begin();
+        auto end = p.end();
+        check_iter(iter, end, PathVerb::move, {{1, 2}});
+        check_iter(iter, end, PathVerb::line, {{1, 2}, {3, 4}});
+        check_iter(iter, end, PathVerb::quad, {{3, 4}, {5, 6}, {7, 8}});
+        check_iter(iter, end, PathVerb::quad, {{7, 8}, {7, 8}, {7, 9}});
+        check_iter(iter, end, PathVerb::quad, {{7, 9}, {7, 9}, {7, 8}});
+        check_iter(iter, end, PathVerb::cubic, {{7, 8}, {9, 10}, {11, 12}, {13, 14}});
+        check_iter(iter, end, PathVerb::cubic, {{13, 14}, {13, 14}, {13, 14}, {13, 15}});
+        check_iter(iter, end, PathVerb::cubic, {{13, 15}, {13, 16}, {13, 15}, {13, 15}});
+        check_iter(iter, end, PathVerb::cubic, {{13, 15}, {13, 15}, {13, 16}, {13, 15}});
+        check_iter(iter, end, PathVerb::cubic, {{13, 15}, {13, 15}, {13, 15}, {13, 16}});
+        check_iter(iter, end, PathVerb::close, {});
+        CHECK(iter == end);
+    }
+
+    {
+        RawPath p;
+        p.moveTo(1, 2);
+        p.lineTo(1, 2);
+        p.lineTo(3, 4);
+
+        RawPath p2;
+        p2.moveTo(5, 6);
+        p2.quadTo(7, 8, 9, 10);
+        p2.close();
+        p2.moveTo(11, 12);
+        p2.cubicTo(13, 14, 15, 16, 17, 18);
+
+        Mat2D matZero = Mat2D(0, 0, 0, 0, 19, 20);
+        auto p2it = p.addPath(p2, &matZero);
+
+        // Pruning at the end does nothing.
+        p.pruneEmptySegments(p.end());
+        {
+            auto iter = p.begin();
+            auto end = p.end();
+            check_iter(iter, end, PathVerb::move, {{1, 2}});
+            check_iter(iter, end, PathVerb::line, {{1, 2}, {1, 2}});
+            check_iter(iter, end, PathVerb::line, {{1, 2}, {3, 4}});
+            check_iter(iter, end, PathVerb::move, {{19, 20}});
+            check_iter(iter, end, PathVerb::quad, {{19, 20}, {19, 20}, {19, 20}});
+            check_iter(iter, end, PathVerb::close, {});
+            check_iter(iter, end, PathVerb::move, {{19, 20}});
+            check_iter(iter, end, PathVerb::cubic, {{19, 20}, {19, 20}, {19, 20}, {19, 20}});
+            CHECK(iter == end);
+        }
+
+        // Pruning just at the beginning of the added p2 won't remove the pre-existing empty
+        // segment.
+        p.pruneEmptySegments(p2it);
+        {
+            auto iter = p.begin();
+            auto end = p.end();
+            check_iter(iter, end, PathVerb::move, {{1, 2}});
+            check_iter(iter, end, PathVerb::line, {{1, 2}, {1, 2}});
+            check_iter(iter, end, PathVerb::line, {{1, 2}, {3, 4}});
+            check_iter(iter, end, PathVerb::move, {{19, 20}});
+            check_iter(iter, end, PathVerb::close, {});
+            check_iter(iter, end, PathVerb::move, {{19, 20}});
+            CHECK(iter == end);
+        }
+
+        // Now remove the pre-existing one.
+        p.pruneEmptySegments(p.begin());
+        {
+            auto iter = p.begin();
+            auto end = p.end();
+            check_iter(iter, end, PathVerb::move, {{1, 2}});
+            check_iter(iter, end, PathVerb::line, {{1, 2}, {3, 4}});
+            check_iter(iter, end, PathVerb::move, {{19, 20}});
+            check_iter(iter, end, PathVerb::close, {});
+            check_iter(iter, end, PathVerb::move, {{19, 20}});
+            CHECK(iter == end);
+        }
+    }
+}
diff --git a/test/reader_test.cpp b/test/reader_test.cpp
new file mode 100644
index 0000000..72824a6
--- /dev/null
+++ b/test/reader_test.cpp
@@ -0,0 +1,84 @@
+#include <catch.hpp>
+#include <rive/core/reader.h>
+
+TEST_CASE("uint leb decoder", "[reader]")
+{
+    uint64_t result;
+
+    uint8_t encoded1[] = {0x01};
+    decode_uint_leb(encoded1, encoded1 + 1, &result);
+    REQUIRE(result == 1);
+
+    uint8_t encoded15[] = {0x0F};
+    decode_uint_leb(encoded15, encoded15 + 1, &result);
+    REQUIRE(result == 15);
+
+    uint8_t encodedMultiByte[] = {0xE5, 0x8E, 0x26};
+    decode_uint_leb(encodedMultiByte, encodedMultiByte + 3, &result);
+    REQUIRE(result == 624485);
+}
+
+TEST_CASE("string decoder", "[reader]")
+{
+    char* str = strdup("New Artboard");
+    uint8_t str_bytes[] = {0x4E, 0x65, 0x77, 0x20, 0x41, 0x72, 0x74, 0x62, 0x6F, 0x61, 0x72, 0x64};
+    // Length of string + 1 is needed to add the null terminator
+    char* decoded_str = (char*)malloc((13) * sizeof(char));
+    uint64_t bytes_read = decode_string(12, str_bytes, str_bytes + 12, decoded_str);
+    REQUIRE(strcmp(str, decoded_str) == 0);
+    REQUIRE(bytes_read == 12);
+
+    bytes_read = decode_string(12, str_bytes, str_bytes + 11, decoded_str);
+    REQUIRE(bytes_read == 0);
+    delete str;
+    delete decoded_str;
+}
+
+TEST_CASE("float decoder", "[reader]")
+{
+    float decoded_num;
+    uint64_t bytes_read;
+
+    uint8_t num_bytes_100[] = {0x00, 0x00, 0xC8, 0x42};
+    bytes_read = decode_float(num_bytes_100, num_bytes_100 + 4, &decoded_num);
+    REQUIRE(decoded_num == 100.0f);
+    REQUIRE(bytes_read == 4);
+
+    uint8_t num_bytes_pi[] = {0xD0, 0x0F, 0x49, 0x40};
+    bytes_read = decode_float(num_bytes_pi, num_bytes_pi + 4, &decoded_num);
+    REQUIRE(decoded_num == 3.14159f);
+    REQUIRE(bytes_read == 4);
+
+    uint8_t num_bytes_neg_euler[] = {0x51, 0xF8, 0x2D, 0xC0};
+    bytes_read = decode_float(num_bytes_neg_euler, num_bytes_neg_euler + 4, &decoded_num);
+    REQUIRE(decoded_num == -2.718281f);
+    REQUIRE(bytes_read == 4);
+
+    bytes_read = decode_float(num_bytes_neg_euler, num_bytes_neg_euler + 3, &decoded_num);
+    REQUIRE(bytes_read == 0);
+}
+
+TEST_CASE("byte decoder", "[reader")
+{
+    uint8_t decoded_byte;
+    // luigi: commented this out as it was giving a warning (I added -Wall to
+    // the compiler flags). Warning was:
+    //
+    // ../../rive/test/reader_test.cpp:108:11: warning: unused variable
+    // 'bytes_read' [-Wunused-variable]
+    //
+    // uint64_t bytes_read;
+    uint8_t pos = 0;
+    uint8_t bytes[] = {0x00, 0x00, 0xC8, 0x42};
+
+    pos += decode_uint_8(bytes + pos, bytes + 4, &decoded_byte);
+    REQUIRE(decoded_byte == bytes[pos - 1]);
+    pos += decode_uint_8(bytes + pos, bytes + 4, &decoded_byte);
+    REQUIRE(decoded_byte == bytes[pos - 1]);
+    pos += decode_uint_8(bytes + pos, bytes + 4, &decoded_byte);
+    REQUIRE(decoded_byte == bytes[pos - 1]);
+    pos += decode_uint_8(bytes + pos, bytes + 4, &decoded_byte);
+    REQUIRE(decoded_byte == bytes[pos - 1]);
+    // overflow
+    REQUIRE(decode_uint_8(bytes + pos, bytes + 4, &decoded_byte) == 0);
+}
diff --git a/test/refcnt_test.cpp b/test/refcnt_test.cpp
new file mode 100644
index 0000000..2334e45
--- /dev/null
+++ b/test/refcnt_test.cpp
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include <rive/refcnt.hpp>
+#include <catch.hpp>
+#include <cstdio>
+
+using namespace rive;
+
+class MyRefCnt : public RefCnt<MyRefCnt>
+{
+public:
+    MyRefCnt() {}
+    MyRefCnt(int, float, bool) {}
+
+    void require_count(int value) { REQUIRE(this->debugging_refcnt() == value); }
+};
+
+TEST_CASE("refcnt", "[basics]")
+{
+    MyRefCnt my;
+    REQUIRE(my.debugging_refcnt() == 1);
+    my.ref();
+    REQUIRE(my.debugging_refcnt() == 2);
+    my.unref();
+    REQUIRE(my.debugging_refcnt() == 1);
+
+    safe_ref(&my);
+    REQUIRE(my.debugging_refcnt() == 2);
+    safe_unref(&my);
+    REQUIRE(my.debugging_refcnt() == 1);
+
+    // just exercise these to be sure they don't crash
+    safe_ref((MyRefCnt*)nullptr);
+    safe_unref((MyRefCnt*)nullptr);
+}
+
+TEST_CASE("rcp", "[basics]")
+{
+    rcp<MyRefCnt> r0(nullptr);
+
+    REQUIRE(r0.get() == nullptr);
+    REQUIRE(!r0);
+
+    auto r1 = make_rcp<MyRefCnt>();
+    REQUIRE(r1.get() != nullptr);
+    REQUIRE(r1);
+    REQUIRE(r1 != r0);
+    REQUIRE(r1->debugging_refcnt() == 1);
+
+    auto r2 = r1;
+    REQUIRE(r1.get() == r2.get());
+    REQUIRE(r1 == r2);
+    REQUIRE(r2->debugging_refcnt() == 2);
+
+    auto r3 = make_rcp<MyRefCnt>(1, .5f, false);
+    REQUIRE(r3.get() != nullptr);
+    REQUIRE(r3);
+    REQUIRE(r3 != r1);
+    REQUIRE(r3->debugging_refcnt() == 1);
+
+    auto ptr = r2.release();
+    REQUIRE(r2.get() == nullptr);
+    REQUIRE(r1.get() == ptr);
+
+    // This is important, calling release() does not modify the ref count on the object
+    // We have to manage that explicit since we called release()
+    REQUIRE(r1->debugging_refcnt() == 2);
+    ptr->unref();
+    REQUIRE(r1->debugging_refcnt() == 1);
+
+    r1.reset();
+    REQUIRE(r1.get() == nullptr);
+
+    struct A : public rive::RefCnt<A>
+    {
+        int x = 17;
+    };
+    struct B : public A
+    {
+        int y = 21;
+    };
+    auto b = make_rcp<B>();
+    CHECK(b->y == 21);
+    rcp<A> a = b;
+    CHECK(a->x == 17);
+    CHECK(static_rcp_cast<B>(a)->y == 21);
+    CHECK(rcp<A>(b)->x == 17);
+    CHECK(a->debugging_refcnt() == 2);
+    CHECK(b->debugging_refcnt() == 2);
+    CHECK(static_rcp_cast<B>(std::move(a))->y == 21);
+    CHECK(a == nullptr);
+    CHECK(b->debugging_refcnt() == 1);
+    a = ref_rcp(b.get());
+    CHECK(b == a);
+    CHECK(a->debugging_refcnt() == 2);
+    CHECK(a->x == 17);
+    CHECK(static_rcp_cast<B>(a)->y == 21);
+}
diff --git a/test/rive_file_reader.hpp b/test/rive_file_reader.hpp
new file mode 100644
index 0000000..19001f0
--- /dev/null
+++ b/test/rive_file_reader.hpp
@@ -0,0 +1,47 @@
+#ifndef _RIVE_FILE_READER_HPP_
+#define _RIVE_FILE_READER_HPP_
+
+#include <rive/file.hpp>
+
+#include "rive_testing.hpp"
+#include "utils/no_op_factory.hpp"
+
+static rive::NoOpFactory gNoOpFactory;
+
+static inline std::vector<uint8_t> ReadFile(const char path[])
+{
+    FILE* fp = fopen(path, "rb");
+    REQUIRE(fp != nullptr);
+
+    fseek(fp, 0, SEEK_END);
+    const size_t length = ftell(fp);
+    fseek(fp, 0, SEEK_SET);
+    std::vector<uint8_t> bytes(length);
+    REQUIRE(fread(bytes.data(), 1, length, fp) == length);
+    fclose(fp);
+
+    return bytes;
+}
+
+static inline std::unique_ptr<rive::File> ReadRiveFile(const char path[],
+                                                       rive::Factory* factory = nullptr,
+                                                       rive::FileAssetLoader* loader = nullptr,
+                                                       bool loadInBandAssets = true)
+{
+    if (!factory)
+    {
+        factory = &gNoOpFactory;
+    }
+
+    std::vector<uint8_t> bytes = ReadFile(path);
+
+    rive::ImportResult result;
+    auto file = rive::File::import(bytes, factory, &result, loader);
+    REQUIRE(result == rive::ImportResult::success);
+    REQUIRE(file.get() != nullptr);
+    REQUIRE(file->artboard() != nullptr);
+
+    return file;
+}
+
+#endif
diff --git a/test/rive_testing.cpp b/test/rive_testing.cpp
new file mode 100644
index 0000000..aa7691e
--- /dev/null
+++ b/test/rive_testing.cpp
@@ -0,0 +1,14 @@
+#include "rive_testing.hpp"
+
+bool aboutEqual(const rive::Mat2D& a, const rive::Mat2D& b)
+{
+    const float epsilon = 0.0001f;
+    for (int i = 0; i < 6; i++)
+    {
+        if (std::fabs(a[i] - b[i]) > epsilon)
+        {
+            return false;
+        }
+    }
+    return true;
+}
\ No newline at end of file
diff --git a/test/rive_testing.hpp b/test/rive_testing.hpp
new file mode 100644
index 0000000..f790280
--- /dev/null
+++ b/test/rive_testing.hpp
@@ -0,0 +1,23 @@
+#ifndef _CATCH_RIVE_TESTING_
+#define _CATCH_RIVE_TESTING_
+
+#include <catch.hpp>
+#include <sstream>
+#include <rive/math/mat2d.hpp>
+
+bool aboutEqual(const rive::Mat2D& a, const rive::Mat2D& b);
+
+namespace Catch
+{
+template <> struct StringMaker<rive::Mat2D>
+{
+    static std::string convert(rive::Mat2D const& value)
+    {
+        std::ostringstream os;
+        os << value[0] << ", " << value[1] << ", " << value[2] << ", " << value[3] << ", "
+           << value[4] << ", " << value[5];
+        return os.str();
+    }
+};
+} // namespace Catch
+#endif
\ No newline at end of file
diff --git a/test/rotation_constraint_test.cpp b/test/rotation_constraint_test.cpp
new file mode 100644
index 0000000..3acd28b
--- /dev/null
+++ b/test/rotation_constraint_test.cpp
@@ -0,0 +1,28 @@
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/bones/bone.hpp>
+#include <rive/shapes/shape.hpp>
+#include <rive/math/transform_components.hpp>
+#include <utils/no_op_renderer.hpp>
+#include "rive_file_reader.hpp"
+#include "rive_testing.hpp"
+#include <cstdio>
+
+TEST_CASE("rotation constraint updates world transform", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/rotation_constraint.riv");
+
+    auto artboard = file->artboard();
+
+    REQUIRE(artboard->find<rive::TransformComponent>("target") != nullptr);
+    auto target = artboard->find<rive::TransformComponent>("target");
+
+    REQUIRE(artboard->find<rive::TransformComponent>("rect") != nullptr);
+    auto rectangle = artboard->find<rive::TransformComponent>("rect");
+
+    artboard->advance(0.0f);
+    auto targetComponents = target->worldTransform().decompose();
+    auto rectComponents = rectangle->worldTransform().decompose();
+
+    REQUIRE(targetComponents.rotation() == rectComponents.rotation());
+}
diff --git a/test/scale_constraint_test.cpp b/test/scale_constraint_test.cpp
new file mode 100644
index 0000000..f5d2333
--- /dev/null
+++ b/test/scale_constraint_test.cpp
@@ -0,0 +1,30 @@
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/bones/bone.hpp>
+#include <rive/shapes/shape.hpp>
+#include <rive/math/transform_components.hpp>
+#include <utils/no_op_renderer.hpp>
+#include "rive_file_reader.hpp"
+#include "rive_testing.hpp"
+#include <cstdio>
+
+TEST_CASE("scale constraint updates world transform", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/scale_constraint.riv");
+
+    auto artboard = file->artboard();
+
+    REQUIRE(artboard->find<rive::TransformComponent>("target") != nullptr);
+    auto target = artboard->find<rive::TransformComponent>("target");
+
+    REQUIRE(artboard->find<rive::TransformComponent>("rect") != nullptr);
+    auto rectangle = artboard->find<rive::TransformComponent>("rect");
+
+    artboard->advance(0.0f);
+
+    auto targetComponents = target->worldTransform().decompose();
+    auto rectComponents = rectangle->worldTransform().decompose();
+
+    REQUIRE(targetComponents.scaleX() == rectComponents.scaleX());
+    REQUIRE(targetComponents.scaleY() == rectComponents.scaleY());
+}
diff --git a/test/simd_test.cpp b/test/simd_test.cpp
new file mode 100644
index 0000000..2192f12
--- /dev/null
+++ b/test/simd_test.cpp
@@ -0,0 +1,781 @@
+/*
+ * Copyright 2019 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ *
+ * "fast_acos" test imported from skia:tests/SkVxTest.cpp
+ *
+ * Copyright 2022 Rive
+ */
+
+// Ignore performance warnings in this file about having AVX disabled. We test vectors larger than 4
+// and aren't worried about performance.
+//
+// If we try to use large vectors in other parts of the code with AVX disabled, we definitely still
+// want this warning.
+#if defined(__clang__) || defined(__GNUC__)
+#pragma GCC diagnostic ignored "-Wpsabi"
+#endif
+
+#include <catch.hpp>
+
+#include "rive/math/math_types.hpp"
+#include "rive/math/simd.hpp"
+#include <limits>
+
+#define CHECK_ALL(B) CHECK(simd::all(B))
+#define CHECK_ANY(B) CHECK(simd::any(B))
+
+namespace rive
+{
+constexpr float kInf = std::numeric_limits<float>::infinity();
+constexpr float kNaN = std::numeric_limits<float>::quiet_NaN();
+constexpr double kInf_double = std::numeric_limits<double>::infinity();
+constexpr double kNaN_double = std::numeric_limits<double>::quiet_NaN();
+
+// Check simd::any.
+TEST_CASE("any", "[simd]")
+{
+    CHECK(!simd::any(int4{0, 0, 0, 0}));
+    CHECK_ANY((int4{-1, 0, 0, 0}));
+    CHECK_ANY((int4{0, -1, 0, 0}));
+    CHECK_ANY((int4{0, 0, -1, 0}));
+    CHECK_ANY((int4{0, 0, 0, -1}));
+    CHECK(!simd::any(ivec<3>{0, 0, 0}));
+    CHECK_ANY((ivec<3>{-1, 0, 0}));
+    CHECK_ANY((ivec<3>{0, -1, 0}));
+    CHECK_ANY((ivec<3>{0, 0, -1}));
+    CHECK(!simd::any(int2{0, 0}));
+    CHECK_ANY((int2{-1, 0}));
+    CHECK_ANY((int2{0, -1}));
+    CHECK(!simd::any(ivec<1>{0}));
+    CHECK_ANY((ivec<1>{-1}));
+}
+
+// Check simd::all.
+TEST_CASE("all", "[simd]")
+{
+    CHECK_ALL((int4{-1, -1, -1, -1}));
+    CHECK(!simd::all(int4{0, -1, -1, -1}));
+    CHECK(!simd::all(int4{-1, 0, -1, -1}));
+    CHECK(!simd::all(int4{-1, -1, 0, -1}));
+    CHECK(!simd::all(int4{-1, -1, -1, 0}));
+    CHECK_ALL((ivec<3>{-1, -1, -1}));
+    CHECK(!simd::all(ivec<3>{0, -1, -1}));
+    CHECK(!simd::all(ivec<3>{-1, 0, -1}));
+    CHECK(!simd::all(ivec<3>{-1, -1, 0}));
+    CHECK_ALL((int2{-1, -1}));
+    CHECK(!simd::all(int2{0, -1}));
+    CHECK(!simd::all(int2{-1, 0}));
+    CHECK_ALL((ivec<1>{-1}));
+    CHECK(!simd::all(ivec<1>{0}));
+}
+
+TEST_CASE("operators", "[simd]")
+{
+    float4 a{1, 2, 3, 4};
+    float4 b{5, 6, 7, 8};
+    CHECK_ALL((a + b == float4{6, 8, 10, 12}));
+    CHECK_ALL((a - b == float4(-4)));
+    CHECK_ALL((a * b == float4{5, 12, 21, 32}));
+    CHECK_ALL((a / a == b / b));
+    CHECK_ALL((a + 10.f == 10.f + a));
+    CHECK_ALL((+(a - 10.f) == -(10.f - a)));
+    CHECK_ALL((a * 2.f == 2.f * a));
+    CHECK_ALL((a / .5f == 2.f * a));
+
+    int4 i{1, 2, 4, 8};
+    int4 j{0, 1, 3, 7};
+    CHECK(!simd::any((i & j)));
+    CHECK_ALL(((i | j) == int4{1, 3, 7, 15}));
+    CHECK_ALL(((i | j) == (i ^ j)));
+    CHECK_ALL((~i + 1 == -i)); // Assume two's compliment for now...
+}
+
+TEST_CASE("swizzles", "[simd]")
+{
+    float2 v2{1, -2};
+    CHECK(v2.x == 1);
+    CHECK(v2.y == -2);
+    CHECK(v2.yx[0] == v2[1]);
+    CHECK(v2.yx[1] == v2[0]);
+    CHECK_ALL((float2(v2.yx) == float2{-2, 1}));
+    CHECK_ALL((v2.yx == float2{-2, 1}));
+    CHECK_ALL((v2.xyxy == float4{1, -2, 1, -2}));
+    CHECK_ALL((v2.yxyx == float4{-2, 1, -2, 1}));
+
+    float4 v4{1, -2, 3, -1};
+    CHECK(v4.x == 1);
+    CHECK(v4.y == -2);
+    CHECK(v4.z == 3);
+    CHECK(v4.w == -1);
+    CHECK_ALL((v4.xy == float2{1, -2}));
+    CHECK_ALL((v4.yz == float2{-2, 3}));
+    CHECK_ALL((v4.zw == float2{3, -1}));
+    CHECK_ALL((v4.xyz == vec<3>{1, -2, 3}));
+    CHECK_ALL((v4.yzw == vec<3>{-2, 3, -1}));
+    CHECK_ALL((v4.yxwz == float4{-2, 1, -1, 3}));
+    CHECK_ALL((v4.zwxy == float4{3, -1, 1, -2}));
+    CHECK_ALL((v4.zyxw == float4{3, -2, 1, -1}));
+    CHECK_ALL((v4.xwzy == float4{1, -1, 3, -2}));
+
+    v4.xy = v2.yx;
+    CHECK_ALL((v4 == float4{-2, 1, 3, -1}));
+    v4.zw = v4.yz;
+    CHECK_ALL((v4 == float4{-2, 1, 1, 3}));
+    v4.yz = -7.f;
+    CHECK_ALL((v4 == float4{-2, -7, -7, 3}));
+    v4.xyz = 0.f;
+    CHECK_ALL((v4 == float4{0, 0, 0, 3}));
+    v4.yzw = -9.f;
+    CHECK_ALL((v4 == float4{0, -9, -9, -9}));
+    v4.x = 1;
+    v4.y = 2;
+    v4.z = -8;
+    v4.w = .5f;
+    CHECK_ALL((v4 == float4{1, 2, -8, .5f}));
+
+    v2.y = -9;
+    v2.x = 88;
+    CHECK_ALL((v2.yx == float2{-9, 88}));
+
+    ivec<3> v3;
+    v3.x = 0;
+    v3.y = 9;
+    v3.z = -1;
+    CHECK_ALL((v3 == ivec<3>{0, 9, -1}));
+    CHECK(v3.x == 0);
+    CHECK(v3.y == 9);
+    CHECK(v3.z == -1);
+
+    uvec<1> v1;
+    v1.x = 7;
+    CHECK(v1[0] == 7);
+    CHECK_ALL((v1 == uvec<1>{7}));
+
+    float4 a = {0, 1, 12, 99.9f};
+    float4 a_ = a.yxwz;
+    float4 b = {.1f, -1, -9, -20};
+    float4 b_ = b.yxwz;
+    CHECK_ALL((simd::abs(b.yxwz) == simd::abs(b_)));
+    CHECK_ALL((simd::floor(a.yxwz) == simd::floor(a_)));
+    CHECK_ALL((simd::ceil(a.yxwz) == simd::ceil(a_)));
+    CHECK_ALL((simd::sqrt(a.yxwz) == simd::sqrt(a_)));
+    CHECK_ALL((simd::fast_acos(a.yxwz) == simd::fast_acos(a_)));
+    CHECK_ALL((simd::min(a.yxwz, b.yxwz) == simd::min(a_, b_)));
+    CHECK_ALL((simd::max(a.yxwz, b.yxwz) == simd::max(a_, b_)));
+    CHECK_ALL(
+        (simd::clamp(a.yxwz, float4(2), float4(10)) == simd::clamp(a_, float4(2), float4(10))));
+    CHECK_ALL((simd::mix(a.yxwz, b.yxwz, float4(.5f)) == simd::mix(a_, b_, float4(.5f))));
+    CHECK_ALL(
+        (simd::precise_mix(a.yxwz, b.yxwz, float4(.5f)) == simd::precise_mix(a_, b_, float4(.5f))));
+    CHECK_ALL(
+        (simd::if_then_else(int4{~0}, a.yxwz, b.yxwz) == simd::if_then_else(int4{~0}, a_, b_)));
+    CHECK_ALL((simd::if_then_else(int4{~0}, a.yxwz, b.yxwz) == float4{a.y, b.x, b.w, b.z}));
+    CHECK_ALL((simd::if_then_else(~int4{~0}, a_, b_) == float4{b.y, a.x, a.w, a.z}));
+    CHECK_ALL(
+        (simd::if_then_else(int4{~0}, a.yxwz, b.yxwz) != simd::if_then_else(~int4{~0}, a_, b_)));
+
+    float mem[4]{};
+    simd::store(mem, a.yxwz);
+    CHECK(!memcmp(mem, &a_, 4 * 4));
+
+    // Unfortunate, but there's no way to block the default assignment operator and still be a POD
+    // type.
+    a.zwxy = b.zwxy;
+    CHECK_ALL((a == b));
+}
+
+// Verify the simd float types are IEEE 754 compliant for infinity and NaN.
+template <typename T> void check_ieee_compliance()
+{
+    using vec4 = simd::gvec<T, 4>;
+    using vec2 = simd::gvec<T, 2>;
+    constexpr T kTInf = std::numeric_limits<T>::infinity();
+
+    vec4 test = vec4{1, -kTInf, 1, 4} / vec4{0, 2, kTInf, 4};
+    CHECK_ALL((test == vec4{kTInf, -kTInf, 0, 1}));
+
+    // Inf * Inf == Inf
+    test = vec4{kTInf, -kTInf, kTInf, -kTInf} * vec4{kTInf, kTInf, -kTInf, -kTInf};
+    CHECK_ALL((test == vec4{kTInf, -kTInf, -kTInf, kTInf}));
+
+    // Inf/0 == Inf, 0/Inf == 0
+    test = vec4{kTInf, -kTInf, 0, 0} / vec4{0, 0, kTInf, -kTInf};
+    CHECK_ALL((test == vec4{kTInf, -kTInf, 0, 0}));
+
+    // Inf/Inf, 0/0, 0 * Inf, Inf - Inf == NaN
+    test = {kTInf, 0, 0, kTInf};
+    test.xy /= vec2{kTInf, 0};
+    test.z *= kTInf;
+    test.w -= kTInf;
+    for (int i = 0; i < 4; ++i)
+    {
+        CHECK(std::isnan(test[i]));
+    }
+    // NaN always fails comparisons.
+    CHECK(!simd::any(test == test));
+    CHECK_ALL((test != test));
+    CHECK(!simd::any(test <= test));
+    CHECK(!simd::any(test >= test));
+    CHECK(!simd::any(test < test));
+    CHECK(!simd::any(test > test));
+
+    // Inf + Inf == Inf, Inf + -Inf == NaN
+    test = vec4{kTInf, -kTInf, kTInf, -kTInf} + vec4{kTInf, -kTInf, -kTInf, kTInf};
+    CHECK_ALL((test.xy == vec2{kTInf, -kTInf}));
+    CHECK(!simd::any(test.zw == test.zw)); // NaN
+}
+
+TEST_CASE("ieee-compliance", "[simd]")
+{
+    check_ieee_compliance<float>();
+    check_ieee_compliance<double>();
+}
+
+// Check simd::if_then_else.
+template <typename T> void check_if_then_else()
+{
+    using vec4 = simd::gvec<T, 4>;
+    using vec2 = simd::gvec<T, 2>;
+
+    // Vector condition.
+    vec4 f4 = simd::if_then_else(vec4{1, 2, 3, 4} < vec4{4, 3, 2, 1}, vec4(1), vec4(2));
+    CHECK_ALL((f4 == vec4{1, 1, 2, 2}));
+
+    // In vector, -1 is true, 0 is false.
+    vec2 u2 = simd::if_then_else(simd::gvec<typename simd::boolean_mask_type<T>::type, 2>{0, -1},
+                                 vec2{1, 2},
+                                 vec2{3, 4});
+    CHECK_ALL((u2 == vec2{3, 2}));
+
+    // Scalar condition.
+    f4 = u2.x == u2.y ? vec4{1, 2, 3, 4} : vec4{5, 6, 7, 8};
+    CHECK_ALL((f4 == vec4{5, 6, 7, 8}));
+}
+
+TEST_CASE("ternary-operator", "[simd]")
+{
+    check_if_then_else<int8_t>();
+    check_if_then_else<uint8_t>();
+    check_if_then_else<int16_t>();
+    check_if_then_else<uint16_t>();
+    check_if_then_else<float>();
+    check_if_then_else<int32_t>();
+    check_if_then_else<uint32_t>();
+    check_if_then_else<size_t>();
+    check_if_then_else<double>();
+    check_if_then_else<int64_t>();
+    check_if_then_else<uint64_t>();
+}
+
+// Check simd::min/max compliance.
+TEST_CASE("min-max", "[simd]")
+{
+    float4 f4 = simd::min(float4{1, 2, 3, 4}, float4{4, 3, 2});
+    CHECK_ALL((f4 == float4{1, 2, 2, 0}));
+    f4 = simd::max(float4{1, 2, 3, 4}, float4{4, 3, 2});
+    CHECK_ALL((f4 == float4{4, 3, 3, 4}));
+
+    int2 i2 = simd::max(int2(-1), int2{-2});
+    CHECK_ALL((i2 == int2{-1, 0}));
+    i2 = simd::min(int2(-1), int2{-2});
+    CHECK_ALL((i2 == int2{-2, -1}));
+
+    // Infinity works as expected.
+    f4 = simd::min(float4{100, -kInf, -kInf, kInf}, float4{kInf, 100, kInf, -kInf});
+    CHECK_ALL((f4 == float4{100, -kInf, -kInf, -kInf}));
+    f4 = simd::max(float4{100, -kInf, -kInf, kInf}, float4{kInf, 100, kInf, -kInf});
+    CHECK_ALL((f4 == float4{kInf, 100, kInf, kInf}));
+
+    // If a or b is NaN, min returns whichever is not NaN.
+    f4 = simd::min(float4{1, kNaN, 2, kNaN}, float4{kNaN, 1, 1, kNaN});
+    CHECK_ALL((f4.xyz == 1.f));
+    CHECK(std::isnan(f4.w));
+    f4 = simd::max(float4{1, kNaN, 2, kNaN}, float4{kNaN, 1, 1, kNaN});
+    CHECK_ALL((f4.xyz == vec<3>{1, 1, 2}));
+    CHECK(std::isnan(f4.w));
+
+    // fminf/fmaxf behaves the same as simd::min/max.
+    // simd::min/max differs from std::min/max when the first argument is NaN.
+    for (float f : {-2.f, -kInf, kInf})
+    {
+        CHECK(simd::min<float, 1>(kNaN, f).x == f);
+        CHECK(fminf(kNaN, f) == f);
+        CHECK(std::isnan(std::min<float>(kNaN, f)));
+        CHECK(simd::max<float, 1>(kNaN, f).x == f);
+        CHECK(fmaxf(kNaN, f) == f);
+        CHECK(std::isnan(std::max<float>(kNaN, f)));
+    }
+    for (double d : {-1.0, -kInf_double, kInf_double})
+    {
+        CHECK(simd::min<double, 1>(kNaN_double, d).x == d);
+        CHECK(fmin(kNaN_double, d) == d);
+        CHECK(std::isnan(std::min<double>(kNaN_double, d)));
+        CHECK(simd::max<double, 1>(kNaN_double, d).x == d);
+        CHECK(fmax(kNaN_double, d) == d);
+        CHECK(std::isnan(std::max<double>(kNaN_double, d)));
+    }
+
+    // fminf/fmaxf/std::min/std::max/simd::min/stmd::max all behave the same when the second
+    // argument is NaN.
+    for (float f : {1.f, -kInf, kInf})
+    {
+        CHECK(simd::min<float, 1>(f, kNaN).x == f);
+        CHECK(fminf(f, kNaN) == f);
+        CHECK(std::min<float>(f, kNaN) == f);
+        CHECK(simd::max<float, 1>(f, kNaN).x == f);
+        CHECK(fmaxf(f, kNaN) == f);
+        CHECK(std::max<float>(f, kNaN) == f);
+    }
+    for (double d : {2.0, -kInf_double, kInf_double})
+    {
+        CHECK(simd::min<double, 1>(d, kNaN_double).x == d);
+        CHECK(fmin(d, kNaN_double) == d);
+        CHECK(std::min<double>(d, kNaN_double) == d);
+        CHECK(simd::max<double, 1>(d, kNaN_double).x == d);
+        CHECK(fmax(d, kNaN_double) == d);
+        CHECK(std::max<double>(d, kNaN_double) == d);
+    }
+
+    // check non-32-bit types.
+    CHECK_ALL((simd::max(simd::gvec<double, 2>{3, 4}, simd::gvec<double, 2>{4, 3}) ==
+               simd::gvec<double, 2>{4, 4}));
+    CHECK_ALL((simd::min(simd::gvec<uint64_t, 2>{3, 4}, simd::gvec<uint64_t, 2>{4, 3}) ==
+               simd::gvec<uint64_t, 2>{3, 3}));
+    CHECK_ALL((simd::max(simd::gvec<size_t, 2>{3, 4}, simd::gvec<size_t, 2>{4, 3}) ==
+               simd::gvec<size_t, 2>{4, 4}));
+    CHECK_ALL(
+        (simd::max(simd::gvec<uint8_t, 16>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
+                   simd::gvec<uint8_t, 16>{15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}) ==
+         simd::gvec<uint8_t, 16>{15, 14, 13, 12, 11, 10, 9, 8, 8, 9, 10, 11, 12, 13, 14, 15}));
+}
+
+// Check simd::clamp.
+TEST_CASE("clamp", "[simd]")
+{
+    CHECK_ALL(
+        (simd::clamp(float4{1, 2, kInf, -kInf}, float4{2, 1, kInf, 0}, float4{3, 1, kInf, kInf}) ==
+         float4{2, 1, kInf, 0}));
+    CHECK_ALL((simd::clamp(float4{1, kNaN, kInf, -kInf},
+                           float4{kNaN, 2, kNaN, 0},
+                           float4{kNaN, 3, kInf, kNaN}) == float4{1, 2, kInf, 0}));
+    float4 f4 = simd::clamp(float4{1, kNaN, kNaN, kNaN},
+                            float4{kNaN, 1, kNaN, kNaN},
+                            float4{kNaN, kNaN, 1, kNaN});
+    CHECK_ALL((1.f == f4.xyz));
+    CHECK(std::isnan(f4.w));
+
+    // Returns lo if x == NaN, but std::clamp() returns NaN.
+    CHECK(simd::clamp<float, 1>(kNaN, 1, 2).x == 1);
+    // Matches math::clamp().
+    CHECK(simd::clamp<float, 1>(kNaN, 1, 2).x == math::clamp(kNaN, 1, 2));
+
+    // Returns hi if hi <= lo.
+    CHECK(simd::clamp<float, 1>(3, 2, 1).x == 1);
+    CHECK(simd::clamp<float, 1>(kNaN, 2, 1).x == 1);
+    CHECK(simd::clamp<float, 1>(kNaN, kNaN, 1).x == 1);
+    // Matches math::clamp().
+    CHECK(simd::clamp<float, 1>(3, 2, 1).x == math::clamp(3, 2, 1));
+    CHECK(simd::clamp<float, 1>(kNaN, 2, 1).x == math::clamp(kNaN, 2, 1));
+    CHECK(simd::clamp<float, 1>(kNaN, kNaN, 1).x == math::clamp(kNaN, kNaN, 1));
+
+    // Ignores hi and/or lo if they are NaN.
+    CHECK(simd::clamp<float, 1>(3, 4, kNaN).x == 4);
+    CHECK(simd::clamp<float, 1>(3, 2, kNaN).x == 3);
+    CHECK(simd::clamp<float, 1>(3, kNaN, 2).x == 2);
+    CHECK(simd::clamp<float, 1>(3, kNaN, 4).x == 3);
+    CHECK(simd::clamp<float, 1>(3, kNaN, kNaN).x == 3);
+    // Matches math::clamp().
+    CHECK(simd::clamp<float, 1>(3, 4, kNaN).x == math::clamp(3, 4, kNaN));
+    CHECK(simd::clamp<float, 1>(3, 2, kNaN).x == math::clamp(3, 2, kNaN));
+    CHECK(simd::clamp<float, 1>(3, kNaN, 2).x == math::clamp(3, kNaN, 2));
+    CHECK(simd::clamp<float, 1>(3, kNaN, 4).x == math::clamp(3, kNaN, 4));
+    CHECK(simd::clamp<float, 1>(3, kNaN, kNaN).x == math::clamp(3, kNaN, kNaN));
+}
+
+// Check simd::abs.
+TEST_CASE("abs", "[simd]")
+{
+    CHECK_ALL((simd::abs(float4{-1, 2, -3, 4}) == float4{1, 2, 3, 4}));
+    CHECK_ALL((simd::abs(float2{-5, 6}) == float2{5, 6}));
+    CHECK_ALL((simd::abs(float2{-0, 0}) == float2{0, 0}));
+    CHECK_ALL((float4{-std::numeric_limits<float>::epsilon(),
+                      -std::numeric_limits<float>::denorm_min(),
+                      -std::numeric_limits<float>::max(),
+                      -kInf} == float4{-std::numeric_limits<float>::epsilon(),
+                                       -std::numeric_limits<float>::denorm_min(),
+                                       -std::numeric_limits<float>::max(),
+                                       -kInf}
+
+               ));
+    float2 nan2 = simd::abs(float2{kNaN, -kNaN});
+    CHECK_ALL((simd::isnan(nan2)));
+    CHECK_ALL((simd::abs(int4{7, -8, 9, -10}) == int4{7, 8, 9, 10}));
+    CHECK_ALL((simd::abs(int2{0, -0}) == int2{0, 0}));
+    // abs(INT_MIN) returns INT_MIN.
+    CHECK(
+        simd::all(simd::abs(int2{-std::numeric_limits<int32_t>::max(),
+                                 std::numeric_limits<int32_t>::min()}) ==
+                  int2{std::numeric_limits<int32_t>::max(), std::numeric_limits<int32_t>::min()}));
+}
+
+// Check simd::reduce* methods.
+TEST_CASE("reduce", "[simd]")
+{
+    {
+        float4 v = {1, 2, 3, 4};
+        CHECK(simd::reduce_add(v) == 10);
+        CHECK(simd::reduce_add(v.zwxy) == 10);
+        CHECK(simd::reduce_add(v.xyz) == 6);
+        CHECK(simd::reduce_add(v.yz) == 5);
+        CHECK(simd::reduce_add(v.xy.yxyx) == 6);
+        CHECK(simd::reduce_min(v) == 1);
+        CHECK(simd::reduce_min(v.zwxy) == 1);
+        CHECK(simd::reduce_min(v.xyz) == 1);
+        CHECK(simd::reduce_min(v.yz) == 2);
+        CHECK(simd::reduce_min(v.xy.yxyx) == 1);
+        CHECK(simd::reduce_max(v) == 4);
+        CHECK(simd::reduce_max(v.zwxy) == 4);
+        CHECK(simd::reduce_max(v.xyz) == 3);
+        CHECK(simd::reduce_max(v.yz) == 3);
+        CHECK(simd::reduce_max(v.xy.yxyx) == 2);
+    }
+
+    {
+        int4 v = {1, 2, 3, 4};
+        CHECK(simd::reduce_add(v) == 10);
+        CHECK(simd::reduce_add(v.zwxy) == 10);
+        CHECK(simd::reduce_add(v.xyz) == 6);
+        CHECK(simd::reduce_add(v.yz) == 5);
+        CHECK(simd::reduce_add(v.xy.yxyx) == 6);
+        CHECK(simd::reduce_min(v) == 1);
+        CHECK(simd::reduce_min(v.zwxy) == 1);
+        CHECK(simd::reduce_min(v.xyz) == 1);
+        CHECK(simd::reduce_min(v.yz) == 2);
+        CHECK(simd::reduce_min(v.xy.yxyx) == 1);
+        CHECK(simd::reduce_max(v) == 4);
+        CHECK(simd::reduce_max(v.zwxy) == 4);
+        CHECK(simd::reduce_max(v.xyz) == 3);
+        CHECK(simd::reduce_max(v.yz) == 3);
+        CHECK(simd::reduce_max(v.xy.yxyx) == 2);
+        CHECK(simd::reduce_and(v) == 0);
+        CHECK(simd::reduce_and(v.zwxy) == 0);
+        CHECK(simd::reduce_and(v.xyz) == 0);
+        CHECK(simd::reduce_and(v.yz) == 2);
+        CHECK(simd::reduce_and(v.xy.yxyx) == 0);
+        CHECK(simd::reduce_or(v) == 7);
+        CHECK(simd::reduce_or(v.zwxy) == 7);
+        CHECK(simd::reduce_or(v.xyz) == 3);
+        CHECK(simd::reduce_or(v.yz) == 3);
+        CHECK(simd::reduce_or(v.xy.yxyx) == 3);
+    }
+
+    {
+        uint4 v = {1, 2, 3, 4};
+        CHECK(simd::reduce_add(v) == 10);
+        CHECK(simd::reduce_add(v.zwxy) == 10);
+        CHECK(simd::reduce_add(v.xyz) == 6);
+        CHECK(simd::reduce_add(v.yz) == 5);
+        CHECK(simd::reduce_add(v.xy.yxyx) == 6);
+        CHECK(simd::reduce_min(v) == 1);
+        CHECK(simd::reduce_min(v.zwxy) == 1);
+        CHECK(simd::reduce_min(v.xyz) == 1);
+        CHECK(simd::reduce_min(v.yz) == 2);
+        CHECK(simd::reduce_min(v.xy.yxyx) == 1);
+        CHECK(simd::reduce_max(v) == 4);
+        CHECK(simd::reduce_max(v.zwxy) == 4);
+        CHECK(simd::reduce_max(v.xyz) == 3);
+        CHECK(simd::reduce_max(v.yz) == 3);
+        CHECK(simd::reduce_max(v.xy.yxyx) == 2);
+        CHECK(simd::reduce_and(v) == 0);
+        CHECK(simd::reduce_and(v.zwxy) == 0);
+        CHECK(simd::reduce_and(v.xyz) == 0);
+        CHECK(simd::reduce_and(v.yz) == 2);
+        CHECK(simd::reduce_and(v.xy.yxyx) == 0);
+        CHECK(simd::reduce_or(v) == 7);
+        CHECK(simd::reduce_or(v.zwxy) == 7);
+        CHECK(simd::reduce_or(v.xyz) == 3);
+        CHECK(simd::reduce_or(v.yz) == 3);
+        CHECK(simd::reduce_or(v.xy.yxyx) == 3);
+    }
+}
+
+// Check simd::floor.
+TEST_CASE("floor", "[simd]")
+{
+    CHECK_ALL((simd::floor(float4{-1.9f, 1.9f, 2, -2}) == float4{-2, 1, 2, -2}));
+    CHECK_ALL((simd::floor(float2{kInf, -kInf}) == float2{kInf, -kInf}));
+    CHECK_ALL((simd::isnan(simd::floor(float2{kNaN, -kNaN}))));
+}
+
+// Check simd::ceil.
+TEST_CASE("ceil", "[simd]")
+{
+    CHECK_ALL((simd::ceil(float4{-1.9f, 1.9f, 2, -2}) == float4{-1, 2, 2, -2}));
+    CHECK_ALL((simd::ceil(float2{kInf, -kInf}) == float2{kInf, -kInf}));
+    CHECK_ALL((simd::isnan(simd::ceil(float2{kNaN, -kNaN}))));
+}
+
+// Check simd::sqrt.
+TEST_CASE("sqrt", "[simd]")
+{
+    CHECK_ALL((simd::sqrt(float4{1, 4, 9, 16}) == float4{1, 2, 3, 4}));
+    CHECK_ALL((simd::sqrt(float2{25, 36}) == float2{5, 6}));
+    CHECK_ALL((simd::sqrt(vec<1>{36}) == vec<1>{6}));
+    CHECK_ALL((simd::sqrt(vec<5>{49, 64, 81, 100, 121}) == vec<5>{7, 8, 9, 10, 11}));
+    CHECK_ALL((simd::isnan(simd::sqrt(float4{-1, -kInf, kNaN, -2}))));
+    CHECK_ALL((simd::sqrt(vec<3>{kInf, 0, 1}) == vec<3>{kInf, 0, 1}));
+}
+
+static bool check_fast_acos(float x, float fast_acos_x)
+{
+    float acosf_x = acosf(x);
+    float error = acosf_x - fast_acos_x;
+    if (!(fabsf(error) <= SIMD_FAST_ACOS_MAX_ERROR))
+    {
+        auto rad2deg = [](float rad) { return rad * 180 / math::PI; };
+        fprintf(stderr,
+                "Larger-than-expected error from skvx::fast_acos\n"
+                "  x=              %f\n"
+                "  fast_acos_x=    %f  (%f degrees\n"
+                "  acosf_x=        %f  (%f degrees\n"
+                "  error=          %f  (%f degrees)\n"
+                "  tolerance=      %f  (%f degrees)\n\n",
+                x,
+                fast_acos_x,
+                rad2deg(fast_acos_x),
+                acosf_x,
+                rad2deg(acosf_x),
+                error,
+                rad2deg(error),
+                SIMD_FAST_ACOS_MAX_ERROR,
+                rad2deg(SIMD_FAST_ACOS_MAX_ERROR));
+        CHECK(false);
+        return false;
+    }
+    return true;
+}
+
+TEST_CASE("fast_acos", "[simd]")
+{
+    float4 boundaries = simd::fast_acos(float4{-1, 0, 1, 0});
+    check_fast_acos(-1, boundaries[0]);
+    check_fast_acos(0, boundaries[1]);
+    check_fast_acos(+1, boundaries[2]);
+
+    // Select a distribution of starting points around which to begin testing fast_acos. These
+    // fall roughly around the known minimum and maximum errors. No need to include -1, 0, or 1
+    // since those were just tested above. (Those are tricky because 0 is an inflection and the
+    // derivative is infinite at 1 and -1.)
+    using float8 = vec<8>;
+    float8 x = {-.99f, -.8f, -.4f, -.2f, .2f, .4f, .8f, .99f};
+
+    // Converge at the various local minima and maxima of "fast_acos(x) - cosf(x)" and verify that
+    // fast_acos is always within "kTolerance" degrees of the expected answer.
+    float8 err_;
+    for (int iter = 0; iter < 10; ++iter)
+    {
+        // Run our approximate inverse cosine approximation.
+        auto fast_acos_x = simd::fast_acos(x);
+
+        // Find d/dx(error)
+        //    = d/dx(fast_acos(x) - acos(x))
+        //    = (f'g - fg')/gg + 1/sqrt(1 - x^2), [where f = bx^3 + ax, g = dx^4 + cx^2 + 1]
+        float8 xx = x * x;
+        float8 a = -0.939115566365855f;
+        float8 b = 0.9217841528914573f;
+        float8 c = -1.2845906244690837f;
+        float8 d = 0.295624144969963174f;
+        float8 f = (b * xx + a) * x;
+        float8 f_ = 3.f * b * xx + a;
+        float8 g = (d * xx + c) * xx + 1.f;
+        float8 g_ = (4.f * d * xx + 2.f * c) * x;
+        float8 gg = g * g;
+        float8 q = simd::sqrt(1.f - xx);
+        err_ = (f_ * g - f * g_) / gg + 1.f / q;
+
+        // Find d^2/dx^2(error)
+        //    = ((f''g - fg'')g^2 - (f'g - fg')2gg') / g^4 + x(1 - x^2)^(-3/2)
+        //    = ((f''g - fg'')g - (f'g - fg')2g') / g^3 + x(1 - x^2)^(-3/2)
+        float8 f__ = 6.f * b * x;
+        float8 g__ = 12.f * d * xx + 2.f * c;
+        float8 err__ = ((f__ * g - f * g__) * g - (f_ * g - f * g_) * 2.f * g_) / (gg * g) +
+                       x / ((1.f - xx) * q);
+
+#if 0
+        SkDebugf("\n\niter %i\n", iter);
+#endif
+        // Ensure each lane's approximation is within maximum error.
+        for (int j = 0; j < 8; ++j)
+        {
+#if 0
+            SkDebugf("x=%f  err=%f  err'=%f  err''=%f\n",
+                     x[j], rad2deg(skvx::fast_acos_x[j] - acosf(x[j])),
+                     rad2deg(err_[j]), rad2deg(err__[j]));
+#endif
+            if (!check_fast_acos(x[j], fast_acos_x[j]))
+            {
+                return;
+            }
+        }
+
+        // Use Newton's method to update the x values to locations closer to their local minimum or
+        // maximum. (This is where d/dx(error) == 0.)
+        x -= err_ / err__;
+        x = simd::clamp<float, 8>(x, -.99f, .99f);
+    }
+
+    // Verify each lane converged to a local minimum or maximum.
+    for (int j = 0; j < 8; ++j)
+    {
+        REQUIRE(math::nearly_zero(err_[j]));
+    }
+
+    // Make sure we found all the actual known locations of local min/max error.
+    for (float knownRoot : {-0.983536f, -0.867381f, -0.410923f, 0.410923f, 0.867381f, 0.983536f})
+    {
+        CHECK_ANY((simd::abs(x - knownRoot) < math::EPSILON));
+    }
+}
+
+TEST_CASE("cast", "[simd]")
+{
+    float4 f4 = float4{-1.9f, -1.5f, 1.5f, 1.1f};
+    CHECK(simd::all(simd::cast<int>(f4) == int4{-1, -1, 1, 1}));
+    CHECK(simd::all(simd::cast<int>(simd::floor(f4)) == int4{-2, -2, 1, 1}));
+    CHECK(simd::all(simd::cast<int>(simd::ceil(f4)) == int4{-1, -1, 2, 2}));
+    CHECK(simd::all(simd::cast<int>(simd::ceil(f4.zwxy)) == int4{2, 2, -1, -1}));
+    CHECK(simd::all(simd::cast<int>(simd::ceil(f4).zwxy) == int4{2, 2, -1, -1}));
+}
+
+// Check simd::dot.
+TEST_CASE("dot", "[simd]")
+{
+    CHECK(simd::dot(int2{0, 1}, int2{1, 0}) == 0);
+    CHECK(simd::dot(uint2{1, 0}, uint2{0, 1}) == 0);
+    CHECK(simd::dot(int2{1, 1}, int2{1, -1}) == 0);
+    CHECK(simd::dot(uint2{1, 1}, uint2{1, 1}) == 2);
+    CHECK(simd::dot(int2{1, 1}, int2{-1, -1}) == -2);
+    CHECK(simd::dot(ivec<3>{1, 2, -3}, ivec<3>{1, 2, 3}) == -4);
+    CHECK(simd::dot(uvec<3>{1, 2, 3}, uvec<3>{1, 2, 3}) == 14);
+    CHECK(simd::dot(int4{1, 2, 3, 4}, int4{1, 2, 3, 4}) == 30);
+    CHECK(simd::dot(ivec<5>{1, 2, 3, 4, 5}, ivec<5>{1, 2, 3, 4, -5}) == 5);
+    CHECK(simd::dot(uvec<5>{1, 2, 3, 4, 5}, uvec<5>{1, 2, 3, 4, 5}) == 55);
+
+    CHECK(simd::dot(float4{1, 2, 3, 4}, float4{4, 3, 2, 1}) == 20);
+    CHECK(simd::dot(vec<3>{1, 2, 3}, vec<3>{3, 2, 1}) == 10);
+    CHECK(simd::dot(float2{0, 1}, float2{1, 0}) == 0);
+    CHECK(simd::dot(vec<5>{1, 2, 3, 4, 5}, vec<5>{1, 2, 3, 4, 5}) == 55);
+}
+
+// Check simd::cross.
+TEST_CASE("cross", "[simd]")
+{
+    CHECK(simd::cross({0, 1}, {0, 1}) == 0);
+    CHECK(simd::cross({1, 0}, {1, 0}) == 0);
+    CHECK(simd::cross({1, 1}, {1, 1}) == 0);
+    CHECK(simd::cross({1, 1}, {1, -1}) == -2);
+    CHECK(simd::cross({1, 1}, {-1, 1}) == 2);
+}
+
+// Check simd::join
+TEST_CASE("join", "[simd]")
+{
+    CHECK_ALL((simd::join(int2{1, 2}, int4{3, 4, 5, 6}) == ivec<6>{1, 2, 3, 4, 5, 6}));
+    CHECK_ALL((simd::join(vec<1>{1}, vec<3>{2, 3, 4}) == float4{1, 2, 3, 4}));
+    CHECK_ALL((simd::join(vec<1>{1}, vec<2>{2, 3}, vec<3>{4, 5, 6}) == vec<6>{1, 2, 3, 4, 5, 6}));
+    CHECK_ALL((simd::join(vec<1>{1}, vec<2>{2, 3}, vec<3>{4, 5, 6}, float4{7, 8, 9, 10}) ==
+               vec<10>{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}));
+    uint8x8 a = 3, b = 9, c = 3, d = 100;
+    CHECK_ALL((simd::join(a, b, c, d) == uint8x32{3, 3, 3,   3,   3,   3,   3,   3,   9,   9,  9,
+                                                  9, 9, 9,   9,   9,   3,   3,   3,   3,   3,  3,
+                                                  3, 3, 100, 100, 100, 100, 100, 100, 100, 100}));
+}
+
+// Check simd::zip
+TEST_CASE("zip", "[simd]")
+{
+    CHECK_ALL((simd::zip(simd::gvec<char, 1>{'a'}, simd::gvec<char, 1>{'b'}) ==
+               simd::gvec<char, 2>{'a', 'b'}));
+    CHECK_ALL((simd::zip(int2{1, 2}, int2{3, 4}) == int4{1, 3, 2, 4}));
+    CHECK_ALL((simd::zip(int4{1, 2, 3, 4}, int4{5, 6, 7, 8}) == ivec<8>{1, 5, 2, 6, 3, 7, 4, 8}));
+    CHECK_ALL((simd::zip(simd::gvec<uint8_t, 8>{1, 2, 3, 4, 5, 6, 7, 8},
+                         simd::gvec<uint8_t, 8>{9, 10, 11, 12, 13, 14, 15, 16}) ==
+               simd::gvec<uint8_t, 16>{1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15, 8, 16}));
+    CHECK_ALL(
+        (simd::zip(float4{1, 2, 3, 4}, float4{5, 6, 7, 8}) == vec<8>{1, 5, 2, 6, 3, 7, 4, 8}));
+}
+
+template <int N> static vec<N> mix_reference_impl(vec<N> a, vec<N> b, float t)
+{
+    return a * (1.f - t) + b * t;
+}
+template <int N> static vec<N> mix_reference_impl(vec<N> a, vec<N> b, vec<N> t)
+{
+    return a * (1.f - t) + b * t;
+}
+
+template <typename T, int N> static bool fuzzy_equal(simd::gvec<T, N> a, simd::gvec<T, N> b)
+{
+    return simd::all(b - a < 1e-4f);
+}
+
+static float frand()
+{
+    float kMaxBelow1 = math::bit_cast<float>(math::bit_cast<uint32_t>(1.f) - 1);
+    float f = static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
+    return std::min(kMaxBelow1, f);
+}
+template <int N> vec<N> vrand()
+{
+    vec<N> vrand{};
+    for (int i = 0; i < N; ++i)
+    {
+        vrand[i] = frand();
+    }
+    return vrand;
+}
+
+template <int N> void check_mix()
+{
+    vec<N> a = vrand<N>();
+    vec<N> b = vrand<N>();
+    float t = frand();
+    CHECK(fuzzy_equal(simd::mix(a, b, vec<N>(t)), mix_reference_impl(a, b, t)));
+    CHECK(fuzzy_equal(simd::precise_mix(a, b, vec<N>(t)), mix_reference_impl(a, b, t)));
+    vec<N> tt = vrand<N>();
+    CHECK(fuzzy_equal(simd::mix(a, b, tt), mix_reference_impl(a, b, tt)));
+    CHECK(fuzzy_equal(simd::precise_mix(a, b, tt), mix_reference_impl(a, b, tt)));
+}
+
+// Check simd::mix
+TEST_CASE("mix", "[simd]")
+{
+    srand(0);
+    check_mix<1>();
+    check_mix<2>();
+    check_mix<3>();
+    check_mix<4>();
+    check_mix<5>();
+    CHECK_ALL((simd::mix(float4{1, 2, 3, 4}, float4{5, 6, 7, 8}, float4(0)) == float4{1, 2, 3, 4}));
+    CHECK_ALL((simd::precise_mix(float4{-1, 2, 3, 4}, float4{5, 6, 7, 8}, float4(0)) ==
+               float4{-1, 2, 3, 4}));
+    CHECK_ALL((simd::precise_mix(float4{1, 2, 3, 4}, float4{5, -6, 7, -8}, float4(1)) ==
+               float4{5, -6, 7, -8}));
+}
+
+// Check simd::load4x4f
+TEST_CASE("load4x4f", "[simd]")
+{
+    // Column major.
+    float m[16] = {0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15};
+    auto c = simd::load4x4f(m);
+    CHECK(simd::all(std::get<0>(c) == float4{0, 1, 2, 3}));
+    CHECK(simd::all(std::get<1>(c) == float4{4, 5, 6, 7}));
+    CHECK(simd::all(std::get<2>(c) == float4{8, 9, 10, 11}));
+    CHECK(simd::all(std::get<3>(c) == float4{12, 13, 14, 15}));
+}
+
+} // namespace rive
diff --git a/test/simple_array_test.cpp b/test/simple_array_test.cpp
new file mode 100644
index 0000000..132c10a
--- /dev/null
+++ b/test/simple_array_test.cpp
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include <rive/simple_array.hpp>
+#include <catch.hpp>
+#include <rive/text_engine.hpp>
+
+using namespace rive;
+
+TEST_CASE("array initializes as expected", "[simple array]")
+{
+    SimpleArray<int> array;
+    REQUIRE(array.empty());
+    REQUIRE(array.size() == 0);
+    REQUIRE(array.size_bytes() == 0);
+    REQUIRE(array.begin() == array.end());
+}
+
+TEST_CASE("simple array can be created", "[simple array]")
+{
+    std::vector<int> v{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+    SimpleArray<int> array(v);
+
+    REQUIRE(!array.empty());
+    REQUIRE(array.size() == 10);
+    REQUIRE(array.size_bytes() == 10 * sizeof(int));
+    REQUIRE(array.begin() + array.size() == array.end());
+
+    int counter = 0;
+    int sum = 0;
+    for (auto s : array)
+    {
+        counter += 1;
+        sum += s;
+    }
+    REQUIRE(counter == 10);
+    REQUIRE(sum == 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9);
+}
+
+TEST_CASE("can iterate simple array", "[simple array]")
+{
+    const int carray[] = {2, 4, 8, 16};
+    SimpleArray<int> array(carray, 4);
+    int expect = 2;
+    for (auto value : array)
+    {
+        REQUIRE(value == expect);
+        expect *= 2;
+    }
+}
+
+TEST_CASE("can build up a simple array", "[simple array]")
+{
+    SimpleArrayBuilder<int> builder;
+    builder.add(1);
+    builder.add(2);
+    REQUIRE(builder.size() == 2);
+    REQUIRE(builder.capacity() == 2);
+
+    builder.add(3);
+    REQUIRE(builder.size() == 3);
+    REQUIRE(builder.capacity() == 4);
+
+    int iterationCount = 0;
+    int expect = 1;
+    for (auto value : builder)
+    {
+        REQUIRE(value == expect++);
+        iterationCount++;
+    }
+    // Should only iterate what's been written so far.
+    REQUIRE(iterationCount == 3);
+
+    int reallocCountBeforeMove = SimpleArrayTesting::reallocCount;
+    SimpleArray<int> array = std::move(builder);
+    REQUIRE(array[0] == 1);
+    REQUIRE(array[1] == 2);
+    REQUIRE(array[2] == 3);
+    REQUIRE(array.size() == 3);
+    REQUIRE(SimpleArrayTesting::reallocCount == reallocCountBeforeMove + 1);
+}
+
+struct StructA
+{
+    rive::SimpleArray<uint32_t> numbers;
+};
+
+static SimpleArray<StructA> buildStructs()
+{
+    SimpleArrayTesting::resetCounters();
+
+    std::vector<uint32_t> vA{33, 22, 44, 66};
+    SimpleArray<uint32_t> numbersA(vA);
+
+    StructA dataA = {std::move(numbersA)};
+    // We moved the data so expect only one alloc and 0 reallocs.
+    REQUIRE(SimpleArrayTesting::mallocCount == 1);
+    REQUIRE(SimpleArrayTesting::reallocCount == 0);
+    REQUIRE(dataA.numbers.size() == 4);
+    REQUIRE(numbersA.size() == 0);
+
+    std::vector<uint32_t> vB{1, 2, 3};
+    SimpleArray<uint32_t> numbersB(vB);
+
+    StructA dataB = {std::move(numbersB)};
+    REQUIRE(SimpleArrayTesting::mallocCount == 2);
+    REQUIRE(SimpleArrayTesting::reallocCount == 0);
+    REQUIRE(dataB.numbers.size() == 3);
+    REQUIRE(numbersB.size() == 0);
+
+    SimpleArray<StructA> structs(2);
+    structs[0] = std::move(dataA);
+    structs[1] = std::move(dataB);
+    // Should've alloc one more time to create the structs object.
+    REQUIRE(SimpleArrayTesting::mallocCount == 3);
+    REQUIRE(SimpleArrayTesting::reallocCount == 0);
+    REQUIRE(structs.size() == 2);
+    REQUIRE(structs[0].numbers.size() == 4);
+    REQUIRE(structs[1].numbers.size() == 3);
+    return structs;
+}
+
+TEST_CASE("arrays of arrays work", "[simple array]")
+{
+    auto structs = buildStructs();
+    REQUIRE(SimpleArrayTesting::mallocCount == 3);
+    REQUIRE(SimpleArrayTesting::reallocCount == 0);
+    REQUIRE(structs.size() == 2);
+    REQUIRE(structs[0].numbers.size() == 4);
+    REQUIRE(structs[1].numbers.size() == 3);
+}
+
+static SimpleArray<StructA> buildStructsWithBuilder()
+{
+    SimpleArrayTesting::resetCounters();
+    SimpleArrayBuilder<StructA> structs(2);
+    REQUIRE(SimpleArrayTesting::mallocCount == 1);
+    for (int i = 0; i < 3; i++)
+    {
+        SimpleArray<uint32_t> numbers({33, 22, 44, 66});
+        StructA data = {std::move(numbers)};
+        structs.add(std::move(data));
+    }
+    REQUIRE(SimpleArrayTesting::mallocCount == 4);
+    // Realloc once because we'd reserved 2 and actually added 3.
+    REQUIRE(SimpleArrayTesting::reallocCount == 1);
+    return std::move(structs);
+}
+
+TEST_CASE("builder arrays of arrays work", "[simple array]")
+{
+    auto structs = buildStructsWithBuilder();
+    // alloc counters should still be the same
+    REQUIRE(SimpleArrayTesting::mallocCount == 4);
+    // Realloc one more time as we sized down.
+    REQUIRE(SimpleArrayTesting::reallocCount == 2);
+}
+
+TEST_CASE("builders can be reset", "[simple array]")
+{
+    SimpleArrayTesting::resetCounters();
+    SimpleArrayBuilder<uint32_t> builder(3);
+    builder.add(1);
+    builder.add(2);
+    builder.add(3);
+    REQUIRE(SimpleArrayTesting::mallocCount == 1);
+    REQUIRE(SimpleArrayTesting::freeCount == 0);
+    REQUIRE(SimpleArrayTesting::reallocCount == 0);
+
+    builder = SimpleArrayBuilder<uint32_t>(4);
+    // Previous builder got freed.
+    REQUIRE(SimpleArrayTesting::freeCount == 1);
+    // We allocated more memory.
+    REQUIRE(SimpleArrayTesting::mallocCount == 2);
+    REQUIRE(SimpleArrayTesting::reallocCount == 0);
+    builder.add(3);
+    builder.add(2);
+
+    SimpleArrayTesting::resetCounters();
+    SimpleArray<uint32_t> array = std::move(builder);
+    // Realloc'd down
+    REQUIRE(SimpleArrayTesting::reallocCount == 1);
+    // Free and malloc counts didn't move.
+    REQUIRE(SimpleArrayTesting::freeCount == 0);
+    REQUIRE(SimpleArrayTesting::mallocCount == 0);
+    REQUIRE(array.size() == 2);
+}
diff --git a/test/solo_test.cpp b/test/solo_test.cpp
new file mode 100644
index 0000000..30d57b0
--- /dev/null
+++ b/test/solo_test.cpp
@@ -0,0 +1,308 @@
+#include <rive/solo.hpp>
+#include <rive/shapes/shape.hpp>
+#include <rive/shapes/path.hpp>
+#include <rive/animation/state_machine_instance.hpp>
+#include <rive/animation/state_machine_input_instance.hpp>
+#include <rive/nested_artboard.hpp>
+#include <rive/shapes/paint/fill.hpp>
+#include <rive/shapes/paint/solid_color.hpp>
+#include "rive_file_reader.hpp"
+#include <catch.hpp>
+#include <cstdio>
+
+TEST_CASE("file with skins in solos loads correctly", "[solo]")
+{
+    auto file = ReadRiveFile("../../test/assets/death_knight.riv");
+
+    auto artboard = file->artboard()->instance();
+    artboard->advance(0.0f);
+    auto solos = artboard->find<rive::Solo>();
+    REQUIRE(solos.size() == 2);
+}
+
+TEST_CASE("children load correctly", "[solo]")
+{
+    auto file = ReadRiveFile("../../test/assets/solo_test.riv");
+
+    auto artboard = file->artboard()->instance();
+    artboard->advance(0.0f);
+    auto solos = artboard->find<rive::Solo>();
+    REQUIRE(solos.size() == 1);
+    auto solo = solos[0];
+    REQUIRE(solo != nullptr);
+    REQUIRE(solo->children().size() == 3);
+    REQUIRE(solo->children()[0]->is<rive::Shape>());
+    REQUIRE(solo->children()[0]->name() == "Blue");
+    REQUIRE(solo->children()[1]->is<rive::Shape>());
+    REQUIRE(solo->children()[1]->name() == "Green");
+    REQUIRE(solo->children()[2]->is<rive::Shape>());
+    REQUIRE(solo->children()[2]->name() == "Red");
+
+    auto blue = solo->children()[0]->as<rive::Shape>();
+    auto green = solo->children()[1]->as<rive::Shape>();
+    auto red = solo->children()[2]->as<rive::Shape>();
+
+    REQUIRE(!blue->isHidden());
+    REQUIRE(green->isHidden());
+    REQUIRE(red->isHidden());
+
+    REQUIRE(green->children().size() == 2);
+    REQUIRE(green->children()[0]->isCollapsed());
+    REQUIRE(green->children()[1]->isCollapsed());
+
+    REQUIRE(red->children().size() == 2);
+    REQUIRE(red->children()[0]->isCollapsed());
+    REQUIRE(red->children()[1]->isCollapsed());
+
+    auto machine = artboard->defaultStateMachine();
+    machine->advanceAndApply(0.0);
+    // Red visible at start
+    REQUIRE(blue->isHidden());
+    REQUIRE(green->isHidden());
+    REQUIRE(!red->isHidden());
+
+    machine->advanceAndApply(0.5);
+    // Green visible after 0.5 seconds.
+    REQUIRE(blue->isHidden());
+    REQUIRE(!green->isHidden());
+    REQUIRE(red->isHidden());
+
+    machine->advanceAndApply(0.5);
+    // Blue visible at end
+    REQUIRE(!blue->isHidden());
+    REQUIRE(green->isHidden());
+    REQUIRE(red->isHidden());
+}
+
+TEST_CASE("nested solos work", "[solo]")
+{
+    auto file = ReadRiveFile("../../test/assets/nested_solo.riv");
+
+    auto artboard = file->artboard()->instance();
+    artboard->advance(0.0f);
+    auto s1 = artboard->find<rive::Solo>("Solo 1");
+    REQUIRE(s1 != nullptr);
+    auto s2 = artboard->find<rive::Solo>("Solo 2");
+    REQUIRE(s2 != nullptr);
+    auto s3 = artboard->find<rive::Solo>("Solo 3");
+    REQUIRE(s3 != nullptr);
+
+    auto a = artboard->find<rive::Shape>("A");
+    REQUIRE(a != nullptr);
+    auto b = artboard->find<rive::Shape>("B");
+    REQUIRE(b != nullptr);
+    auto c = artboard->find<rive::Shape>("C");
+    REQUIRE(c != nullptr);
+    auto d = artboard->find<rive::Shape>("D");
+    REQUIRE(d != nullptr);
+    auto e = artboard->find<rive::Shape>("E");
+    REQUIRE(e != nullptr);
+    auto f = artboard->find<rive::Shape>("F");
+    REQUIRE(f != nullptr);
+    auto g = artboard->find<rive::Shape>("G");
+    REQUIRE(g != nullptr);
+    auto h = artboard->find<rive::Shape>("H");
+    REQUIRE(h != nullptr);
+    auto i = artboard->find<rive::Shape>("I");
+    REQUIRE(i != nullptr);
+
+    s1->activeComponentId(artboard->idOf(a));
+    s2->activeComponentId(artboard->idOf(d));
+    s3->activeComponentId(artboard->idOf(h));
+    artboard->advance(0.0f);
+
+    REQUIRE(a->isCollapsed() == false);
+    REQUIRE(b->isCollapsed() == true);
+    REQUIRE(c->isCollapsed() == true);
+
+    REQUIRE(d->isCollapsed() == true);
+    REQUIRE(e->isCollapsed() == true);
+    REQUIRE(f->isCollapsed() == true);
+
+    REQUIRE(g->isCollapsed() == true);
+    REQUIRE(h->isCollapsed() == true);
+    REQUIRE(i->isCollapsed() == true);
+
+    // Changing active in a collapsed solo doesn't affect anything.
+    s3->activeComponentId(artboard->idOf(g));
+    artboard->advance(0.0f);
+
+    REQUIRE(a->isCollapsed() == false);
+    REQUIRE(b->isCollapsed() == true);
+    REQUIRE(c->isCollapsed() == true);
+
+    REQUIRE(d->isCollapsed() == true);
+    REQUIRE(e->isCollapsed() == true);
+    REQUIRE(f->isCollapsed() == true);
+
+    REQUIRE(g->isCollapsed() == true);
+    REQUIRE(h->isCollapsed() == true);
+    REQUIRE(i->isCollapsed() == true);
+
+    s1->activeComponentId(artboard->idOf(c));
+    artboard->advance(0.0);
+
+    // Now the rest of the nested solo items should be visible.
+    REQUIRE(a->isCollapsed() == true);
+    REQUIRE(b->isCollapsed() == true);
+    REQUIRE(c->isCollapsed() == false);
+
+    REQUIRE(d->isCollapsed() == false);
+    REQUIRE(e->isCollapsed() == true);
+    REQUIRE(f->isCollapsed() == true);
+
+    REQUIRE(g->isCollapsed() == false);
+    REQUIRE(h->isCollapsed() == true);
+    REQUIRE(i->isCollapsed() == true);
+}
+
+TEST_CASE("hit test on solos", "[solo]")
+{
+    auto file = ReadRiveFile("../../test/assets/hit_test_solos.riv");
+
+    auto artboard = file->artboard()->instance();
+
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->stateMachineCount() == 1);
+
+    auto stateMachine = artboard->stateMachineAt(0);
+    REQUIRE(stateMachine != nullptr);
+
+    stateMachine->advance(0.0f);
+    artboard->advance(0.0f);
+
+    auto toggle = stateMachine->getBool("hovered");
+    REQUIRE(toggle != nullptr);
+
+    // Inactive shape position
+    stateMachine->pointerMove(rive::Vec2D(200.0f, 100.0f));
+    REQUIRE(toggle->value() == true);
+
+    // // Active shape position
+    stateMachine->pointerMove(rive::Vec2D(200.0f, 300.0f));
+    REQUIRE(toggle->value() == false);
+
+    // // Inactive shape position
+    stateMachine->pointerMove(rive::Vec2D(200.0f, 400.0f));
+    REQUIRE(toggle->value() == false);
+
+    // Switches active shape to middle one
+    stateMachine->advance(1.5f);
+    artboard->advance(1.5f);
+
+    // Inactive shape position
+    stateMachine->pointerMove(rive::Vec2D(200.0f, 100.0f));
+    REQUIRE(toggle->value() == false);
+
+    // // Active shape position
+    stateMachine->pointerMove(rive::Vec2D(200.0f, 300.0f));
+    REQUIRE(toggle->value() == true);
+
+    // // Inactive shape position
+    stateMachine->pointerMove(rive::Vec2D(200.0f, 400.0f));
+    REQUIRE(toggle->value() == false);
+
+    // Switches active shape to last one
+    stateMachine->advance(1.0f);
+    artboard->advance(1.0f);
+
+    // Inactive shape position
+    stateMachine->pointerMove(rive::Vec2D(200.0f, 100.0f));
+    REQUIRE(toggle->value() == false);
+
+    // // Inactive shape position
+    stateMachine->pointerMove(rive::Vec2D(200.0f, 300.0f));
+    REQUIRE(toggle->value() == false);
+
+    // // Active shape position
+    stateMachine->pointerMove(rive::Vec2D(200.0f, 400.0f));
+    REQUIRE(toggle->value() == true);
+}
+
+TEST_CASE("hit test on nested artboards in solos", "[solo]")
+{
+    auto file = ReadRiveFile("../../test/assets/pointer_events_nested_artboards_in_solos.riv");
+
+    auto mainArtboard = file->artboard()->instance();
+
+    auto green_color = 0xFF00B511;
+    auto red_color = 0xFFC80000;
+    auto gray_color = 0xFF747474;
+
+    REQUIRE(mainArtboard->find("Parent-Artboard") != nullptr);
+    auto artboard = mainArtboard->find<rive::Artboard>("Parent-Artboard");
+
+    REQUIRE(artboard != nullptr);
+    artboard->updateComponents();
+    REQUIRE(artboard->is<rive::Artboard>());
+    REQUIRE(artboard->find("Nested-Artboard-Active") != nullptr);
+    auto nestedArtboardActive = artboard->find<rive::NestedArtboard>("Nested-Artboard-Active");
+    REQUIRE(nestedArtboardActive->artboardInstance() != nullptr);
+
+    auto nestedArtboardActiveArtboardInstance = nestedArtboardActive->artboardInstance();
+    auto activeRect =
+        nestedArtboardActiveArtboardInstance->find<rive::Shape>("Clickable-Rectangle");
+    REQUIRE(activeRect != nullptr);
+    auto activeRectFill = activeRect->children()[1]->as<rive::Fill>();
+    REQUIRE(activeRectFill != nullptr);
+    auto activeRectFillSolidColor = activeRectFill->paint()->as<rive::SolidColor>();
+    REQUIRE(activeRectFillSolidColor != nullptr);
+
+    REQUIRE(artboard->find("Nested-Artboard-Inactive") != nullptr);
+    auto nestedArtboardInactive = artboard->find<rive::NestedArtboard>("Nested-Artboard-Inactive");
+    REQUIRE(nestedArtboardInactive->artboardInstance() != nullptr);
+    auto nestedArtboardInactiveArtboardInstance = nestedArtboardInactive->artboardInstance();
+    auto inactiveRect =
+        nestedArtboardInactiveArtboardInstance->find<rive::Shape>("Clickable-Rectangle");
+    REQUIRE(inactiveRect != nullptr);
+    auto inactiveRectFill = inactiveRect->children()[1]->as<rive::Fill>();
+    REQUIRE(inactiveRectFill != nullptr);
+    auto inactiveRectFillSolidColor = inactiveRectFill->paint()->as<rive::SolidColor>();
+    REQUIRE(inactiveRectFillSolidColor != nullptr);
+
+    REQUIRE(artboard->stateMachineCount() == 1);
+
+    auto stateMachine = mainArtboard->stateMachineAt(0);
+    REQUIRE(stateMachine != nullptr);
+
+    // Initialize state machine
+    stateMachine->advance(0.0f);
+    mainArtboard->advance(0.0f);
+
+    REQUIRE(nestedArtboardActive->isCollapsed() == false);
+    REQUIRE(nestedArtboardInactive->isCollapsed() == true);
+    REQUIRE(activeRectFillSolidColor->colorValue() == green_color);
+    REQUIRE(inactiveRectFillSolidColor->colorValue() == gray_color);
+
+    // Advance to a position in the timeline where the inactive artboard
+    // is active so it is rendered once
+    stateMachine->advance(0.1f);
+    mainArtboard->advance(0.1f);
+
+    REQUIRE(nestedArtboardActive->isCollapsed() == true);
+    REQUIRE(nestedArtboardInactive->isCollapsed() == false);
+    REQUIRE(inactiveRectFillSolidColor->colorValue() == green_color);
+
+    // Advance to a position in the timeline where the active artboard is active
+    stateMachine->advance(0.1f);
+    mainArtboard->advance(0.1f);
+
+    REQUIRE(nestedArtboardActive->isCollapsed() == false);
+    REQUIRE(nestedArtboardInactive->isCollapsed() == true);
+
+    // Apply pointer up
+    stateMachine->pointerUp(rive::Vec2D(200.0f, 200.0f));
+    stateMachine->advance(0.0f);
+    artboard->advance(0.0f);
+
+    // Advance to activate the inactive artboard again so it redraws
+    stateMachine->advance(0.1f);
+    artboard->advance(0.1f);
+    REQUIRE(nestedArtboardActive->isCollapsed() == true);
+    REQUIRE(nestedArtboardInactive->isCollapsed() == false);
+
+    // If the test succeeds the active nested artboard should have changed to red
+    REQUIRE(activeRectFillSolidColor->colorValue() == red_color);
+    // And the inactive artboard should have stayed green
+    REQUIRE(inactiveRectFillSolidColor->colorValue() == green_color);
+}
diff --git a/test/span_test.cpp b/test/span_test.cpp
new file mode 100644
index 0000000..874b766
--- /dev/null
+++ b/test/span_test.cpp
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include <rive/span.hpp>
+#include <catch.hpp>
+#include <cstdio>
+#include <vector>
+
+using namespace rive;
+
+TEST_CASE("basics", "[span]")
+{
+    Span<int> span;
+    REQUIRE(span.empty());
+    REQUIRE(span.size() == 0);
+    REQUIRE(span.size_bytes() == 0);
+    REQUIRE(span.begin() == span.end());
+
+    int array[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
+
+    span = {array, 4};
+    REQUIRE(!span.empty());
+    REQUIRE(span.data() == array);
+    REQUIRE(span.size() == 4);
+    REQUIRE(span.size_bytes() == 4 * sizeof(int));
+    REQUIRE(span.begin() + span.size() == span.end());
+
+    int counter = 0;
+    int sum = 0;
+    for (auto s : span)
+    {
+        counter += 1;
+        sum += s;
+    }
+    REQUIRE(counter == 4);
+    REQUIRE(sum == 0 + 1 + 2 + 3);
+
+    auto sub = span.subset(1, 2);
+    REQUIRE(!sub.empty());
+    REQUIRE(sub.data() == array + 1);
+    REQUIRE(sub.size() == 2);
+
+    sub = sub.subset(1, 0);
+    REQUIRE(sub.empty());
+    REQUIRE(sub.size() == 0);
+}
+
+static void funca(Span<int> span) {}
+static void funcb(Span<const int> span) {}
+
+TEST_CASE("const-and-containers", "[span]")
+{
+    const int carray[] = {1, 2, 3, 4};
+    funcb({carray, 4});
+
+    int array[] = {1, 2, 3, 4};
+    funca({array, 4});
+    funcb({array, 4});
+
+    std::vector<int> v;
+    funca(v);
+    funcb(v);
+}
+
+TEST_CASE("can iterate span", "[span]")
+{
+    const int carray[] = {2, 4, 8, 16};
+
+    auto span = make_span(carray, 4);
+    int expect = 2;
+    for (auto value : span)
+    {
+        REQUIRE(value == expect);
+        expect *= 2;
+    }
+}
diff --git a/test/state_machine_event_test.cpp b/test/state_machine_event_test.cpp
new file mode 100644
index 0000000..e4551d8
--- /dev/null
+++ b/test/state_machine_event_test.cpp
@@ -0,0 +1,322 @@
+#include "rive/core/binary_reader.hpp"
+#include "rive/file.hpp"
+#include "rive/event.hpp"
+#include "rive/shapes/shape.hpp"
+#include "rive/animation/state_machine_bool.hpp"
+#include "rive/animation/state_machine_layer.hpp"
+#include "rive/animation/state_machine_listener.hpp"
+#include "rive/animation/animation_state.hpp"
+#include "rive/animation/entry_state.hpp"
+#include "rive/animation/state_transition.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+#include "rive/animation/blend_state_1d.hpp"
+#include "rive/animation/blend_animation_1d.hpp"
+#include "rive/animation/blend_state_direct.hpp"
+#include "rive/animation/blend_state_transition.hpp"
+#include "rive/animation/listener_input_change.hpp"
+#include "rive/animation/listener_fire_event.hpp"
+#include "rive/animation/nested_state_machine.hpp"
+#include "rive/animation/entry_state.hpp"
+#include "rive/node.hpp"
+#include "catch.hpp"
+#include "rive_file_reader.hpp"
+#include <cstdio>
+
+TEST_CASE("file with state machine listeners be read", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/bullet_man.riv");
+
+    auto artboard = file->artboard("Bullet Man");
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->stateMachineCount() == 1);
+
+    auto stateMachine = artboard->stateMachine(0);
+    REQUIRE(stateMachine != nullptr);
+
+    REQUIRE(stateMachine->listenerCount() == 3);
+    REQUIRE(stateMachine->inputCount() == 4);
+
+    // Expect each of the three listeners to have one input change each.
+    auto listener1 = stateMachine->listener(0);
+    auto target1 = artboard->resolve(listener1->targetId());
+    REQUIRE(target1->is<rive::Node>());
+    REQUIRE(target1->as<rive::Node>()->name() == "HandWickHit");
+    REQUIRE(listener1->actionCount() == 1);
+    auto inputChange1 = listener1->action(0);
+    REQUIRE(inputChange1 != nullptr);
+    REQUIRE(inputChange1->is<rive::ListenerInputChange>());
+    REQUIRE(inputChange1->as<rive::ListenerInputChange>()->inputId() == 0);
+
+    auto listener2 = stateMachine->listener(1);
+    auto target2 = artboard->resolve(listener2->targetId());
+    REQUIRE(target2->is<rive::Node>());
+    REQUIRE(target2->as<rive::Node>()->name() == "HandCannonHit");
+    REQUIRE(listener2->actionCount() == 1);
+    auto inputChange2 = listener2->action(0);
+    REQUIRE(inputChange2 != nullptr);
+    REQUIRE(inputChange2->is<rive::ListenerInputChange>());
+    REQUIRE(inputChange2->as<rive::ListenerInputChange>()->inputId() == 1);
+
+    auto listener3 = stateMachine->listener(2);
+    auto target3 = artboard->resolve(listener3->targetId());
+    REQUIRE(target3->is<rive::Node>());
+    REQUIRE(target3->as<rive::Node>()->name() == "HandHelmetHit");
+    REQUIRE(listener3->actionCount() == 1);
+    auto inputChange3 = listener3->action(0);
+    REQUIRE(inputChange3 != nullptr);
+    REQUIRE(inputChange3->is<rive::ListenerInputChange>());
+    REQUIRE(inputChange3->as<rive::ListenerInputChange>()->inputId() == 2);
+}
+
+TEST_CASE("hit testing via a state machine works", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/bullet_man.riv");
+
+    auto artboard = file->artboard("Bullet Man")->instance();
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->stateMachineCount() == 1);
+
+    auto stateMachine = artboard->stateMachineAt(0);
+    REQUIRE(stateMachine != nullptr);
+    // Advance artboard once so design time state is effectively in the transforms.
+    artboard->advance(0.0f);
+    stateMachine->advance(0.0f);
+    // Don't advance artboard again after applying state machine or our pointerDown will be off. The
+    // coordinates used in this test were from the design-time state.
+
+    auto trigger = stateMachine->getTrigger("Light");
+    REQUIRE(trigger != nullptr);
+    stateMachine->pointerDown(rive::Vec2D(71.0f, 263.0f));
+
+    REQUIRE(trigger->didFire());
+}
+
+TEST_CASE("hit a toggle boolean listener", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/light_switch.riv");
+
+    auto artboard = file->artboard()->instance();
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->stateMachineCount() == 1);
+
+    auto stateMachine = artboard->stateMachineAt(0);
+    REQUIRE(stateMachine != nullptr);
+
+    artboard->advance(0.0f);
+    stateMachine->advance(0.0f);
+
+    auto switchButton = stateMachine->getBool("On");
+    REQUIRE(switchButton != nullptr);
+    REQUIRE(switchButton->value() == true);
+
+    stateMachine->pointerDown(rive::Vec2D(150.0f, 258.0f));
+    stateMachine->pointerUp(rive::Vec2D(150.0f, 258.0f));
+    // Got toggled off after pressing
+    REQUIRE(switchButton->value() == false);
+
+    stateMachine->pointerDown(rive::Vec2D(150.0f, 258.0f));
+    stateMachine->pointerUp(rive::Vec2D(150.0f, 258.0f));
+    // Got toggled back on after pressing
+    REQUIRE(switchButton->value() == true);
+}
+
+TEST_CASE("can query for all rive events", "[events]")
+{
+    auto file = ReadRiveFile("../../test/assets/event_on_listener.riv");
+    auto artboard = file->artboard();
+
+    auto eventCount = artboard->count<rive::Event>();
+    REQUIRE(eventCount == 4);
+}
+
+TEST_CASE("can query for a rive event at a given index", "[events]")
+{
+    auto file = ReadRiveFile("../../test/assets/event_on_listener.riv");
+    auto artboard = file->artboard();
+
+    auto event = artboard->objectAt<rive::Event>(0);
+    REQUIRE(event->name() == "Somewhere.com");
+}
+
+TEST_CASE("events load correctly on a listener", "[events]")
+{
+    auto file = ReadRiveFile("../../test/assets/event_on_listener.riv");
+
+    auto artboard = file->artboard()->instance();
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->stateMachineCount() == 1);
+
+    auto stateMachineInstance = artboard->stateMachineAt(0);
+    REQUIRE(stateMachineInstance != nullptr);
+
+    artboard->advance(0.0f);
+    stateMachineInstance->advance(0.0f);
+
+    auto events = artboard->find<rive::Event>();
+    REQUIRE(events.size() == 4);
+
+    REQUIRE(stateMachineInstance->stateMachine()->listenerCount() == 1);
+    auto listener1 = stateMachineInstance->stateMachine()->listener(0);
+    auto target1 = artboard->resolve(listener1->targetId());
+    REQUIRE(target1->is<rive::Shape>());
+    REQUIRE(listener1->actionCount() == 2);
+    auto fireEvent1 = listener1->action(0);
+    REQUIRE(fireEvent1 != nullptr);
+    REQUIRE(fireEvent1->is<rive::ListenerFireEvent>());
+    REQUIRE(fireEvent1->as<rive::ListenerFireEvent>()->eventId() != 0);
+    auto event = artboard->resolve(fireEvent1->as<rive::ListenerFireEvent>()->eventId());
+    REQUIRE(event->is<rive::Event>());
+    REQUIRE(event->as<rive::Event>()->name() == "Footstep");
+
+    REQUIRE(stateMachineInstance->reportedEventCount() == 0);
+    stateMachineInstance->pointerDown(rive::Vec2D(343.0f, 116.0f));
+    stateMachineInstance->pointerUp(rive::Vec2D(343.0f, 116.0f));
+
+    // There are two events on the listener.
+    REQUIRE(stateMachineInstance->reportedEventCount() == 2);
+    auto reportedEvent1 = stateMachineInstance->reportedEventAt(0);
+    REQUIRE(reportedEvent1.event()->name() == "Footstep");
+    auto reportedEvent2 = stateMachineInstance->reportedEventAt(1);
+    REQUIRE(reportedEvent2.event()->name() == "Event 3");
+
+    // After advancing again the reportedEventCount should return to 0.
+    stateMachineInstance->advance(0.0f);
+    REQUIRE(stateMachineInstance->reportedEventCount() == 0);
+}
+
+TEST_CASE("events load correctly on a state and transition", "[events]")
+{
+    auto file = ReadRiveFile("../../test/assets/events_on_states.riv");
+
+    auto artboard = file->artboard()->instance();
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->stateMachineCount() == 1);
+
+    auto stateMachineInstance = artboard->stateMachineAt(0);
+    REQUIRE(stateMachineInstance != nullptr);
+
+    artboard->advance(0.0f);
+    stateMachineInstance->advance(0.0f);
+
+    REQUIRE(stateMachineInstance->stateMachine()->layerCount() == 1);
+    auto layer = stateMachineInstance->stateMachine()->layer(0);
+    REQUIRE(layer->stateCount() == 5);
+    REQUIRE(layer->entryState()->transitionCount() == 1);
+    auto transition = layer->entryState()->transition(0);
+
+    // No events on transition from entry.
+    REQUIRE(transition->events().size() == 0);
+    REQUIRE(transition->stateTo()->is<rive::AnimationState>());
+    auto firstAnimationState = transition->stateTo()->as<rive::AnimationState>();
+    REQUIRE(firstAnimationState->events().size() == 2);
+    REQUIRE(firstAnimationState->transitionCount() == 1);
+    transition = firstAnimationState->transition(0);
+    // Transition from first animation state to next one should have two events.
+    REQUIRE(transition->events().size() == 2);
+
+    // First should've fired as we immediately went to Timeline 1.
+    REQUIRE(stateMachineInstance->reportedEventCount() == 1);
+    REQUIRE(stateMachineInstance->reportedEventAt(0).event()->name() == "First");
+
+    stateMachineInstance->advance(1.0f);
+    // Exits after 2 seconds so 1 second in no events should've fired yet
+    REQUIRE(stateMachineInstance->reportedEventCount() == 0);
+
+    stateMachineInstance->advance(1.0f);
+    // At 2 seconds 2 events should fire, one for exiting the state and for taking the transition.
+    REQUIRE(stateMachineInstance->reportedEventCount() == 2);
+    REQUIRE(stateMachineInstance->reportedEventAt(0).event()->name() == "Second");
+    REQUIRE(stateMachineInstance->reportedEventAt(1).event()->name() == "Third");
+
+    stateMachineInstance->advance(1.0f);
+    // Another second in the transition should complete
+    REQUIRE(stateMachineInstance->reportedEventCount() == 1);
+    REQUIRE(stateMachineInstance->reportedEventAt(0).event()->name() == "Fourth");
+}
+
+TEST_CASE("timeline events load correctly and report", "[events]")
+{
+    auto file = ReadRiveFile("../../test/assets/timeline_event_test.riv");
+
+    auto artboard = file->artboard()->instance();
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->stateMachineCount() == 1);
+
+    auto stateMachineInstance = artboard->stateMachineAt(0);
+    REQUIRE(stateMachineInstance != nullptr);
+
+    artboard->advance(0.0f);
+    stateMachineInstance->advance(0.0f);
+    REQUIRE(stateMachineInstance->reportedEventCount() == 0);
+
+    stateMachineInstance->advance(0.4f);
+    REQUIRE(stateMachineInstance->reportedEventCount() == 0);
+
+    stateMachineInstance->advance(0.2f);
+    REQUIRE(stateMachineInstance->reportedEventCount() == 1);
+    REQUIRE(stateMachineInstance->reportedEventAt(0).event()->name() == "Half");
+
+    // Event should've occurred right at 0.5 seconds.
+    REQUIRE(stateMachineInstance->reportedEventAt(0).secondsDelay() == Approx(0.1f));
+}
+
+TEST_CASE("events from a nested artboard propagate to a listener on a parent", "[events]")
+{
+    auto file = ReadRiveFile("../../test/assets/nested_event_test.riv");
+
+    auto artboard = file->artboard()->instance();
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->stateMachineCount() == 1);
+
+    auto stateMachineInstance = artboard->stateMachineAt(0);
+    REQUIRE(stateMachineInstance != nullptr);
+    REQUIRE(stateMachineInstance->stateMachine()->inputCount() == 1);
+
+    // Input is on the main artboard
+    auto input = stateMachineInstance->getBool("Boolean 1");
+    REQUIRE(input->value() == false);
+
+    artboard->advance(0.0f);
+    stateMachineInstance->advance(0.0f);
+
+    auto nested = artboard->find<rive::NestedArtboard>();
+    REQUIRE(nested.size() == 1);
+    auto nestedArtboard = nested[0]->artboardInstance();
+    auto nestedStateMachineInstance =
+        nested[0]->nestedAnimations()[0]->as<rive::NestedStateMachine>()->stateMachineInstance();
+    REQUIRE(nestedStateMachineInstance != nullptr);
+    auto events = nestedArtboard->find<rive::Event>();
+    REQUIRE(events.size() == 1);
+
+    // Validate listener on the nested artboard
+    REQUIRE(nestedStateMachineInstance->stateMachine()->listenerCount() == 1);
+    auto listener1 = nestedStateMachineInstance->stateMachine()->listener(0);
+    auto target1 = nestedArtboard->resolve(listener1->targetId());
+    REQUIRE(target1->is<rive::Shape>());
+    REQUIRE(listener1->actionCount() == 1);
+    auto fireEvent1 = listener1->action(0);
+    REQUIRE(fireEvent1 != nullptr);
+    REQUIRE(fireEvent1->is<rive::ListenerFireEvent>());
+    REQUIRE(fireEvent1->as<rive::ListenerFireEvent>()->eventId() != 0);
+    auto event = nestedArtboard->resolve(fireEvent1->as<rive::ListenerFireEvent>()->eventId());
+    REQUIRE(event->is<rive::Event>());
+    REQUIRE(event->as<rive::Event>()->name() == "NestedEvent");
+
+    // Validate the event is reported to the nested artboard
+    REQUIRE(nestedStateMachineInstance->reportedEventCount() == 0);
+    stateMachineInstance->pointerDown(rive::Vec2D(250.0f, 100.0f));
+    REQUIRE(nestedStateMachineInstance->reportedEventCount() == 1);
+    auto nestedReportedEvent1 = nestedStateMachineInstance->reportedEventAt(0);
+    REQUIRE(nestedReportedEvent1.event()->name() == "NestedEvent");
+
+    artboard->advance(0.0f);
+
+    // Validate the input on the main artboard updates as a result of the event
+    // from the nested artboard
+    REQUIRE(input->value() == true);
+
+    // After advancing again the reportedEventCount should return to 0.
+    stateMachineInstance->advance(0.0f);
+    REQUIRE(stateMachineInstance->reportedEventCount() == 0);
+}
diff --git a/test/state_machine_test.cpp b/test/state_machine_test.cpp
new file mode 100644
index 0000000..6f25ae8
--- /dev/null
+++ b/test/state_machine_test.cpp
@@ -0,0 +1,416 @@
+#include <rive/file.hpp>
+#include <rive/animation/state_machine_bool.hpp>
+#include <rive/animation/state_machine_layer.hpp>
+#include <rive/animation/animation_state.hpp>
+#include <rive/animation/entry_state.hpp>
+#include <rive/animation/state_transition.hpp>
+#include <rive/animation/state_machine_instance.hpp>
+#include <rive/animation/state_machine_input_instance.hpp>
+#include <rive/animation/blend_state_1d.hpp>
+#include <rive/animation/blend_animation_1d.hpp>
+#include <rive/animation/blend_state_direct.hpp>
+#include <rive/animation/blend_state_transition.hpp>
+#include <rive/animation/animation_reset_factory.hpp>
+#include <rive/shapes/paint/solid_color.hpp>
+#include <rive/shapes/paint/stroke.hpp>
+#include <rive/shapes/shape.hpp>
+#include "catch.hpp"
+#include "rive_file_reader.hpp"
+#include <cstdio>
+
+TEST_CASE("file with state machine be read", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/rocket.riv");
+
+    auto artboard = file->artboard();
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->animationCount() == 3);
+    REQUIRE(artboard->stateMachineCount() == 1);
+
+    auto stateMachine = artboard->stateMachine("Button");
+    REQUIRE(stateMachine != nullptr);
+
+    REQUIRE(stateMachine->layerCount() == 1);
+    REQUIRE(stateMachine->inputCount() == 2);
+
+    auto hover = stateMachine->input("Hover");
+    REQUIRE(hover != nullptr);
+    REQUIRE(hover->is<rive::StateMachineBool>());
+
+    auto press = stateMachine->input("Press");
+    REQUIRE(press != nullptr);
+    REQUIRE(press->is<rive::StateMachineBool>());
+
+    auto layer = stateMachine->layer(0);
+    REQUIRE(layer->stateCount() == 6);
+
+    REQUIRE(layer->anyState() != nullptr);
+    REQUIRE(layer->entryState() != nullptr);
+    REQUIRE(layer->exitState() != nullptr);
+
+    int foundAnimationStates = 0;
+    for (int i = 0; i < layer->stateCount(); i++)
+    {
+        auto state = layer->state(i);
+        if (state->is<rive::AnimationState>())
+        {
+            foundAnimationStates++;
+            REQUIRE(state->as<rive::AnimationState>()->animation() != nullptr);
+        }
+    }
+
+    REQUIRE(foundAnimationStates == 3);
+
+    REQUIRE(layer->entryState()->transitionCount() == 1);
+    auto stateTo = layer->entryState()->transition(0)->stateTo();
+    REQUIRE(stateTo != nullptr);
+    REQUIRE(stateTo->is<rive::AnimationState>());
+    REQUIRE(stateTo->as<rive::AnimationState>()->animation() != nullptr);
+    REQUIRE(stateTo->as<rive::AnimationState>()->animation()->name() == "idle");
+
+    auto idleState = stateTo->as<rive::AnimationState>();
+    REQUIRE(idleState->transitionCount() == 2);
+    for (int i = 0; i < idleState->transitionCount(); i++)
+    {
+        auto transition = idleState->transition(i);
+        if (transition->stateTo()->as<rive::AnimationState>()->animation()->name() == "Roll_over")
+        {
+            // Check the condition
+            REQUIRE(transition->conditionCount() == 1);
+        }
+    }
+
+    auto abi = artboard->instance();
+    rive::StateMachineInstance smi(artboard->stateMachine("Button"), abi.get());
+
+    REQUIRE(smi.getBool("Hover")->name() == "Hover");
+    REQUIRE(smi.getBool("Press")->name() == "Press");
+    REQUIRE(smi.getBool("Hover") != nullptr);
+    REQUIRE(smi.getBool("Press") != nullptr);
+    REQUIRE(smi.stateChangedCount() == 0);
+    REQUIRE(smi.currentAnimationCount() == 0);
+}
+
+TEST_CASE("file with blend states loads correctly", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/blend_test.riv");
+
+    auto artboard = file->artboard();
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->animationCount() == 4);
+    REQUIRE(artboard->stateMachineCount() == 2);
+
+    auto stateMachine = artboard->stateMachine("blend");
+    REQUIRE(stateMachine != nullptr);
+
+    REQUIRE(stateMachine->layerCount() == 1);
+    auto layer = stateMachine->layer(0);
+    REQUIRE(layer->stateCount() == 5);
+
+    REQUIRE(layer->anyState() != nullptr);
+    REQUIRE(layer->entryState() != nullptr);
+    REQUIRE(layer->exitState() != nullptr);
+
+    REQUIRE(layer->state(1)->is<rive::BlendState1D>());
+    REQUIRE(layer->state(2)->is<rive::BlendState1D>());
+
+    auto blendStateA = layer->state(1)->as<rive::BlendState1D>();
+    auto blendStateB = layer->state(2)->as<rive::BlendState1D>();
+
+    REQUIRE(blendStateA->animationCount() == 3);
+    REQUIRE(blendStateB->animationCount() == 3);
+
+    auto animation = blendStateA->animation(0);
+    REQUIRE(animation->is<rive::BlendAnimation1D>());
+    auto animation1D = animation->as<rive::BlendAnimation1D>();
+    REQUIRE(animation1D->animation() != nullptr);
+    REQUIRE(animation1D->animation()->name() == "horizontal");
+    REQUIRE(animation1D->value() == 0.0f);
+
+    animation = blendStateA->animation(1);
+    REQUIRE(animation->is<rive::BlendAnimation1D>());
+    animation1D = animation->as<rive::BlendAnimation1D>();
+    REQUIRE(animation1D->animation() != nullptr);
+    REQUIRE(animation1D->animation()->name() == "vertical");
+    REQUIRE(animation1D->value() == 100.0f);
+
+    animation = blendStateA->animation(2);
+    REQUIRE(animation->is<rive::BlendAnimation1D>());
+    animation1D = animation->as<rive::BlendAnimation1D>();
+    REQUIRE(animation1D->animation() != nullptr);
+    REQUIRE(animation1D->animation()->name() == "rotate");
+    REQUIRE(animation1D->value() == 0.0f);
+
+    REQUIRE(blendStateA->transitionCount() == 1);
+    REQUIRE(blendStateA->transition(0)->is<rive::BlendStateTransition>());
+    REQUIRE(blendStateA->transition(0)->as<rive::BlendStateTransition>()->exitBlendAnimation() !=
+            nullptr);
+}
+
+TEST_CASE("animation state with no animation doesn't crash", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/multiple_state_machines.riv");
+
+    auto artboard = file->artboard();
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->animationCount() == 1);
+    REQUIRE(artboard->stateMachineCount() == 4);
+
+    auto stateMachine = artboard->stateMachine("two");
+    REQUIRE(stateMachine != nullptr);
+
+    REQUIRE(stateMachine->layerCount() == 1);
+    auto layer = stateMachine->layer(0);
+    REQUIRE(layer->stateCount() == 4);
+
+    REQUIRE(layer->anyState() != nullptr);
+    REQUIRE(layer->entryState() != nullptr);
+    REQUIRE(layer->exitState() != nullptr);
+
+    REQUIRE(layer->state(3)->is<rive::AnimationState>());
+
+    auto animationState = layer->state(3)->as<rive::AnimationState>();
+    REQUIRE(animationState->animation() == nullptr);
+
+    auto abi = artboard->instance();
+    rive::StateMachineInstance(stateMachine, abi.get()).advance(0.0f);
+}
+
+TEST_CASE("1D blend state keeps keepsGoing true even when animations themselves have stopped",
+          "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/oneshotblend.riv");
+
+    auto artboard = file->artboard();
+    auto stateMachine = artboard->stateMachine("State Machine 1");
+
+    auto abi = artboard->instance();
+    rive::StateMachineInstance* stateMachineInstance =
+        new rive::StateMachineInstance(stateMachine, abi.get());
+    stateMachineInstance->advance(0.0f);
+    REQUIRE(stateMachineInstance->needsAdvance() == true);
+
+    // after advancing into the 1DBlendState we still need to keep going.
+    stateMachineInstance->advance(0.5f);
+    REQUIRE(stateMachineInstance->needsAdvance() == true);
+
+    // even after advancing past the duration of the animations in the blend states
+    // we need to keep going.
+    stateMachineInstance->advance(1.0f);
+    REQUIRE(stateMachineInstance->needsAdvance() == true);
+
+    delete stateMachineInstance;
+}
+
+TEST_CASE("Transitions with duration completes the state correctly before changing states",
+          "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/state_machine_transition.riv");
+    auto black_color = 0xFF000000;
+    auto white_color = 0xFFFFFFFF;
+
+    auto artboard = file->artboard();
+    auto stateMachine = artboard->stateMachine("State-Machine-Test");
+
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->animationCount() == 3);
+    REQUIRE(artboard->stateMachineCount() == 1);
+
+    REQUIRE(stateMachine->layerCount() == 1);
+    auto layer = stateMachine->layer(0);
+    REQUIRE(layer->stateCount() == 6);
+
+    auto abi = artboard->instance();
+    REQUIRE(abi->children()[0]->is<rive::Shape>());
+    REQUIRE(abi->children()[0]->name() == "Star-Stroke");
+    auto shape = abi->children()[0]->as<rive::Shape>();
+    REQUIRE(shape->children()[1]->is<rive::Stroke>());
+    auto stroke = shape->children()[1]->as<rive::Stroke>();
+    REQUIRE(stroke->paint()->is<rive::SolidColor>());
+    auto solidColor = stroke->paint()->as<rive::SolidColor>();
+    // Before the transition, the color has to be full black
+    REQUIRE(solidColor->colorValue() == black_color);
+    rive::StateMachineInstance* stateMachineInstance =
+        new rive::StateMachineInstance(stateMachine, abi.get());
+    stateMachineInstance->advanceAndApply(0.1f);
+    abi->advance(0.1f);
+    REQUIRE(stateMachineInstance->currentAnimationByIndex(0)->name() == "State-2");
+    // After the transition has passed, the color has to be full white.
+    stateMachineInstance->advanceAndApply(2.0f);
+    abi->advance(2.0f);
+    REQUIRE(stateMachineInstance->currentAnimationByIndex(0)->name() == "State-3");
+    REQUIRE(solidColor->colorValue() == white_color);
+
+    delete stateMachineInstance;
+}
+
+TEST_CASE("Blend state animations with reset applied to them.", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/animation_reset_cases.riv");
+
+    auto artboard = file->artboard();
+    auto stateMachine = artboard->stateMachine("blend-states-state-machine");
+
+    // We empty all factory reset resources to start the test clean
+    rive::AnimationResetFactory::releaseResources();
+    REQUIRE(rive::AnimationResetFactory::resourcesCount() == 0);
+
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->animationCount() == 9);
+    REQUIRE(artboard->stateMachineCount() == 1);
+
+    auto abi = artboard->instance();
+    rive::StateMachineInstance* stateMachineInstance =
+        new rive::StateMachineInstance(stateMachine, abi.get());
+    stateMachineInstance->advanceAndApply(0.1f);
+    abi->advance(0.1f);
+
+    auto blendValueNumber = stateMachineInstance->getNumber("blend-value");
+    REQUIRE(blendValueNumber != nullptr);
+    REQUIRE(blendValueNumber->value() == 0);
+    blendValueNumber->value(50);
+    REQUIRE(blendValueNumber->value() == 50);
+    stateMachineInstance->advanceAndApply(0.1f);
+    abi->advance(0.1f);
+
+    auto rect1 = abi->children()[9]->as<rive::Shape>();
+    REQUIRE(rect1->name() == "rect1");
+
+    auto rect2 = abi->children()[7]->as<rive::Shape>();
+    REQUIRE(rect2->name() == "rect2");
+
+    auto triangle = abi->children()[5]->as<rive::Shape>();
+    REQUIRE(triangle->name() == "triangle");
+
+    // This blend rotates 2 * Pi. At 50% it should have rotated 1 * Pi
+    REQUIRE(rect1->rotation() == Approx(3.141592f));
+
+    auto state2Bool = stateMachineInstance->getBool("state-2");
+    REQUIRE(state2Bool != nullptr);
+    REQUIRE(state2Bool->value() == false);
+    state2Bool->value(true);
+    blendValueNumber->value(50);
+    stateMachineInstance->advanceAndApply(0.1f);
+    abi->advance(0.1f);
+    stateMachineInstance->advanceAndApply(0.1f);
+    REQUIRE(rive::AnimationResetFactory::resourcesCount() == 0);
+    REQUIRE(state2Bool->value() == true);
+    // X and Y interpolation in this state ranges are [75; 425] and [50; 450]
+    // so at 50% they should be at 250, 250
+    REQUIRE(rect1->x() == 250.0f);
+    REQUIRE(rect1->y() == 250.0f);
+    // rect2 rotation range is [0; -360]
+    // so at 50% it should be at -180
+    REQUIRE(rect2->rotation() == Approx(-3.141592f));
+
+    auto state3Bool = stateMachineInstance->getBool("state-3");
+    REQUIRE(state3Bool != nullptr);
+    REQUIRE(state3Bool->value() == false);
+    state3Bool->value(true);
+    REQUIRE(rive::AnimationResetFactory::resourcesCount() == 0);
+    stateMachineInstance->advanceAndApply(0.1f);
+    abi->advance(0.1f);
+    stateMachineInstance->advanceAndApply(0.1f);
+    blendValueNumber->value(100);
+    abi->advance(0.1f);
+    stateMachineInstance->advanceAndApply(0.1f);
+    REQUIRE(state3Bool->value() == true);
+    REQUIRE(triangle->y() == Approx(43.13281f));
+
+    REQUIRE(rive::AnimationResetFactory::resourcesCount() == 1);
+
+    auto state4Bool = stateMachineInstance->getBool("state-4");
+    REQUIRE(state4Bool != nullptr);
+    REQUIRE(state4Bool->value() == false);
+    state4Bool->value(true);
+    stateMachineInstance->advanceAndApply(0.1f);
+    abi->advance(0.1f);
+    REQUIRE(state4Bool->value() == true);
+    REQUIRE(rive::AnimationResetFactory::resourcesCount() == 2);
+
+    state4Bool->value(false);
+    stateMachineInstance->advanceAndApply(0.1f);
+    abi->advance(0.1f);
+    stateMachineInstance->advanceAndApply(0.1f);
+    REQUIRE(state4Bool->value() == false);
+    // After switching states mutiple times resources stay at 2 because they are released
+    // and retrieved from the pool
+    REQUIRE(rive::AnimationResetFactory::resourcesCount() == 2);
+    delete stateMachineInstance;
+}
+
+TEST_CASE("Transitions with reset applied to them.", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/animation_reset_cases.riv");
+
+    auto artboard = file->artboard("transitions");
+    auto stateMachine = artboard->stateMachine("transitions-state-machine");
+
+    // We empty all factory reset resources to start the test clean
+    rive::AnimationResetFactory::releaseResources();
+    REQUIRE(rive::AnimationResetFactory::resourcesCount() == 0);
+
+    REQUIRE(artboard != nullptr);
+    REQUIRE(artboard->animationCount() == 5);
+    REQUIRE(artboard->stateMachineCount() == 1);
+
+    auto abi = artboard->instance();
+    rive::StateMachineInstance* stateMachineInstance =
+        new rive::StateMachineInstance(stateMachine, abi.get());
+    stateMachineInstance->advanceAndApply(0.1f);
+    abi->advance(0.1f);
+
+    auto rect = abi->children()[7]->as<rive::Shape>();
+    REQUIRE(rect->name() == "rectangle");
+    REQUIRE(rect->x() == 50);
+
+    auto ellipse = abi->children()[5]->as<rive::Shape>();
+    REQUIRE(ellipse->name() == "ellipse");
+    REQUIRE(ellipse->x() == Approx(440.31241));
+
+    auto stateNumber = stateMachineInstance->getNumber("Number 1");
+    REQUIRE(stateNumber != nullptr);
+    REQUIRE(stateNumber->value() == 0);
+    stateNumber->value(1);
+    REQUIRE(stateNumber->value() == 1);
+    stateMachineInstance->advanceAndApply(0.1f);
+    abi->advance(0.1f);
+    stateMachineInstance->advanceAndApply(1.25f);
+
+    // rect transitions in 2.5 secs from x->50 to x->433
+    // so if the translation is linear, after 1.25s it should have
+    // traversed half the path
+    REQUIRE(rect->x() == 241.5f);
+
+    stateNumber->value(2);
+    REQUIRE(stateNumber->value() == 2);
+    stateMachineInstance->advanceAndApply(0.1f);
+    abi->advance(0.1f);
+    stateMachineInstance->advanceAndApply(1.25f);
+    // range is [440.31241; 42.69]
+    // half if the path is 42.69 + (440.21241 - 42.60) = 241.4962
+    REQUIRE(ellipse->x() == Approx(241.49992f));
+
+    // Transitions release their instance immediately so it's available for the next instance to use
+    REQUIRE(rive::AnimationResetFactory::resourcesCount() == 0);
+
+    stateNumber->value(3);
+    REQUIRE(stateNumber->value() == 3);
+    stateMachineInstance->advanceAndApply(0.1f);
+    abi->advance(0.1f);
+    stateMachineInstance->advanceAndApply(1.25f);
+
+    REQUIRE(rive::AnimationResetFactory::resourcesCount() == 0);
+
+    stateNumber->value(4);
+    REQUIRE(stateNumber->value() == 4);
+    stateMachineInstance->advanceAndApply(0.1f);
+    abi->advance(0.1f);
+    stateMachineInstance->advanceAndApply(1.25f);
+
+    // The last two states don't have a transition with duration set so the instance is released
+    // and available
+    REQUIRE(rive::AnimationResetFactory::resourcesCount() == 1);
+
+    delete stateMachineInstance;
+}
diff --git a/test/stroke_test.cpp b/test/stroke_test.cpp
new file mode 100644
index 0000000..392c836
--- /dev/null
+++ b/test/stroke_test.cpp
@@ -0,0 +1,22 @@
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/shapes/rectangle.hpp>
+#include <rive/shapes/shape.hpp>
+#include <rive/shapes/paint/stroke.hpp>
+#include <rive/shapes/paint/solid_color.hpp>
+#include <rive/shapes/paint/color.hpp>
+#include <utils/no_op_renderer.hpp>
+#include "rive_file_reader.hpp"
+#include <catch.hpp>
+#include <cstdio>
+
+TEST_CASE("stroke can be looked up at runtime", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/stroke_name_test.riv");
+
+    auto artboard = file->artboard();
+    REQUIRE(artboard->find<rive::Stroke>("white_stroke") != nullptr);
+    auto stroke = artboard->find<rive::Stroke>("white_stroke");
+    REQUIRE(stroke->paint()->is<rive::SolidColor>());
+    stroke->paint()->as<rive::SolidColor>()->colorValue(rive::colorARGB(255, 0, 255, 255));
+}
diff --git a/test/text_modifier_test.cpp b/test/text_modifier_test.cpp
new file mode 100644
index 0000000..99a01b9
--- /dev/null
+++ b/test/text_modifier_test.cpp
@@ -0,0 +1,27 @@
+#include "rive/text/text.hpp"
+#include "rive/text/text_style.hpp"
+#include "rive/text/text_value_run.hpp"
+#include "rive/text/text_modifier_group.hpp"
+#include "rive/text/text_modifier_range.hpp"
+#include "rive_file_reader.hpp"
+#include "rive_testing.hpp"
+#include <utils/no_op_renderer.hpp>
+
+TEST_CASE("text modifiers load correctly", "[text]")
+{
+    auto file = ReadRiveFile("../../test/assets/modifier_test.riv");
+    auto artboard = file->artboard();
+
+    auto textObjects = artboard->find<rive::Text>();
+    REQUIRE(textObjects.size() == 1);
+
+    auto text = textObjects[0];
+    REQUIRE(text->modifierGroups().size() == 1);
+
+    auto modifierGroup = text->modifierGroups()[0];
+    REQUIRE(modifierGroup->ranges().size() == 1);
+    REQUIRE(modifierGroup->modifiers().size() == 0);
+
+    auto range = modifierGroup->ranges()[0];
+    REQUIRE(range->interpolator() != nullptr);
+}
diff --git a/test/text_test.cpp b/test/text_test.cpp
new file mode 100644
index 0000000..36e8d6b
--- /dev/null
+++ b/test/text_test.cpp
@@ -0,0 +1,334 @@
+#include "rive/text/text.hpp"
+#include "rive/text/text_style.hpp"
+#include "rive/text/text_value_run.hpp"
+#include "rive_file_reader.hpp"
+#include "rive_testing.hpp"
+#include "rive/text/glyph_lookup.hpp"
+#include "rive/text/text_modifier_range.hpp"
+#include "utils/no_op_renderer.hpp"
+#include "rive/text/utf.hpp"
+#include "rive/text/text_modifier_group.hpp"
+
+TEST_CASE("file with text loads correctly", "[text]")
+{
+    auto file = ReadRiveFile("../../test/assets/new_text.riv");
+    auto artboard = file->artboard();
+
+    auto textObjects = artboard->find<rive::Text>();
+    REQUIRE(textObjects.size() == 5);
+
+    auto styleObjects = artboard->find<rive::TextStyle>();
+    REQUIRE(styleObjects.size() == 13);
+
+    auto runObjects = artboard->find<rive::TextValueRun>();
+    REQUIRE(runObjects.size() == 22);
+
+    artboard->advance(0.0f);
+    rive::NoOpRenderer renderer;
+    artboard->draw(&renderer);
+}
+
+TEST_CASE("can query for all text runs", "[text]")
+{
+    auto file = ReadRiveFile("../../test/assets/new_text.riv");
+    auto artboard = file->artboard();
+
+    auto textRunCount = artboard->count<rive::TextValueRun>();
+    REQUIRE(textRunCount == 22);
+}
+
+TEST_CASE("can query for a text run at a given index", "[text]")
+{
+    auto file = ReadRiveFile("../../test/assets/hello_world.riv");
+    auto artboard = file->artboard();
+
+    auto textRun = artboard->objectAt<rive::TextValueRun>(0);
+    REQUIRE(textRun->text() == "Hello World!");
+}
+
+TEST_CASE("simple text loads", "[text]")
+{
+    auto file = ReadRiveFile("../../test/assets/hello_world.riv");
+    auto artboard = file->artboard();
+
+    auto textObjects = artboard->find<rive::Text>();
+    REQUIRE(textObjects.size() == 1);
+
+    auto styleObjects = artboard->find<rive::TextStyle>();
+    REQUIRE(styleObjects.size() == 1);
+
+    auto runObjects = artboard->find<rive::TextValueRun>();
+    REQUIRE(runObjects.size() == 1);
+    REQUIRE(runObjects[0]->text() == "Hello World!");
+
+    rive::NoOpRenderer renderer;
+
+    artboard->advance(0.0f);
+    artboard->draw(&renderer);
+    {
+        REQUIRE(textObjects[0]->shape().size() == 1);
+        const rive::Paragraph& paragraph = textObjects[0]->shape()[0];
+        REQUIRE(paragraph.runs.size() == 1);
+        REQUIRE(paragraph.runs[0].glyphs.size() == 12);
+    }
+
+    // Changing to "Just Hello" works.
+    runObjects[0]->text("Just Hello");
+    artboard->advance(0.0f);
+    artboard->draw(&renderer);
+    {
+        REQUIRE(textObjects[0]->shape().size() == 1);
+        const rive::Paragraph& paragraph = textObjects[0]->shape()[0];
+        REQUIRE(paragraph.runs.size() == 1);
+        REQUIRE(paragraph.runs[0].glyphs.size() == 10);
+    }
+
+    // Changing to an empty space " " works.
+    runObjects[0]->text(" ");
+    artboard->advance(0.0f);
+    artboard->draw(&renderer);
+    {
+        REQUIRE(textObjects[0]->shape().size() == 1);
+        const rive::Paragraph& paragraph = textObjects[0]->shape()[0];
+        REQUIRE(paragraph.runs.size() == 1);
+        REQUIRE(paragraph.runs[0].glyphs.size() == 1);
+    }
+
+    // Changing to completely empty works.
+    runObjects[0]->text("");
+    artboard->advance(0.0f);
+    artboard->draw(&renderer);
+    {
+        REQUIRE(textObjects[0]->shape().size() == 0);
+        REQUIRE(textObjects[0]->localBounds().width() == 0.0f);
+        REQUIRE(textObjects[0]->localBounds().height() == 0.0f);
+    }
+}
+
+TEST_CASE("ellipsis is shown", "[text]")
+{
+    auto file = ReadRiveFile("../../test/assets/ellipsis.riv");
+    auto artboard = file->artboard();
+
+    auto textObjects = artboard->find<rive::Text>();
+    REQUIRE(textObjects.size() == 1);
+
+    auto styleObjects = artboard->find<rive::TextStyle>();
+    REQUIRE(styleObjects.size() == 1);
+
+    auto runObjects = artboard->find<rive::TextValueRun>();
+    REQUIRE(runObjects.size() == 1);
+
+    artboard->advance(0.0f);
+    auto text = textObjects[0];
+    auto lines = text->orderedLines();
+    REQUIRE(lines.size() == 1);
+
+    auto orderedLine = lines[0];
+    int glyphCount = 0;
+    std::vector<rive::GlyphID> glyphIds;
+    // for (auto itr = orderedLine.begin(); itr != orderedLine.end(); ++itr)
+    for (auto glyphItr : orderedLine)
+    {
+        auto run = std::get<0>(glyphItr);
+        size_t glyphIndex = std::get<1>(glyphItr);
+        glyphIds.push_back(run->glyphs[glyphIndex]);
+        glyphCount++;
+    }
+    // Expect 10 glyphs 'one two...'
+    REQUIRE(glyphCount == 10);
+    // Third to last glyph should be the "." for Inter.
+    REQUIRE(glyphIds[7] == 1405);
+    // Last three glyphs should be the same '.'
+    REQUIRE(glyphIds[7] == glyphIds[8]);
+    REQUIRE(glyphIds[8] == glyphIds[9]);
+
+    // now set overflow to visible.
+    text->overflow(rive::TextOverflow::visible);
+    artboard->advance(0.0f);
+    lines = text->orderedLines();
+
+    // 2 lines after we set overflow to visible and advance which updates the
+    // ordered lines.
+    REQUIRE(lines.size() == 2);
+
+    orderedLine = lines[0];
+    glyphCount = 0;
+    for (auto itr = orderedLine.begin(); itr != orderedLine.end(); ++itr)
+    {
+        glyphCount++;
+    }
+    // Expect 7 glyphs 'one two' as now 'three' is on line 2.
+    REQUIRE(glyphCount == 7);
+
+    rive::NoOpRenderer renderer;
+    artboard->draw(&renderer);
+
+    rive::GlyphLookup lookup;
+    lookup.compute(text->unichars(), text->shape());
+    REQUIRE(lookup.count(0) == 1);
+
+    runObjects[0]->text("a -> b");
+    artboard->advance(0.0f);
+    lookup.compute(text->unichars(), text->shape());
+    REQUIRE(lookup.count(0) == 1); // a
+    REQUIRE(lookup.count(1) == 1); // space
+    REQUIRE(lookup.count(2) == 2); // - ligates > to ->
+    REQUIRE(lookup.count(4) == 1); // space
+    REQUIRE(lookup.count(5) == 1); // b
+}
+
+static std::vector<rive::Unichar> toUnicode(const char text[])
+{
+    std::vector<rive::Unichar> codePoints;
+    const uint8_t* ptr = (const uint8_t*)text;
+    while (*ptr)
+    {
+        codePoints.push_back(rive::UTF::NextUTF8(&ptr));
+    }
+    return codePoints;
+}
+
+TEST_CASE("range mapper maps words", "[text]")
+{
+    auto codePoints = toUnicode("one two three four");
+    rive::RangeMapper rangeMapper;
+    rangeMapper.fromWords(codePoints, 0, (uint32_t)codePoints.size());
+    REQUIRE(rangeMapper.unitCount() == 4);
+}
+
+TEST_CASE("run modifier ranges select runs", "[text]")
+{
+    auto file = ReadRiveFile("../../test/assets/modifier_to_run.riv");
+    auto artboard = file->artboard();
+
+    artboard->advance(0.0f);
+    rive::NoOpRenderer renderer;
+    artboard->draw(&renderer);
+
+    {
+        auto characterSelectedText = artboard->find<rive::Text>("Characters");
+        REQUIRE(characterSelectedText != nullptr);
+        REQUIRE(characterSelectedText->haveModifiers());
+        REQUIRE(characterSelectedText->modifierGroups().size() == 2);
+        auto firstModifierGroup = characterSelectedText->modifierGroups()[0];
+        REQUIRE(firstModifierGroup->ranges().size() == 1);
+        auto firstRange = firstModifierGroup->ranges()[0];
+        REQUIRE(firstRange->run() != nullptr);
+        for (int i = 0; i < 4; i++)
+        {
+            REQUIRE(firstModifierGroup->coverage(i) == 0.0f);
+        }
+        // Run from 4-9 got selected.
+        REQUIRE(firstRange->run()->offset() == 4);
+        REQUIRE(firstRange->run()->length() == 5);
+        for (int i = 4; i < 9; i++)
+        {
+            REQUIRE(firstModifierGroup->coverage(i) != 0.0f);
+        }
+        for (int i = 9; i < 26; i++)
+        {
+            REQUIRE(firstModifierGroup->coverage(i) == 0.0f);
+        }
+    }
+    {
+        auto wordSelectedText = artboard->find<rive::Text>("Words");
+        REQUIRE(wordSelectedText != nullptr);
+        REQUIRE(wordSelectedText->haveModifiers());
+        REQUIRE(wordSelectedText->modifierGroups().size() == 1);
+        auto firstModifierGroup = wordSelectedText->modifierGroups()[0];
+        REQUIRE(firstModifierGroup->ranges().size() == 1);
+        auto firstRange = firstModifierGroup->ranges()[0];
+        REQUIRE(firstRange->run() != nullptr);
+        for (int i = 0; i < 4; i++)
+        {
+            REQUIRE(firstModifierGroup->coverage(i) == 0.0f);
+        }
+        // Run from 4-34 got selected.
+        REQUIRE(firstRange->run()->offset() == 4);
+        auto text = firstRange->run()->text();
+        REQUIRE(text.size() == 34);
+        for (int i = 4; i < 39; i++)
+        {
+            if (rive::isWhiteSpace(text[i - 4]))
+            {
+                REQUIRE(firstModifierGroup->coverage(i) == 0.0f);
+            }
+            else
+            {
+                REQUIRE(firstModifierGroup->coverage(i) != 0.0f);
+            }
+        }
+        for (int i = 39; i < 50; i++)
+        {
+            REQUIRE(firstModifierGroup->coverage(i) == 0.0f);
+        }
+    }
+}
+
+TEST_CASE("run modifier ranges select runs with varying text size", "[text]")
+{
+    auto file = ReadRiveFile("../../test/assets/test_modifier_run.riv");
+    auto artboard = file->artboard();
+
+    artboard->advance(0.0f);
+    rive::NoOpRenderer renderer;
+    artboard->draw(&renderer);
+
+    {
+        /*
+        Full text is:
+        text for first run[UN]line separator[UN]text for second run
+        [UN] is the united nations flag
+        the first run is 18 characters long
+        the second run is 16 characters long
+        the second run is 20 characters long
+        */
+        auto characterSelectedText = artboard->find<rive::Text>("MultiRunText");
+        REQUIRE(characterSelectedText != nullptr);
+        REQUIRE(characterSelectedText->haveModifiers());
+        REQUIRE(characterSelectedText->modifierGroups().size() == 1);
+        auto firstModifierGroup = characterSelectedText->modifierGroups()[0];
+        REQUIRE(firstModifierGroup->ranges().size() == 1);
+        auto firstRange = firstModifierGroup->ranges()[0];
+        REQUIRE(firstRange->run() != nullptr);
+        for (int i = 0; i < 18; i++)
+        {
+            REQUIRE(firstModifierGroup->coverage(i) == 0.0f);
+        }
+        // // Run from 18-33 got selected.
+        REQUIRE(firstRange->run()->offset() == 18);
+        REQUIRE(firstRange->run()->length() == 16);
+        // We confirm that the size and the length of the text are different and they're
+        // both correct
+        REQUIRE(firstRange->run()->text().size() == 22);
+        for (int i = 18; i < 34; i++)
+        {
+            REQUIRE(firstModifierGroup->coverage(i) != 0.0f);
+        }
+        for (int i = 34; i < 54; i++)
+        {
+            REQUIRE(firstModifierGroup->coverage(i) == 0.0f);
+        }
+    }
+}
+
+TEST_CASE("double new line type works", "[text]")
+{
+    auto file = ReadRiveFile("../../test/assets/double_line.riv");
+    auto artboard = file->artboard();
+
+    auto textObjects = artboard->find<rive::Text>();
+    REQUIRE(textObjects.size() == 1);
+
+    auto styleObjects = artboard->find<rive::TextStyle>();
+    REQUIRE(styleObjects.size() == 1);
+
+    auto runObjects = artboard->find<rive::TextValueRun>();
+    REQUIRE(runObjects.size() == 9);
+
+    artboard->advance(0.0f);
+    auto text = textObjects[0];
+    auto lines = text->orderedLines();
+    REQUIRE(lines.size() == 3);
+}
diff --git a/test/transform_constraint_test.cpp b/test/transform_constraint_test.cpp
new file mode 100644
index 0000000..315a3c4
--- /dev/null
+++ b/test/transform_constraint_test.cpp
@@ -0,0 +1,27 @@
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/bones/bone.hpp>
+#include <rive/shapes/shape.hpp>
+#include <utils/no_op_renderer.hpp>
+#include "rive_file_reader.hpp"
+#include "rive_testing.hpp"
+#include <cstdio>
+
+TEST_CASE("transform constraint updates world transform", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/transform_constraint.riv");
+
+    auto artboard = file->artboard();
+
+    REQUIRE(artboard->find<rive::TransformComponent>("Target") != nullptr);
+    auto target = artboard->find<rive::TransformComponent>("Target");
+
+    REQUIRE(artboard->find<rive::TransformComponent>("Rectangle") != nullptr);
+    auto rectangle = artboard->find<rive::TransformComponent>("Rectangle");
+
+    artboard->advance(0.0f);
+
+    // Expect the transform constraint to have placed the shape in the same
+    // exact world transform as the target.
+    REQUIRE(aboutEqual(target->worldTransform(), rectangle->worldTransform()));
+}
diff --git a/test/translation_constraint_test.cpp b/test/translation_constraint_test.cpp
new file mode 100644
index 0000000..b38af62
--- /dev/null
+++ b/test/translation_constraint_test.cpp
@@ -0,0 +1,30 @@
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/bones/bone.hpp>
+#include <rive/shapes/shape.hpp>
+#include <rive/math/transform_components.hpp>
+#include <utils/no_op_renderer.hpp>
+#include "rive_file_reader.hpp"
+#include "rive_testing.hpp"
+#include <cstdio>
+
+TEST_CASE("translation constraint updates world transform", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/translation_constraint.riv");
+
+    auto artboard = file->artboard();
+
+    REQUIRE(artboard->find<rive::TransformComponent>("target") != nullptr);
+    auto target = artboard->find<rive::TransformComponent>("target");
+
+    REQUIRE(artboard->find<rive::TransformComponent>("rect") != nullptr);
+    auto rectangle = artboard->find<rive::TransformComponent>("rect");
+
+    artboard->advance(0.0f);
+
+    auto targetComponents = target->worldTransform().decompose();
+    auto rectComponents = rectangle->worldTransform().decompose();
+
+    REQUIRE(targetComponents.x() == rectComponents.x());
+    REQUIRE(targetComponents.y() == rectComponents.y());
+}
diff --git a/test/trim_test.cpp b/test/trim_test.cpp
new file mode 100644
index 0000000..1e59d0e
--- /dev/null
+++ b/test/trim_test.cpp
@@ -0,0 +1,32 @@
+#include <rive/file.hpp>
+#include <rive/node.hpp>
+#include <rive/shapes/rectangle.hpp>
+#include <rive/shapes/shape.hpp>
+#include <rive/shapes/paint/stroke.hpp>
+#include <rive/shapes/paint/solid_color.hpp>
+#include <rive/shapes/paint/color.hpp>
+#include <utils/no_op_renderer.hpp>
+#include "rive_file_reader.hpp"
+#include <catch.hpp>
+#include <cstdio>
+
+TEST_CASE("A 0 scale path will trim with no crash", "[file]")
+{
+    auto file = ReadRiveFile("../../test/assets/trim.riv");
+
+    auto artboard = file->artboard();
+    auto node = artboard->find<rive::Node>("I");
+    REQUIRE(node != nullptr);
+    REQUIRE(node->scaleX() != 0);
+    REQUIRE(node->scaleY() != 0);
+    artboard->advance(0.0f);
+
+    rive::NoOpRenderer renderer;
+    artboard->draw(&renderer);
+
+    // Now set scale to 0 and make sure it doesn't crash.
+    node->scaleX(0.0f);
+    node->scaleY(0.0f);
+    artboard->advance(0.0f);
+    artboard->draw(&renderer);
+}
diff --git a/test/wangs_formula_test.cpp b/test/wangs_formula_test.cpp
new file mode 100644
index 0000000..a24cd71
--- /dev/null
+++ b/test/wangs_formula_test.cpp
@@ -0,0 +1,625 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ *
+ * Initial import from skia:tests/WangsFormulaTest.cpp
+ *
+ * Copyright 2023 Rive
+ */
+
+#include "rive/math/wangs_formula.hpp"
+#include <catch.hpp>
+#include <functional>
+
+namespace rive
+{
+constexpr static float kPrecision = 4;
+constexpr static float kEpsilon = 1.f / (1 << 12);
+
+static bool fuzzy_equal(float a, float b, float tolerance = kEpsilon)
+{
+    assert(tolerance >= 0);
+    return fabsf(a - b) <= tolerance;
+}
+
+const Vec2D kSerp[4] = {{285.625f, 499.687f},
+                        {411.625f, 808.188f},
+                        {1064.62f, 135.688f},
+                        {1042.63f, 585.187f}};
+
+const Vec2D kLoop[4] = {{635.625f, 614.687f},
+                        {171.625f, 236.188f},
+                        {1064.62f, 135.688f},
+                        {516.625f, 570.187f}};
+
+const Vec2D kQuad[4] = {{460.625f, 557.187f}, {707.121f, 209.688f}, {779.628f, 577.687f}};
+
+static void map_pts(const Mat2D& m, Vec2D out[], const Vec2D in[], int n)
+{
+    for (int i = 0; i < n; ++i)
+    {
+        out[i] = m * in[i];
+    }
+}
+
+static float wangs_formula_quadratic_reference_impl(float precision, const Vec2D p[3])
+{
+    float k = (2 * 1) / 8.f * precision;
+    return sqrtf(k * (p[0] - p[1] * 2 + p[2]).length());
+}
+
+static float wangs_formula_cubic_reference_impl(float precision, const Vec2D p[4])
+{
+    float k = (3 * 2) / 8.f * precision;
+    return sqrtf(k *
+                 std::max((p[0] - p[1] * 2 + p[2]).length(), (p[1] - p[2] * 2 + p[3]).length()));
+}
+
+static void chop_quad_at(const Vec2D src[3], Vec2D dst[5], float t)
+{
+    assert(t > 0 && t < 1);
+
+    float2 p0 = simd::load2f(&src[0].x);
+    float2 p1 = simd::load2f(&src[1].x);
+    float2 p2 = simd::load2f(&src[2].x);
+    float2 tt(t);
+
+    float2 p01 = simd::mix(p0, p1, tt);
+    float2 p12 = simd::mix(p1, p2, tt);
+
+    simd::store(&dst[0].x, p0);
+    simd::store(&dst[1].x, p01);
+    simd::store(&dst[2].x, simd::mix(p01, p12, tt));
+    simd::store(&dst[3].x, p12);
+    simd::store(&dst[4].x, p2);
+}
+
+static Vec2D eval_quad_at(const Vec2D src[3], float t)
+{
+    assert(t > 0 && t < 1);
+
+    float2 p0 = simd::load2f(&src[0].x);
+    float2 p1 = simd::load2f(&src[1].x);
+    float2 p2 = simd::load2f(&src[2].x);
+    float2 tt(t);
+
+    float2 p01 = simd::mix(p0, p1, tt);
+    float2 p12 = simd::mix(p1, p2, tt);
+    float2 p012 = simd::mix(p01, p12, tt);
+
+    Vec2D vec;
+    simd::store(&vec.x, p012);
+    return vec;
+}
+
+// Returns number of segments for linearized quadratic rational. This is an analogue
+// to Wang's formula, taken from:
+//
+//   J. Zheng, T. Sederberg. "Estimating Tessellation Parameter Intervals for
+//   Rational Curves and Surfaces." ACM Transactions on Graphics 19(1). 2000.
+// See Thm 3, Corollary 1.
+//
+// Input points should be in projected space.
+static float wangs_formula_conic_reference_impl(float precision, const Vec2D P[3], const float w)
+{
+    // Compute center of bounding box in projected space
+    float min_x = P[0].x, max_x = min_x, min_y = P[0].y, max_y = min_y;
+    for (int i = 1; i < 3; i++)
+    {
+        min_x = std::min(min_x, P[i].x);
+        max_x = std::max(max_x, P[i].x);
+        min_y = std::min(min_y, P[i].y);
+        max_y = std::max(max_y, P[i].y);
+    }
+    const Vec2D C = Vec2D(0.5f * (min_x + max_x), 0.5f * (min_y + max_y));
+
+    // Translate control points and compute max length
+    Vec2D tP[3] = {P[0] - C, P[1] - C, P[2] - C};
+    float max_len = 0;
+    for (int i = 0; i < 3; i++)
+    {
+        max_len = std::max(max_len, tP[i].length());
+    }
+    assert(max_len > 0);
+
+    // Compute delta = parametric step size of linearization
+    const float eps = 1 / precision;
+    const float r_minus_eps = std::max(0.f, max_len - eps);
+    const float min_w = std::min(w, 1.f);
+    const float numer = 4 * min_w * eps;
+    const float denom =
+        (tP[2] - tP[1] * 2 * w + tP[0]).length() + r_minus_eps * std::abs(1 - 2 * w + 1);
+    const float delta = sqrtf(numer / denom);
+
+    // Return corresponding num segments in the interval [tmin,tmax]
+    constexpr float tmin = 0, tmax = 1;
+    assert(delta > 0);
+    return (tmax - tmin) / delta;
+}
+
+static float frand() { return rand() / static_cast<float>(RAND_MAX); }
+
+static float frand_range(float min, float max) { return min + frand() * (max - min); }
+
+static void for_random_matrices(std::function<void(const Mat2D&)> f)
+{
+    srand(0);
+
+    Mat2D m{};
+    f(m);
+
+    for (int i = -10; i <= 30; ++i)
+    {
+        for (int j = -10; j <= 30; ++j)
+        {
+            m[0] = std::ldexp(1 + frand(), i);
+            m[1] = 0;
+            m[2] = 0;
+            m[3] = std::ldexp(1 + frand(), j);
+            f(m);
+
+            m[0] = std::ldexp(1 + frand(), i);
+            m[1] = std::ldexp(1 + frand(), (j + i) / 2);
+            m[2] = std::ldexp(1 + frand(), (j + i) / 2);
+            m[3] = std::ldexp(1 + frand(), j);
+            f(m);
+        }
+    }
+}
+
+static void for_random_beziers(int numPoints,
+                               std::function<void(const Vec2D[])> f,
+                               int maxExponent = 30)
+{
+    srand(0);
+
+    assert(numPoints <= 4);
+    Vec2D pts[4];
+    for (int i = -10; i <= maxExponent; ++i)
+    {
+        for (int j = 0; j < numPoints; ++j)
+        {
+            pts[j] = {std::ldexp(1 + frand(), i), std::ldexp(1 + frand(), i)};
+        }
+        f(pts);
+    }
+}
+
+// Ensure the optimized "*_log2" versions return the same value as ceil(std::log2(f)).
+TEST_CASE("wangs_formula_log2", "[wangs_formula]")
+{
+    // Constructs a cubic such that the 'length' term in wang's formula == term.
+    //
+    //     f = sqrt(k * length(max(abs(p0 - p1*2 + p2),
+    //                             abs(p1 - p2*2 + p3))));
+    auto setupCubicLengthTerm = [](int seed, Vec2D pts[], float term) {
+        memset(pts, 0, sizeof(Vec2D) * 4);
+
+        Vec2D term2d = (seed & 1) ? Vec2D(term, 0) : Vec2D(.5f, std::sqrt(3) / 2) * term;
+        seed >>= 1;
+
+        if (seed & 1)
+        {
+            term2d.x = -term2d.x;
+        }
+        seed >>= 1;
+
+        if (seed & 1)
+        {
+            std::swap(term2d.x, term2d.y);
+        }
+        seed >>= 1;
+
+        switch (seed % 4)
+        {
+            case 0:
+                pts[0] = term2d;
+                pts[3] = term2d * .75f;
+                return;
+            case 1:
+                pts[1] = term2d * -.5f;
+                return;
+            case 2:
+                pts[1] = term2d * -.5f;
+                return;
+            case 3:
+                pts[3] = term2d;
+                pts[0] = term2d * .75f;
+                return;
+        }
+    };
+
+    // Constructs a quadratic such that the 'length' term in wang's formula == term.
+    //
+    //     f = sqrt(k * length(p0 - p1*2 + p2));
+    auto setupQuadraticLengthTerm = [](int seed, Vec2D pts[], float term) {
+        memset(pts, 0, sizeof(Vec2D) * 3);
+
+        Vec2D term2d = (seed & 1) ? Vec2D(term, 0) : Vec2D(.5f, std::sqrt(3) / 2) * term;
+        seed >>= 1;
+
+        if (seed & 1)
+        {
+            term2d.x = -term2d.x;
+        }
+        seed >>= 1;
+
+        if (seed & 1)
+        {
+            std::swap(term2d.x, term2d.y);
+        }
+        seed >>= 1;
+
+        switch (seed % 3)
+        {
+            case 0:
+                pts[0] = term2d;
+                return;
+            case 1:
+                pts[1] = term2d * -.5f;
+                return;
+            case 2:
+                pts[2] = term2d;
+                return;
+        }
+    };
+
+    // wangs_formula_cubic and wangs_formula_quadratic both use rsqrt instead of sqrt for speed.
+    // Linearization is all approximate anyway, so as long as we are within ~1/2 tessellation
+    // segment of the reference value we are good enough.
+    constexpr static float kTessellationTolerance = 1 / 128.f;
+
+    for (int level = 0; level < 30; ++level)
+    {
+        float epsilon = std::ldexp(kEpsilon, level * 2);
+        Vec2D pts[4];
+
+        {
+            // Test cubic boundaries.
+            //     f = sqrt(k * length(max(abs(p0 - p1*2 + p2),
+            //                             abs(p1 - p2*2 + p3))));
+            constexpr static float k = (3 * 2) / (8 * (1.f / kPrecision));
+            float x = std::ldexp(1, level * 2) / k;
+            setupCubicLengthTerm(level << 1, pts, x - epsilon);
+            float referenceValue = wangs_formula_cubic_reference_impl(kPrecision, pts);
+            REQUIRE(std::ceil(std::log2(referenceValue)) == level);
+            float c = wangs_formula::cubic(pts, kPrecision);
+            REQUIRE(fuzzy_equal(c / referenceValue, 1, kTessellationTolerance));
+            REQUIRE(wangs_formula::cubic_log2(pts, kPrecision) == level);
+            setupCubicLengthTerm(level << 1, pts, x + epsilon);
+            referenceValue = wangs_formula_cubic_reference_impl(kPrecision, pts);
+            REQUIRE(std::ceil(std::log2(referenceValue)) == level + 1);
+            c = wangs_formula::cubic(pts, kPrecision);
+            REQUIRE(fuzzy_equal(c / referenceValue, 1, kTessellationTolerance));
+            REQUIRE(wangs_formula::cubic_log2(pts, kPrecision) == level + 1);
+        }
+
+        {
+            // Test quadratic boundaries.
+            //     f = std::sqrt(k * Length(p0 - p1*2 + p2));
+            constexpr static float k = 2 / (8 * (1.f / kPrecision));
+            float x = std::ldexp(1, level * 2) / k;
+            setupQuadraticLengthTerm(level << 1, pts, x - epsilon);
+            float referenceValue = wangs_formula_quadratic_reference_impl(kPrecision, pts);
+            REQUIRE(std::ceil(std::log2(referenceValue)) == level);
+            float q = wangs_formula::quadratic(pts, kPrecision);
+            REQUIRE(fuzzy_equal(q / referenceValue, 1, kTessellationTolerance));
+            REQUIRE(wangs_formula::quadratic_log2(pts, kPrecision) == level);
+            setupQuadraticLengthTerm(level << 1, pts, x + epsilon);
+            referenceValue = wangs_formula_quadratic_reference_impl(kPrecision, pts);
+            REQUIRE(std::ceil(std::log2(referenceValue)) == level + 1);
+            q = wangs_formula::quadratic(pts, kPrecision);
+            REQUIRE(fuzzy_equal(q / referenceValue, 1, kTessellationTolerance));
+            REQUIRE(wangs_formula::quadratic_log2(pts, kPrecision) == level + 1);
+        }
+    }
+
+    auto check_cubic_log2 = [&](const Vec2D* pts) {
+        float f = std::max(1.f, wangs_formula_cubic_reference_impl(kPrecision, pts));
+        int f_log2 = wangs_formula::cubic_log2(pts, kPrecision);
+        REQUIRE(ceilf(std::log2(f)) == f_log2);
+        float c = std::max(1.f, wangs_formula::cubic(pts, kPrecision));
+        REQUIRE(fuzzy_equal(c / f, 1, kTessellationTolerance));
+    };
+
+    auto check_quadratic_log2 = [&](const Vec2D* pts) {
+        float f = std::max(1.f, wangs_formula_quadratic_reference_impl(kPrecision, pts));
+        int f_log2 = wangs_formula::quadratic_log2(pts, kPrecision);
+        REQUIRE(ceilf(std::log2(f)) == f_log2);
+        float q = std::max(1.f, wangs_formula::quadratic(pts, kPrecision));
+        REQUIRE(fuzzy_equal(q / f, 1, kTessellationTolerance));
+    };
+
+    for_random_matrices([&](const Mat2D& m) {
+        Vec2D pts[4 + 999];
+        map_pts(m, pts, kSerp, 4);
+        check_cubic_log2(pts);
+
+        map_pts(m, pts, kLoop, 4);
+        check_cubic_log2(pts);
+
+        map_pts(m, pts, kQuad, 3);
+        check_quadratic_log2(pts);
+    });
+
+    for_random_beziers(4, [&](const Vec2D pts[]) { check_cubic_log2(pts); });
+
+    for_random_beziers(3, [&](const Vec2D pts[]) { check_quadratic_log2(pts); });
+}
+
+static void check_cubic_log2_with_transform(const Vec2D* pts, const Mat2D& m)
+{
+    Vec2D ptsXformed[4];
+    map_pts(m, ptsXformed, pts, 4);
+    int expected = wangs_formula::cubic_log2(ptsXformed, kPrecision);
+    int actual = wangs_formula::cubic_log2(pts, kPrecision, wangs_formula::VectorXform(m));
+    REQUIRE(actual == expected);
+};
+
+static void check_quadratic_log2_with_transform(const Vec2D* pts, const Mat2D& m)
+{
+    Vec2D ptsXformed[3];
+    map_pts(m, ptsXformed, pts, 3);
+    int expected = wangs_formula::quadratic_log2(ptsXformed, kPrecision);
+    int actual = wangs_formula::quadratic_log2(pts, kPrecision, wangs_formula::VectorXform(m));
+    REQUIRE(actual == expected);
+};
+
+// Ensure using transformations gives the same result as pre-transforming all points.
+TEST_CASE("wangs_formula_vectorXforms", "[wangs_formula]")
+{
+    for_random_matrices([&](const Mat2D& m) {
+        check_cubic_log2_with_transform(kSerp, m);
+        check_cubic_log2_with_transform(kLoop, m);
+        check_quadratic_log2_with_transform(kQuad, m);
+
+        for_random_beziers(4, [&](const Vec2D pts[]) { check_cubic_log2_with_transform(pts, m); });
+
+        for_random_beziers(3,
+                           [&](const Vec2D pts[]) { check_quadratic_log2_with_transform(pts, m); });
+    });
+}
+
+TEST_CASE("wangs_formula_worst_case_cubic", "[wangs_formula]")
+{
+    {
+        Vec2D worstP[] = {{0, 0}, {100, 100}, {0, 0}, {0, 0}};
+        REQUIRE(wangs_formula::worst_case_cubic(100, 100, kPrecision) ==
+                wangs_formula_cubic_reference_impl(kPrecision, worstP));
+        REQUIRE(wangs_formula::worst_case_cubic_log2(100, 100, kPrecision) ==
+                wangs_formula::cubic_log2(worstP, kPrecision));
+    }
+    {
+        Vec2D worstP[] = {{100, 100}, {100, 100}, {200, 200}, {100, 100}};
+        REQUIRE(wangs_formula::worst_case_cubic(100, 100, kPrecision) ==
+                wangs_formula_cubic_reference_impl(kPrecision, worstP));
+        REQUIRE(wangs_formula::worst_case_cubic_log2(100, 100, kPrecision) ==
+                wangs_formula::cubic_log2(worstP, kPrecision));
+    }
+    auto check_worst_case_cubic = [&](const Vec2D* pts) {
+        float2 min = simd::load2f(&pts[0].x), max = simd::load2f(&pts[0].x);
+        for (int i = 1; i < 4; ++i)
+        {
+            min = simd::min(min, simd::load2f(&pts[i].x));
+            max = simd::max(max, simd::load2f(&pts[i].x));
+        }
+        float2 size = max - min;
+        float worst = wangs_formula::worst_case_cubic(size.x, size.y, kPrecision);
+        int worst_log2 = wangs_formula::worst_case_cubic_log2(size.x, size.y, kPrecision);
+        float actual = wangs_formula_cubic_reference_impl(kPrecision, pts);
+        REQUIRE(worst >= actual);
+        REQUIRE(std::ceil(std::log2(std::max(1.f, worst))) == worst_log2);
+    };
+    for (int i = 0; i < 100; ++i)
+    {
+        for_random_beziers(4, [&](const Vec2D pts[]) { check_worst_case_cubic(pts); });
+    }
+    // Make sure overflow saturates at infinity (not NaN).
+    constexpr static float inf = std::numeric_limits<float>::infinity();
+    REQUIRE(wangs_formula::worst_case_cubic_pow4(inf, inf, kPrecision) == inf);
+    REQUIRE(wangs_formula::worst_case_cubic(inf, inf, kPrecision) == inf);
+}
+
+// Ensure Wang's formula for quads produces max error within tolerance.
+TEST_CASE("wangs_formula_quad_within_tol", "[wangs_formula]")
+{
+    // Wang's formula and the quad math starts to lose precision with very large
+    // coordinate values, so limit the magnitude a bit to prevent test failures
+    // due to loss of precision.
+    constexpr int maxExponent = 15;
+    for_random_beziers(
+        3,
+        [](const Vec2D pts[]) {
+            const int nsegs = static_cast<int>(
+                std::ceil(wangs_formula_quadratic_reference_impl(kPrecision, pts)));
+
+            const float tdelta = 1.f / nsegs;
+            for (int j = 0; j < nsegs; ++j)
+            {
+                const float tmin = j * tdelta, tmax = (j + 1) * tdelta;
+
+                // Get section of quad in [tmin,tmax]
+                const Vec2D* sectionPts;
+                Vec2D tmp0[5];
+                Vec2D tmp1[5];
+                if (tmin == 0)
+                {
+                    if (tmax == 1)
+                    {
+                        sectionPts = pts;
+                    }
+                    else
+                    {
+                        chop_quad_at(pts, tmp0, tmax);
+                        sectionPts = tmp0;
+                    }
+                }
+                else
+                {
+                    chop_quad_at(pts, tmp0, tmin);
+                    if (tmax == 1)
+                    {
+                        sectionPts = tmp0 + 2;
+                    }
+                    else
+                    {
+                        chop_quad_at(tmp0 + 2, tmp1, (tmax - tmin) / (1 - tmin));
+                        sectionPts = tmp1;
+                    }
+                }
+
+                // For quads, max distance from baseline is always at t=0.5.
+                Vec2D p;
+                p = eval_quad_at(sectionPts, 0.5f);
+
+                // Get distance of p to baseline
+                const Vec2D n = {sectionPts[2].y - sectionPts[0].y,
+                                 sectionPts[0].x - sectionPts[2].x};
+                const float d = std::abs(Vec2D::dot(p - sectionPts[0], n)) / n.length();
+
+                // Check distance is within specified tolerance
+                REQUIRE(d <= (1.f / kPrecision) + 1e-2f);
+            }
+        },
+        maxExponent);
+}
+
+// Ensure the specialized version for rational quads reduces to regular Wang's
+// formula when all weights are equal to one
+TEST_CASE("wangs_formula_rational_quad_reduces", "[wangs_formula]")
+{
+    constexpr static float kTessellationTolerance = 1 / 128.f;
+
+    for (int i = 0; i < 100; ++i)
+    {
+        for_random_beziers(3, [](const Vec2D pts[]) {
+            const float rational_nsegs = wangs_formula::conic(kPrecision, pts, 1.f);
+            const float integral_nsegs = wangs_formula_quadratic_reference_impl(kPrecision, pts);
+            REQUIRE(fuzzy_equal(rational_nsegs, integral_nsegs, kTessellationTolerance));
+        });
+    }
+}
+
+// Ensure the rational quad version (used for conics) produces max error within tolerance.
+TEST_CASE("wangs_formula_conic_within_tol", "[wangs_formula]")
+{
+    constexpr int maxExponent = 24;
+
+    srand(0);
+
+    // Single-precision functions in SkConic/SkGeometry lose too much accuracy with
+    // large-magnitude curves and large weights for this test to pass.
+    using Sk2d = simd::gvec<double, 2>;
+    const auto eval_conic = [](const Vec2D pts[3], double w, double t) -> Sk2d {
+        const auto eval = [](Sk2d A, Sk2d B, Sk2d C, double t) -> Sk2d {
+            return (A * t + B) * t + C;
+        };
+
+        const Sk2d p0 = {pts[0].x, pts[0].y};
+        const Sk2d p1 = {pts[1].x, pts[1].y};
+        const Sk2d p1w = p1 * w;
+        const Sk2d p2 = {pts[2].x, pts[2].y};
+        Sk2d numer = eval(p2 - p1w * 2.0 + p0, (p1w - p0) * 2.0, p0, t);
+
+        Sk2d denomC = {1, 1};
+        Sk2d denomB = {2 * (w - 1), 2 * (w - 1)};
+        Sk2d denomA = {-2 * (w - 1), -2 * (w - 1)};
+        Sk2d denom = eval(denomA, denomB, denomC, t);
+        return numer / denom;
+    };
+
+    const auto dot = [](const Sk2d& a, const Sk2d& b) -> double {
+        return a[0] * b[0] + a[1] * b[1];
+    };
+
+    const auto length = [](const Sk2d& p) -> double { return sqrt(p[0] * p[0] + p[1] * p[1]); };
+
+    for (int i = -10; i <= 10; ++i)
+    {
+        const float w = std::ldexp(1 + frand(), i);
+        for_random_beziers(
+            3,
+            [&](const Vec2D pts[]) {
+                const int nsegs = static_cast<int>(ceilf(wangs_formula::conic(kPrecision, pts, w)));
+
+                const float tdelta = 1.f / nsegs;
+                for (int j = 0; j < nsegs; ++j)
+                {
+                    const float tmin = j * tdelta, tmax = (j + 1) * tdelta,
+                                tmid = 0.5f * (tmin + tmax);
+
+                    Sk2d p0, p1, p2;
+                    p0 = eval_conic(pts, w, tmin);
+                    p1 = eval_conic(pts, w, tmid);
+                    p2 = eval_conic(pts, w, tmax);
+
+                    // Get distance of p1 to baseline (p0, p2).
+                    const Sk2d n = {p2[1] - p0[1], p0[0] - p2[0]};
+                    assert(length(n) != 0);
+                    const double d = std::abs(dot(p1 - p0, n)) / length(n);
+
+                    // Check distance is within tolerance
+                    REQUIRE(d <= (1.0 / kPrecision) + kEpsilon);
+                    REQUIRE(d <= (1.0 / kPrecision) + kEpsilon);
+                }
+            },
+            maxExponent);
+    }
+}
+
+// Ensure the vectorized conic version equals the reference implementation
+TEST_CASE("wangs_formula_conic_matches_reference", "[wangs_formula]")
+{
+    srand(0);
+
+    for (int i = -10; i <= 10; ++i)
+    {
+        const float w = std::ldexp(1 + frand(), i);
+        for_random_beziers(3, [w](const Vec2D pts[]) {
+            const float ref_nsegs = wangs_formula_conic_reference_impl(kPrecision, pts, w);
+            const float nsegs = wangs_formula::conic(kPrecision, pts, w);
+
+            // Because the Gr version may implement the math differently for performance,
+            // allow different slack in the comparison based on the rough scale of the answer.
+            const float cmpThresh = ref_nsegs * (1.f / (1 << 20));
+            REQUIRE(fuzzy_equal(ref_nsegs, nsegs, cmpThresh));
+        });
+    }
+}
+
+// Ensure using transformations gives the same result as pre-transforming all points.
+TEST_CASE("wangs_formula_conic_vectorXforms", "[wangs_formula]")
+{
+    srand(0);
+
+    auto check_conic_with_transform = [&](const Vec2D* pts, float w, const Mat2D& m) {
+        Vec2D ptsXformed[3];
+        map_pts(m, ptsXformed, pts, 3);
+        float expected = wangs_formula::conic(kPrecision, ptsXformed, w);
+        float actual = wangs_formula::conic(kPrecision, pts, w, wangs_formula::VectorXform(m));
+        REQUIRE(actual == Approx(expected).margin(1e-4));
+    };
+
+    for (int i = -10; i <= 10; ++i)
+    {
+        const float w = std::ldexp(1 + frand(), i);
+        for_random_beziers(3, [&](const Vec2D pts[]) {
+            check_conic_with_transform(pts, w, Mat2D());
+            check_conic_with_transform(
+                pts,
+                w,
+                Mat2D::fromScale(frand_range(-10, 10), frand_range(-10, 10)));
+
+            // Random 2x2 matrix
+            Mat2D m;
+            m[0] = frand_range(-10, 10);
+            m[1] = frand_range(-10, 10);
+            m[2] = frand_range(-10, 10);
+            m[3] = frand_range(-10, 10);
+            check_conic_with_transform(pts, w, m);
+        });
+    }
+}
+} // namespace rive
diff --git a/utils/no_op_factory.cpp b/utils/no_op_factory.cpp
new file mode 100644
index 0000000..0656882
--- /dev/null
+++ b/utils/no_op_factory.cpp
@@ -0,0 +1,76 @@
+#include "utils/no_op_factory.hpp"
+
+using namespace rive;
+
+namespace
+{
+class NoOpRenderImage : public RenderImage
+{
+public:
+};
+
+class NoOpRenderPaint : public RenderPaint
+{
+public:
+    void color(unsigned int value) override {}
+    void style(RenderPaintStyle value) override {}
+    void thickness(float value) override {}
+    void join(StrokeJoin value) override {}
+    void cap(StrokeCap value) override {}
+    void blendMode(BlendMode value) override {}
+    void shader(rcp<RenderShader>) override {}
+    void invalidateStroke() override {}
+};
+
+class NoOpRenderPath : public RenderPath
+{
+public:
+    void rewind() override {}
+
+    void fillRule(FillRule value) override {}
+    void addPath(CommandPath* path, const Mat2D& transform) override {}
+    void addRenderPath(RenderPath* path, const Mat2D& transform) override {}
+
+    void moveTo(float x, float y) override {}
+    void lineTo(float x, float y) override {}
+    void cubicTo(float ox, float oy, float ix, float iy, float x, float y) override {}
+    void close() override {}
+};
+} // namespace
+
+rcp<RenderBuffer> NoOpFactory::makeRenderBuffer(RenderBufferType, RenderBufferFlags, size_t)
+{
+    return nullptr;
+}
+
+rcp<RenderShader> NoOpFactory::makeLinearGradient(float sx,
+                                                  float sy,
+                                                  float ex,
+                                                  float ey,
+                                                  const ColorInt colors[], // [count]
+                                                  const float stops[],     // [count]
+                                                  size_t count)
+{
+    return nullptr;
+}
+
+rcp<RenderShader> NoOpFactory::makeRadialGradient(float cx,
+                                                  float cy,
+                                                  float radius,
+                                                  const ColorInt colors[], // [count]
+                                                  const float stops[],     // [count]
+                                                  size_t count)
+{
+    return nullptr;
+}
+
+rcp<RenderPath> NoOpFactory::makeRenderPath(RawPath&, FillRule)
+{
+    return make_rcp<NoOpRenderPath>();
+}
+
+rcp<RenderPath> NoOpFactory::makeEmptyRenderPath() { return make_rcp<NoOpRenderPath>(); }
+
+rcp<RenderPaint> NoOpFactory::makeRenderPaint() { return make_rcp<NoOpRenderPaint>(); }
+
+rcp<RenderImage> NoOpFactory::decodeImage(Span<const uint8_t>) { return nullptr; }
diff --git a/viewer/README.md b/viewer/README.md
new file mode 100644
index 0000000..931355d
--- /dev/null
+++ b/viewer/README.md
@@ -0,0 +1,22 @@
+# Rive Viewer
+This is a desktop utility for previewing .riv files, Rive experiments, and different renderers. It also serves as a reference implementation for how to interface the Rive C++ Runtime (rive-cpp) with different renderers and factories for fonts, images, etc. 
+
+## Abstraction
+Rive is built to be platform and subsystem agnostic so you can plug-in any renderer to draw all of or only portions of your Rive animations. For example a simple WebGL renderer could only draw Rive meshes and drop all the vector content if it wanted to. Similarly image and font loading can be deferred to platform decoders. We provide some example fully fledged implementations that support all of the Rive features.
+
+## Building
+We currently provide build files for MacOS but we will be adding Windows and others soon too.
+### MacOS
+All the build scripts are in viewer/build/macosx.
+```
+cd viewer/build/macosx
+```
+You can tell the build script to build and run a Viewer that's backed by a Metal view with our Skia renderer:
+```
+./build_viewer.sh metal skia run
+```
+An OpenGL example using a tessellating renderer:
+```
+./build_viewer.sh gl tess run
+```
+Both the Skia and Tess renderers work with either gl or metal options.
\ No newline at end of file
diff --git a/viewer/build/macosx/build_viewer.sh b/viewer/build/macosx/build_viewer.sh
new file mode 100755
index 0000000..8053947
--- /dev/null
+++ b/viewer/build/macosx/build_viewer.sh
@@ -0,0 +1,79 @@
+#!/bin/sh
+set -e
+
+source ../../../dependencies/macosx/config_directories.sh
+
+CONFIG=debug
+GRAPHICS=gl
+RENDERER=skia
+
+for var in "$@"; do
+    if [[ $var = "release" ]]; then
+        CONFIG=release
+    fi
+    if [[ $var = "gl" ]]; then
+        GRAPHICS=gl
+    fi
+    if [[ $var = "d3d" ]]; then
+        GRAPHICS=d3d
+    fi
+    if [[ $var = "metal" ]]; then
+        GRAPHICS=metal
+    fi
+    if [[ $var = "tess" ]]; then
+        RENDERER=tess
+    fi
+    if [[ $var = "skia" ]]; then
+        RENDERER=skia
+    fi
+done
+
+if [[ ! -f "$DEPENDENCIES/bin/premake5" ]]; then
+    pushd $DEPENDENCIES_SCRIPTS
+    ./get_premake5.sh
+    popd
+fi
+
+if [[ ! -d "$DEPENDENCIES/imgui" ]]; then
+    pushd $DEPENDENCIES_SCRIPTS
+    ./get_imgui.sh
+    popd
+fi
+
+if [ $RENDERER = "skia" ]; then
+    pushd ../../../skia/renderer/build/macosx
+    ./build_skia_renderer.sh text $@
+    popd
+fi
+
+if [ $RENDERER = "tess" ]; then
+    pushd ../../../tess/build/macosx
+    ./build_tess.sh $@
+    popd
+fi
+
+export PREMAKE=$DEPENDENCIES/bin/premake5
+
+pushd ..
+
+OUT=out/$RENDERER/$GRAPHICS/$CONFIG
+$PREMAKE --scripts=../../build --file=./premake5_viewer.lua --config=$CONFIG --out=$OUT gmake2 --graphics=$GRAPHICS --renderer=$RENDERER --with_rive_tools --with_rive_text --with_rive_audio=system --with_rive_layout
+
+for var in "$@"; do
+    if [[ $var = "clean" ]]; then
+        make -C $OUT clean
+    fi
+done
+
+make -C $OUT -j$(($(sysctl -n hw.physicalcpu) + 1))
+
+for var in "$@"; do
+    if [[ $var = "run" ]]; then
+        $OUT/rive_viewer
+    fi
+    if [[ $var = "lldb" ]]; then
+        lldb $OUT/rive_viewer
+    fi
+done
+
+popd
diff --git a/viewer/build/premake5_viewer.lua b/viewer/build/premake5_viewer.lua
new file mode 100644
index 0000000..725da54
--- /dev/null
+++ b/viewer/build/premake5_viewer.lua
@@ -0,0 +1,150 @@
+dofile('rive_build_config.lua')
+
+dependencies = os.getenv('DEPENDENCIES')
+
+rive = '../../'
+rive_tess = '../../tess'
+rive_skia = '../../skia'
+skia = dependencies .. '/skia'
+
+if _OPTIONS.renderer == 'tess' then
+    dofile(rive .. '/decoders/build/premake5.lua')
+    dofile(path.join(path.getabsolute(rive_tess) .. '/build', 'premake5_tess.lua'))
+else
+    -- tess renderer includes this
+    dofile(path.join(path.getabsolute(rive) .. '/build', 'premake5.lua'))
+end
+
+dofile(path.join(path.getabsolute(rive) .. '/cg_renderer', 'premake5.lua'))
+
+project('rive_viewer')
+do
+    if _OPTIONS.renderer == 'tess' then
+        dependson('rive_decoders')
+    end
+    kind('ConsoleApp')
+
+    defines({ 'WITH_RIVE_TEXT', 'WITH_RIVE_AUDIO', 'WITH_RIVE_LAYOUT', 'YOGA_EXPORT=' })
+
+    includedirs({
+        '../include',
+        rive .. '/include',
+        rive .. '/skia/renderer/include', -- for font backends
+        dependencies,
+        dependencies .. '/sokol',
+        dependencies .. '/imgui',
+        miniaudio,
+        yoga,
+    })
+
+    links({ 'rive', 'rive_harfbuzz', 'rive_sheenbidi', 'rive_yoga' })
+
+    libdirs({ rive .. '/build/%{cfg.system}/bin/%{cfg.buildcfg}' })
+
+    files({
+        '../src/**.cpp',
+        rive .. '/utils/**.cpp',
+        dependencies .. '/imgui/imgui.cpp',
+        dependencies .. '/imgui/imgui_widgets.cpp',
+        dependencies .. '/imgui/imgui_tables.cpp',
+        dependencies .. '/imgui/imgui_draw.cpp',
+    })
+
+    buildoptions({ '-Wall', '-fno-exceptions', '-fno-rtti' })
+
+    filter({ 'system:macosx' })
+    do
+        links({
+            'Cocoa.framework',
+            'IOKit.framework',
+            'CoreVideo.framework',
+            'OpenGL.framework',
+            'rive_cg_renderer',
+        })
+        files({ '../src/**.m', '../src/**.mm' })
+    end
+
+    filter({ 'system:macosx', 'options:graphics=gl' })
+    do
+        links({ 'OpenGL.framework' })
+    end
+
+    filter({ 'system:macosx', 'options:graphics=metal' })
+    do
+        links({ 'Metal.framework', 'MetalKit.framework', 'QuartzCore.framework' })
+    end
+
+    -- Tess Renderer Configuration
+    filter({ 'options:renderer=tess' })
+    do
+        includedirs({ rive_tess .. '/include', rive .. '/decoders/include' })
+        defines({ 'RIVE_RENDERER_TESS' })
+        links({ 'rive_tess_renderer', 'rive_decoders', 'libpng', 'zlib', 'libjpeg', 'libwebp' })
+        libdirs({ rive_tess .. '/build/%{cfg.system}/bin/%{cfg.buildcfg}' })
+    end
+
+    filter({ 'options:renderer=tess', 'options:graphics=gl' })
+    do
+        defines({ 'SOKOL_GLCORE33' })
+    end
+
+    filter({ 'options:renderer=tess', 'options:graphics=metal' })
+    do
+        defines({ 'SOKOL_METAL' })
+    end
+
+    filter({ 'options:renderer=tess', 'options:graphics=d3d' })
+    do
+        defines({ 'SOKOL_D3D11' })
+    end
+
+    filter({ 'options:renderer=skia', 'options:graphics=gl' })
+    do
+        defines({ 'SK_GL', 'SOKOL_GLCORE33' })
+        files({ '../src/skia/viewer_skia_gl.cpp' })
+        libdirs({ skia .. '/out/gl/%{cfg.buildcfg}' })
+    end
+
+    filter({ 'options:renderer=skia', 'options:graphics=metal' })
+    do
+        defines({ 'SK_METAL', 'SOKOL_METAL' })
+        libdirs({ skia .. '/out/metal/%{cfg.buildcfg}' })
+    end
+
+    filter({ 'options:renderer=skia', 'options:graphics=d3d' })
+    do
+        defines({ 'SK_DIRECT3D' })
+        libdirs({ skia .. '/out/d3d/%{cfg.buildcfg}' })
+    end
+
+    filter({ 'options:renderer=skia' })
+    do
+        includedirs({
+            skia,
+            skia .. '/include/core',
+            skia .. '/include/effects',
+            skia .. '/include/gpu',
+            skia .. '/include/config',
+        })
+        defines({ 'RIVE_RENDERER_SKIA' })
+        libdirs({
+            rive_skia .. '/renderer/build/%{cfg.system}/bin/%{cfg.buildcfg}',
+        })
+        links({ 'skia', 'rive_skia_renderer' })
+    end
+
+    -- CLI config options
+    newoption({
+        trigger = 'graphics',
+        value = 'gl',
+        description = 'The graphics api to use.',
+        allowed = { { 'gl' }, { 'metal' }, { 'd3d' } },
+    })
+
+    newoption({
+        trigger = 'renderer',
+        value = 'skia',
+        description = 'The renderer to use.',
+        allowed = { { 'skia' }, { 'tess' } },
+    })
+end
diff --git a/viewer/include/viewer/sample_tools/sample_atlas_packer.hpp b/viewer/include/viewer/sample_tools/sample_atlas_packer.hpp
new file mode 100644
index 0000000..be871db
--- /dev/null
+++ b/viewer/include/viewer/sample_tools/sample_atlas_packer.hpp
@@ -0,0 +1,79 @@
+#ifndef _RIVE_SAMPLE_ATLAS_PACKER_HPP_
+#define _RIVE_SAMPLE_ATLAS_PACKER_HPP_
+
+#include "rive/span.hpp"
+#include "rive/file_asset_loader.hpp"
+#include "rive/math/mat2d.hpp"
+#include "rive/renderer.hpp"
+#include "rive/tess/sokol/sokol_tess_renderer.hpp"
+#include <vector>
+#include <unordered_map>
+
+namespace rive
+{
+class ImageAsset;
+class SokolRenderImageResource;
+/// A single atlas image generated by the packer.
+class SampleAtlas
+{
+private:
+    uint32_t m_width;
+    uint32_t m_height;
+    std::vector<uint8_t> m_pixels;
+
+    uint32_t m_x = 0;
+    uint32_t m_y = 0;
+    uint32_t m_nextY = 0;
+
+public:
+    SampleAtlas(const uint8_t* pixels, uint32_t width, uint32_t height);
+    SampleAtlas(uint32_t width, uint32_t height);
+    bool pack(const uint8_t* pixels, uint32_t width, uint32_t height, Mat2D& packTransform);
+
+    uint32_t width() const { return m_width; }
+    uint32_t height() const { return m_height; }
+    Span<const uint8_t> pixels() const { return m_pixels; }
+};
+
+struct SampleAtlasLocation
+{
+    std::size_t atlasIndex;
+    Mat2D transform;
+    // Original width & height of the image, so we can make vertex buffers for the "default renderer
+    // drawImage"
+    uint32_t width;
+    uint32_t height;
+};
+
+/// An Atlas packer which will create multiple atlas images (SampleAtles) as
+/// necessary.
+class SampleAtlasPacker
+{
+private:
+    uint32_t m_maxWidth;
+    uint32_t m_maxHeight;
+    std::vector<SampleAtlas*> m_atlases;
+    std::unordered_map<uint32_t, SampleAtlasLocation> m_lookup;
+
+public:
+    SampleAtlasPacker(uint32_t maxWidth, uint32_t maxHeight);
+    ~SampleAtlasPacker();
+    // Pack the images found in the file represented by rivBytes.
+    void pack(Span<const uint8_t> rivBytes);
+
+    bool find(const ImageAsset& asset, SampleAtlasLocation* location);
+    SampleAtlas* atlas(std::size_t index);
+};
+
+class SampleAtlasLoader : public FileAssetLoader
+{
+private:
+    SampleAtlasPacker* m_packer;
+    std::unordered_map<uint32_t, rive::rcp<rive::SokolRenderImageResource>> m_sharedImageResources;
+
+public:
+    SampleAtlasLoader(SampleAtlasPacker* packer);
+    bool loadContents(FileAsset& asset, Span<const uint8_t> inBandBytes, Factory*) override;
+};
+} // namespace rive
+#endif
diff --git a/viewer/include/viewer/tess/viewer_sokol_factory.hpp b/viewer/include/viewer/tess/viewer_sokol_factory.hpp
new file mode 100644
index 0000000..59421a4
--- /dev/null
+++ b/viewer/include/viewer/tess/viewer_sokol_factory.hpp
@@ -0,0 +1,11 @@
+#ifndef _RIVE_VIEWER_SOKOL_FACTORY_HPP_
+#define _RIVE_VIEWER_SOKOL_FACTORY_HPP_
+
+#include "rive/tess/sokol/sokol_factory.hpp"
+
+class ViewerSokolFactory : public rive::SokolFactory
+{
+public:
+    rive::rcp<rive::RenderImage> decodeImage(rive::Span<const uint8_t>) override;
+};
+#endif
\ No newline at end of file
diff --git a/viewer/include/viewer/viewer.hpp b/viewer/include/viewer/viewer.hpp
new file mode 100644
index 0000000..5142e17
--- /dev/null
+++ b/viewer/include/viewer/viewer.hpp
@@ -0,0 +1,20 @@
+#ifndef _RIVE_VIEWER_HPP_
+#define _RIVE_VIEWER_HPP_
+
+#ifdef RIVE_RENDERER_SKIA
+#include "GrBackendSurface.h"
+#include "GrDirectContext.h"
+#include "SkCanvas.h"
+#include "SkColorSpace.h"
+#include "SkSurface.h"
+#include "SkTypes.h"
+
+sk_sp<GrDirectContext> makeSkiaContext();
+sk_sp<SkSurface> makeSkiaSurface(GrDirectContext* context, int width, int height);
+void skiaPresentSurface(sk_sp<SkSurface> surface);
+#endif
+
+// Helper to ensure the gl context is currently bound.
+void bindGraphicsContext();
+
+#endif
\ No newline at end of file
diff --git a/viewer/include/viewer/viewer_content.hpp b/viewer/include/viewer/viewer_content.hpp
new file mode 100644
index 0000000..f19f138
--- /dev/null
+++ b/viewer/include/viewer/viewer_content.hpp
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_VIEWER_CONTENT_HPP_
+#define _RIVE_VIEWER_CONTENT_HPP_
+
+#include <vector>
+
+#include "rive/span.hpp"
+#include "rive/refcnt.hpp"
+
+#ifndef RIVE_SKIP_IMGUI
+#include "imgui.h"
+#endif
+
+namespace rive
+{
+class Renderer;
+class Factory;
+class Font;
+} // namespace rive
+
+class ViewerContent
+{
+public:
+    virtual ~ViewerContent();
+
+    virtual void handleResize(int width, int height) = 0;
+    virtual void handleDraw(rive::Renderer* renderer, double elapsed) = 0;
+#ifndef RIVE_SKIP_IMGUI
+    virtual void handleImgui() = 0;
+#endif
+
+    virtual void handlePointerMove(float x, float y) {}
+    virtual void handlePointerDown(float x, float y) {}
+    virtual void handlePointerUp(float x, float y) {}
+
+    using Factory = std::unique_ptr<ViewerContent> (*)(const char filename[]);
+
+    // Searches all handlers and returns a content if it is found.
+    static std::unique_ptr<ViewerContent> findHandler(const char filename[])
+    {
+        Factory factories[] = {
+            Scene,
+            Image,
+            Text,
+            TextPath,
+        };
+        for (auto f : factories)
+        {
+            if (auto content = f(filename))
+            {
+                return content;
+            }
+        }
+        return nullptr;
+    }
+
+    // Private factories...
+    static std::unique_ptr<ViewerContent> Image(const char[]);
+    static std::unique_ptr<ViewerContent> Scene(const char[]);
+    static std::unique_ptr<ViewerContent> Text(const char[]);
+    static std::unique_ptr<ViewerContent> TextPath(const char[]);
+    static std::unique_ptr<ViewerContent> TrimPath(const char[]);
+
+    static std::vector<uint8_t> LoadFile(const char path[]);
+
+    // Abstracts which rive Factory is currently used.
+    static rive::Factory* RiveFactory();
+
+    // Abstracts which font backend is currently used.
+    static rive::rcp<rive::Font> DecodeFont(rive::Span<const uint8_t>);
+};
+
+#endif
diff --git a/viewer/include/viewer/viewer_host.hpp b/viewer/include/viewer/viewer_host.hpp
new file mode 100644
index 0000000..00ddb5a
--- /dev/null
+++ b/viewer/include/viewer/viewer_host.hpp
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#ifndef _RIVE_VIEWER_HOST_HPP_
+#define _RIVE_VIEWER_HOST_HPP_
+
+#include "rive/factory.hpp"
+#include "rive/renderer.hpp"
+#include "rive/text_engine.hpp"
+
+#include "sokol_gfx.h"
+
+class ViewerContent;
+
+class ViewerHost
+{
+public:
+    virtual ~ViewerHost() {}
+
+    // subclasses can modify sg_pass_action if they wish, but need not.
+    virtual bool init(sg_pass_action*, int width, int height) = 0;
+
+    virtual void handleResize(int width, int height) = 0;
+
+    // subclasses need only override one or the other
+    virtual void beforeDefaultPass(ViewerContent*, double) {}
+    virtual void afterDefaultPass(ViewerContent*, double) {}
+
+    static std::unique_ptr<ViewerHost> Make();
+    static rive::Factory* Factory();
+};
+
+#endif
diff --git a/viewer/src/platform/imgui_sokol_impl.cpp b/viewer/src/platform/imgui_sokol_impl.cpp
new file mode 100644
index 0000000..8888b32
--- /dev/null
+++ b/viewer/src/platform/imgui_sokol_impl.cpp
@@ -0,0 +1,5 @@
+#include "sokol_app.h"
+#include "sokol_gfx.h"
+#define SOKOL_IMPL
+#include "imgui.h"
+#include "util/sokol_imgui.h"
\ No newline at end of file
diff --git a/viewer/src/platform/viewer_gl.mm b/viewer/src/platform/viewer_gl.mm
new file mode 100644
index 0000000..77e687a
--- /dev/null
+++ b/viewer/src/platform/viewer_gl.mm
@@ -0,0 +1,18 @@
+#include "viewer/viewer.hpp"
+#ifdef SOKOL_GLCORE33
+#include "sokol_app.h"
+#ifndef GL_SILENCE_DEPRECATION
+#define GL_SILENCE_DEPRECATION
+#endif
+#import "Cocoa/Cocoa.h"
+#endif
+
+void bindGraphicsContext()
+{
+#ifdef SOKOL_GLCORE33
+    NSWindow* window = (__bridge NSWindow*)sapp_macos_get_window();
+    NSOpenGLView* sokolView = (NSOpenGLView*)window.contentView;
+    NSOpenGLContext* ctx = [sokolView openGLContext];
+    [ctx makeCurrentContext];
+#endif
+}
diff --git a/viewer/src/platform/viewer_mac.m b/viewer/src/platform/viewer_mac.m
new file mode 100644
index 0000000..7bbb1bf
--- /dev/null
+++ b/viewer/src/platform/viewer_mac.m
@@ -0,0 +1,4 @@
+#define SOKOL_IMPL
+#include "sokol_app.h"
+#include "sokol_gfx.h"
+#include "sokol_glue.h"
\ No newline at end of file
diff --git a/viewer/src/sample_tools/sample_atlas_packer.cpp b/viewer/src/sample_tools/sample_atlas_packer.cpp
new file mode 100644
index 0000000..8453615
--- /dev/null
+++ b/viewer/src/sample_tools/sample_atlas_packer.cpp
@@ -0,0 +1,248 @@
+#ifdef RIVE_RENDERER_TESS
+#include "viewer/sample_tools/sample_atlas_packer.hpp"
+#include "utils/no_op_factory.hpp"
+#include "rive/decoders/bitmap_decoder.hpp"
+#include "rive/file.hpp"
+#include "rive/assets/image_asset.hpp"
+#include "rive/tess/sokol/sokol_tess_renderer.hpp"
+#include <algorithm>
+
+using namespace rive;
+
+class AtlasRenderImage : public lite_rtti_override<RenderImage, AtlasRenderImage>
+{
+private:
+    std::vector<uint8_t> m_Pixels;
+
+public:
+    AtlasRenderImage(const uint8_t* pixels, uint32_t width, uint32_t height) :
+        m_Pixels(pixels, pixels + (width * height * 4))
+    {
+        m_Width = width;
+        m_Height = height;
+    }
+
+    Span<const uint8_t> pixels() { return m_Pixels; }
+};
+
+class AtlasPackerFactory : public NoOpFactory
+{
+    rcp<RenderImage> decodeImage(Span<const uint8_t> bytes) override
+    {
+        auto bitmap = Bitmap::decode(bytes.data(), bytes.size());
+        if (bitmap)
+        {
+            // We have a bitmap, let's make an image.
+
+            // For now only deal with RGBA.
+            if (bitmap->pixelFormat() != Bitmap::PixelFormat::RGBA)
+            {
+                bitmap->pixelFormat(Bitmap::PixelFormat::RGBA);
+            }
+
+            return make_rcp<AtlasRenderImage>(bitmap->bytes(), bitmap->width(), bitmap->height());
+        }
+        return nullptr;
+    }
+};
+
+SampleAtlasPacker::SampleAtlasPacker(uint32_t maxWidth, uint32_t maxHeight) :
+    m_maxWidth(maxWidth), m_maxHeight(maxHeight)
+{}
+
+void SampleAtlasPacker::pack(Span<const uint8_t> rivBytes)
+{
+    AtlasPackerFactory factory;
+    if (auto file = rive::File::import(rivBytes, &factory))
+    {
+        for (auto asset : file->assets())
+        {
+            if (asset->is<ImageAsset>())
+            {
+                Mat2D uvTransform;
+
+                auto imageAsset = asset->as<ImageAsset>();
+                LITE_RTTI_CAST_OR_CONTINUE(renderImage,
+                                           AtlasRenderImage*,
+                                           imageAsset->renderImage());
+
+                if (m_atlases.empty())
+                {
+                    // Make the first atlas.
+                    m_atlases.push_back(new SampleAtlas(m_maxWidth, m_maxHeight));
+                }
+
+                // Pack into the current atlas.
+                if (!m_atlases.back()->pack(renderImage->pixels().data(),
+                                            renderImage->width(),
+                                            renderImage->height(),
+                                            uvTransform))
+                {
+                    // Didn't fit in previous atlas, make a new one.
+                    auto nextAtlas = new SampleAtlas(m_maxWidth, m_maxHeight);
+
+                    // Try to pack into the new one.
+                    if (!nextAtlas->pack(renderImage->pixels().data(),
+                                         renderImage->width(),
+                                         renderImage->height(),
+                                         uvTransform))
+                    {
+                        // Still failed. Image must be larger than max atlas. Just push the whole
+                        // image as an atlas.
+                        m_atlases.push_back(new SampleAtlas(renderImage->pixels().data(),
+                                                            renderImage->width(),
+                                                            renderImage->height()));
+
+                        // Clean up unused next atlas.
+                        delete nextAtlas;
+                    }
+                    else
+                    {
+                        m_atlases.push_back(nextAtlas);
+                    }
+                }
+
+                // Store where the image ended up.
+
+                m_lookup[asset->assetId()] = {
+                    .atlasIndex = m_atlases.size() - 1,
+                    .transform = uvTransform,
+                    .width = (uint32_t)renderImage->width(),
+                    .height = (uint32_t)renderImage->height(),
+                };
+            }
+        }
+    }
+}
+
+SampleAtlasPacker::~SampleAtlasPacker()
+{
+    for (auto atlas : m_atlases)
+    {
+        delete atlas;
+    }
+}
+
+SampleAtlas* SampleAtlasPacker::atlas(std::size_t index)
+{
+    assert(index < m_atlases.size());
+    return m_atlases[index];
+}
+
+SampleAtlas::SampleAtlas(const uint8_t* pixels, uint32_t width, uint32_t height) :
+    m_width(width), m_height(height), m_pixels(pixels, pixels + width * height * 4)
+{}
+
+SampleAtlas::SampleAtlas(uint32_t width, uint32_t height) :
+    m_width(width), m_height(height), m_pixels(width * height * 4)
+{}
+
+bool SampleAtlas::pack(const uint8_t* sourcePixels,
+                       uint32_t width,
+                       uint32_t height,
+                       Mat2D& packTransform)
+{
+    if (m_x + width >= m_width)
+    {
+        m_x = 0;
+        m_y = m_nextY;
+    }
+    // Check if we overflow vertically, we're done.
+    if (m_y + height >= m_height)
+    {
+        return false;
+    }
+
+    // We fit, pack into m_x, m_y.
+    for (uint32_t sy = 0; sy < height; sy++)
+    {
+        for (uint32_t sx = 0; sx < width; sx++)
+        {
+            for (uint8_t channel = 0; channel < 4; channel++)
+            {
+                m_pixels[((m_y + sy) * m_width + (m_x + sx)) * 4 + channel] =
+                    sourcePixels[(sy * width + sx) * 4 + channel];
+            }
+        }
+    }
+
+    // Set the UV transform.
+    packTransform = {
+        width / (float)m_width,
+        0,
+        0,
+        height / (float)m_height,
+        m_x / (float)m_width,
+        m_y / (float)m_height,
+    };
+
+    // Increment internal positions.
+    m_x += width;
+    if (m_y + height > m_nextY)
+    {
+        m_nextY = m_y + height;
+    }
+
+    return true;
+}
+
+bool SampleAtlasPacker::find(const ImageAsset& asset, SampleAtlasLocation* location)
+{
+    auto assetId = asset.assetId();
+    auto result = m_lookup.find(assetId);
+    if (result != m_lookup.end())
+    {
+        *location = result->second;
+        return true;
+    }
+    return false;
+}
+
+SampleAtlasLoader::SampleAtlasLoader(SampleAtlasPacker* packer) : m_packer(packer) {}
+
+bool SampleAtlasLoader::loadContents(FileAsset& asset, Span<const uint8_t> inBandBytes, Factory*)
+{
+    if (asset.is<ImageAsset>())
+    {
+        SampleAtlasLocation location;
+        auto imageAsset = asset.as<ImageAsset>();
+
+        // Find which location this image got packed into.
+        if (m_packer->find(*imageAsset, &location))
+        {
+            // Determine if we've already loaded the a render image.
+            auto sharedItr = m_sharedImageResources.find(location.atlasIndex);
+
+            rive::rcp<rive::SokolRenderImageResource> imageResource;
+            if (sharedItr != m_sharedImageResources.end())
+            {
+                imageResource = sharedItr->second;
+            }
+            else
+            {
+                auto atlas = m_packer->atlas(location.atlasIndex);
+                imageResource = rive::rcp<rive::SokolRenderImageResource>(
+                    new SokolRenderImageResource(atlas->pixels().data(),
+                                                 atlas->width(),
+                                                 atlas->height()));
+                m_sharedImageResources[location.atlasIndex] = imageResource;
+            }
+
+            // Make a render image from the atlas if we haven't yet. Our Factory
+            // doesn't have an abstraction for creating a RenderImage from a
+            // decoded set of pixels, we should either add that or live with/be
+            // ok with the fact that most people writing a resolver doing
+            // something like this will likely also have a custom/specific
+            // renderer (and hence will know which RenderImage they need to
+            // make).
+
+            imageAsset->renderImage(make_rcp<SokolRenderImage>(imageResource,
+                                                               location.width,
+                                                               location.height,
+                                                               location.transform));
+            return true;
+        }
+    }
+    return false;
+}
+#endif
diff --git a/viewer/src/skia/skia_host.cpp b/viewer/src/skia/skia_host.cpp
new file mode 100644
index 0000000..aae1d10
--- /dev/null
+++ b/viewer/src/skia/skia_host.cpp
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "viewer/viewer_host.hpp"
+#include "viewer/viewer_content.hpp"
+
+#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"
+#include "include/core/SkCanvas.h"
+#include "include/core/SkPaint.h"
+#include "include/core/SkSize.h"
+#include "GrDirectContext.h"
+
+sk_sp<GrDirectContext> makeSkiaContext();
+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
+
+// #define SW_SKIA_MODE
+
+#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;
+    SkISize m_dimensions;
+
+    bool init(sg_pass_action* action, int width, int height) override
+    {
+        m_dimensions = {width, height};
+
+#if defined(SK_METAL)
+        // Skia is layered behind the Sokol view, so we need to make sure Sokol
+        // clears transparent. Skia will draw the background.
+        *action = (sg_pass_action){.colors[0] = {
+            .action = SG_ACTION_CLEAR,
+            .value =
+            { 0.0f,
+              0.0,
+              0.0f,
+              0.0 }
+        }};
+#elif defined(SK_GL)
+        // Skia commands are issued to the same GL context before Sokol, so we need
+        // to make sure Sokol does not clear the buffer.
+        *action = (sg_pass_action){.colors[0] = {.action = SG_ACTION_DONTCARE }};
+#endif
+
+        m_context = makeSkiaContext();
+        return m_context != nullptr;
+    }
+
+    void handleResize(int width, int height) override { m_dimensions = {width, height}; }
+
+    void beforeDefaultPass(ViewerContent* content, double elapsed) override
+    {
+        m_context->resetContext();
+        auto surf = makeSkiaSurface(m_context.get(), m_dimensions.width(), m_dimensions.height());
+        SkCanvas* canvas = surf->getCanvas();
+        SkPaint paint;
+        paint.setColor(0xFF161616);
+        canvas->drawPaint(paint);
+
+        if (content)
+        {
+#ifdef TEST_CG_RENDERER
+            render_with_cg(canvas, m_dimensions.width(), m_dimensions.height(), content, elapsed);
+#elif defined(SW_SKIA_MODE)
+            auto info = SkImageInfo::MakeN32Premul(m_dimensions.width(), m_dimensions.height());
+            auto swsurf = SkSurface::MakeRaster(info);
+            rive::SkiaRenderer skiaRenderer(swsurf->getCanvas());
+            content->handleDraw(&skiaRenderer, elapsed);
+            auto img = swsurf->makeImageSnapshot();
+            canvas->drawImage(img, 0, 0, SkSamplingOptions(SkFilterMode::kNearest), nullptr);
+#else
+            rive::SkiaRenderer skiaRenderer(canvas);
+            content->handleDraw(&skiaRenderer, elapsed);
+#endif
+        }
+
+        canvas->flush();
+        skiaPresentSurface(surf);
+        sg_reset_state_cache();
+    }
+};
+
+std::unique_ptr<ViewerHost> ViewerHost::Make() { return rivestd::make_unique<SkiaViewerHost>(); }
+
+rive::Factory* ViewerHost::Factory()
+{
+#ifdef TEST_CG_RENDERER
+    static rive::CGFactory gFactory;
+    return &gFactory;
+#else
+    return &skiaFactory;
+#endif
+}
+
+#endif
diff --git a/viewer/src/skia/viewer_skia_gl.cpp b/viewer/src/skia/viewer_skia_gl.cpp
new file mode 100644
index 0000000..b42a5ed
--- /dev/null
+++ b/viewer/src/skia/viewer_skia_gl.cpp
@@ -0,0 +1,31 @@
+#if defined(RIVE_RENDERER_SKIA) && defined(SK_GL)
+#include "sokol_app.h"
+#include "sokol_gfx.h"
+#include "viewer/viewer.hpp"
+
+#include "gl/GrGLInterface.h"
+
+sk_sp<GrDirectContext> makeSkiaContext() { return GrDirectContext::MakeGL(); }
+
+sk_sp<SkSurface> makeSkiaSurface(GrDirectContext* context, int width, int height)
+{
+    GrGLFramebufferInfo framebufferInfo;
+    framebufferInfo.fFBOID = 0;
+    framebufferInfo.fFormat = 0x8058; // GL_RGBA8;
+
+    GrBackendRenderTarget backendRenderTarget(width,
+                                              height,
+                                              0, // sample count
+                                              0, // stencil bits
+                                              framebufferInfo);
+
+    return SkSurface::MakeFromBackendRenderTarget(context,
+                                                  backendRenderTarget,
+                                                  kBottomLeft_GrSurfaceOrigin,
+                                                  kRGBA_8888_SkColorType,
+                                                  nullptr,
+                                                  nullptr);
+}
+
+void skiaPresentSurface(sk_sp<SkSurface> surface) {}
+#endif
\ No newline at end of file
diff --git a/viewer/src/skia/viewer_skia_metal.mm b/viewer/src/skia/viewer_skia_metal.mm
new file mode 100644
index 0000000..5a18230
--- /dev/null
+++ b/viewer/src/skia/viewer_skia_metal.mm
@@ -0,0 +1,92 @@
+#if defined(RIVE_RENDERER_SKIA) && defined(SK_METAL)
+#include "viewer/viewer.hpp"
+#include "sokol_app.h"
+#include "sokol_gfx.h"
+
+#import <Metal/Metal.h>
+#import <MetalKit/MetalKit.h>
+#include "mtl/GrMtlBackendContext.h"
+#include "mtl/GrMtlTypes.h"
+#import <QuartzCore/CAMetalLayer.h>
+#import "Cocoa/Cocoa.h"
+
+id<MTLCommandQueue> commandQueue;
+id<CAMetalDrawable> drawable;
+GrMtlTextureInfo mtlTexture;
+MTKView* skiaView;
+NSView* contentView;
+
+typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
+    UIViewAutoresizingNone = 0,
+    UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
+    UIViewAutoresizingFlexibleWidth = 1 << 1,
+    UIViewAutoresizingFlexibleRightMargin = 1 << 2,
+    UIViewAutoresizingFlexibleTopMargin = 1 << 3,
+    UIViewAutoresizingFlexibleHeight = 1 << 4,
+    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
+};
+
+sk_sp<GrDirectContext> makeSkiaContext()
+{
+    // This is a little tricky...when using Metal we need to divorce the two
+    // views so we don't get contention between Sokol drawing (mostly for ImGui)
+    // with Metal and Skia drawing with Metal. I couldn't find a good way to let
+    // them share a command queue, so drawing to two separate Metal Layers is
+    // the next best thing.
+    id<MTLDevice> device = (__bridge id<MTLDevice>)sg_mtl_device();
+    commandQueue = [device newCommandQueue];
+
+    NSWindow* window = (__bridge NSWindow*)sapp_macos_get_window();
+
+    // Add a new metal view to our window.
+    skiaView = [[MTKView alloc] init];
+    skiaView.device = device;
+    skiaView.autoresizingMask =
+        (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
+    [skiaView setWantsLayer:YES];
+
+    // Grab the current contentView which is the default view Sokol App creates.
+    NSView* sokolView = window.contentView;
+    sokolView.autoresizingMask =
+        (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
+
+    // Make a new contentView (root container).
+    contentView = [[NSView alloc] init];
+    contentView.frame = sokolView.bounds;
+    skiaView.frame = sokolView.bounds;
+    window.contentView = contentView;
+
+    // Add Sokol and Skia views to it. Make sure to layer Sokol over Skia.
+    [contentView addSubview:skiaView];
+    [contentView addSubview:sokolView];
+    // Make sure Sokol view is transparent so ImGui can draw over our Skia
+    // content.
+    sokolView.layer.opaque = false;
+
+    return GrDirectContext::MakeMetal((__bridge void*)device, (__bridge void*)commandQueue);
+}
+
+sk_sp<SkSurface> makeSkiaSurface(GrDirectContext* context, int width, int height)
+{
+    NSView* view = skiaView;
+    CAMetalLayer* layer = (CAMetalLayer*)view.layer;
+
+    drawable = [layer nextDrawable];
+    GrMtlTextureInfo fbInfo;
+    fbInfo.fTexture.retain((__bridge const void*)(drawable.texture));
+    GrBackendRenderTarget renderTarget =
+        GrBackendRenderTarget(width, height, 1 /* sample count/MSAA */, fbInfo);
+
+    return SkSurface::MakeFromBackendRenderTarget(
+        context, renderTarget, kTopLeft_GrSurfaceOrigin, kBGRA_8888_SkColorType, nullptr, nullptr);
+}
+
+void skiaPresentSurface(sk_sp<SkSurface> surface)
+{
+    id<MTLCommandBuffer> commandBuffer = [(id<MTLCommandQueue>)commandQueue commandBuffer];
+    commandBuffer.label = @"Present";
+    [commandBuffer presentDrawable:(id<CAMetalDrawable>)drawable];
+    [commandBuffer commit];
+}
+
+#endif
\ No newline at end of file
diff --git a/viewer/src/stats.cpp b/viewer/src/stats.cpp
new file mode 100644
index 0000000..679f19f
--- /dev/null
+++ b/viewer/src/stats.cpp
@@ -0,0 +1,73 @@
+#include "sokol_app.h"
+#include "imgui.h"
+
+void displayStats()
+{
+    bool isOpen = true;
+    ImGuiStyle& style = ImGui::GetStyle();
+    style.WindowBorderSize = 0.0f;
+    ImGui::Begin("stats",
+                 &isOpen,
+                 ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove);
+    if (ImGui::BeginTable("table2", 2))
+    {
+
+        ImGui::TableNextRow();
+        ImGui::TableNextColumn();
+        ImGui::Text("fps");
+        ImGui::TableNextColumn();
+        ImGui::Text("%.1f", ImGui::GetIO().Framerate);
+
+        ImGui::TableNextRow();
+        ImGui::TableNextColumn();
+        ImGui::Text("ms/frame");
+        ImGui::TableNextColumn();
+        ImGui::Text("%.3f", 1000.0f / ImGui::GetIO().Framerate);
+
+        ImGui::TableNextRow();
+        ImGui::TableNextColumn();
+        ImGui::Text("window size");
+        ImGui::TableNextColumn();
+        ImGui::Text("%dx%d (%.1f)", sapp_width(), sapp_height(), sapp_dpi_scale());
+
+        ImGui::TableNextRow();
+        ImGui::TableNextColumn();
+        ImGui::Text("graphics api");
+        ImGui::TableNextColumn();
+        ImGui::Text(
+#if defined(SOKOL_GLCORE33)
+            "OpenGL 3.3"
+#elif defined(SOKOL_GLES2)
+            "OpenGL ES 2"
+#elif defined(SOKOL_GLES3)
+            "OpenGL ES 3"
+#elif defined(SOKOL_D3D11)
+            "D3D11"
+#elif defined(SOKOL_METAL)
+            "Metal"
+#elif defined(SOKOL_WGPU)
+            "WebGPU"
+#endif
+        );
+
+        ImGui::TableNextRow();
+        ImGui::TableNextColumn();
+        ImGui::Text("renderer");
+        ImGui::TableNextColumn();
+        ImGui::Text(
+#if defined(RIVE_RENDERER_TESS)
+            "Rive Tess"
+#elif defined(RIVE_RENDERER_SKIA)
+            "Rive Skia"
+#endif
+        );
+
+        ImGui::EndTable();
+    }
+
+    ImGui::SetWindowSize(ImVec2(230.0f, 102.0f));
+    ImGui::SetWindowPos(ImVec2(sapp_width() / sapp_dpi_scale() - ImGui::GetWindowWidth(),
+                               sapp_height() / sapp_dpi_scale() - ImGui::GetWindowHeight()),
+                        true);
+    ImGui::End();
+}
\ No newline at end of file
diff --git a/viewer/src/tess/tess_host.cpp b/viewer/src/tess/tess_host.cpp
new file mode 100644
index 0000000..416f910
--- /dev/null
+++ b/viewer/src/tess/tess_host.cpp
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "viewer/viewer_host.hpp"
+#include "viewer/viewer_content.hpp"
+
+#ifdef RIVE_RENDERER_TESS
+
+#include "rive/tess/sokol/sokol_tess_renderer.hpp"
+#include "viewer/tess/viewer_sokol_factory.hpp"
+
+class TessViewerHost : public ViewerHost
+{
+public:
+    std::unique_ptr<rive::SokolTessRenderer> m_renderer;
+
+    bool init(sg_pass_action*, int width, int height) override
+    {
+        m_renderer = rivestd::make_unique<rive::SokolTessRenderer>();
+        m_renderer->orthographicProjection(0.0f, width, height, 0.0f, 0.0f, 1.0f);
+        return true;
+    }
+
+    void handleResize(int width, int height) override
+    {
+        m_renderer->orthographicProjection(0.0f, width, height, 0.0f, 0.0f, 1.0f);
+    }
+
+    void afterDefaultPass(ViewerContent* content, double elapsed) override
+    {
+        m_renderer->reset();
+        if (content)
+        {
+            content->handleDraw(m_renderer.get(), elapsed);
+        }
+    }
+};
+
+std::unique_ptr<ViewerHost> ViewerHost::Make() { return rivestd::make_unique<TessViewerHost>(); }
+
+rive::Factory* ViewerHost::Factory()
+{
+    static ViewerSokolFactory sokolFactory;
+    return &sokolFactory;
+}
+
+#endif
diff --git a/viewer/src/tess/viewer_sokol_factory.cpp b/viewer/src/tess/viewer_sokol_factory.cpp
new file mode 100644
index 0000000..c8349bd
--- /dev/null
+++ b/viewer/src/tess/viewer_sokol_factory.cpp
@@ -0,0 +1,36 @@
+#ifdef RIVE_RENDERER_TESS
+#include "viewer/tess/viewer_sokol_factory.hpp"
+#include "rive/decoders/bitmap_decoder.hpp"
+#include "rive/tess/sokol/sokol_tess_renderer.hpp"
+#include "sokol_gfx.h"
+
+rive::rcp<rive::RenderImage> ViewerSokolFactory::decodeImage(rive::Span<const uint8_t> bytes)
+{
+    auto bitmap = Bitmap::decode(bytes.data(), bytes.size());
+    if (bitmap)
+    {
+        // We have a bitmap, let's make an image.
+
+        // For now our SokolRenderImage only works with RGBA.
+        if (bitmap->pixelFormat() != Bitmap::PixelFormat::RGBA)
+        {
+            bitmap->pixelFormat(Bitmap::PixelFormat::RGBA);
+        }
+
+        // In this case the image is in-band and the imageGpuResource is only
+        // used once by the unique SokolRenderImage. We introduced this
+        // abstraction because when the image is loaded externally (say by
+        // something that built up an atlas) the image gpu resource may be
+        // shared by multiple RenderImage referenced by multiple Rive objects.
+        auto imageGpuResource = rive::rcp<rive::SokolRenderImageResource>(
+            new rive::SokolRenderImageResource(bitmap->bytes(), bitmap->width(), bitmap->height()));
+
+        static rive::Mat2D identity;
+        return rive::make_rcp<rive::SokolRenderImage>(imageGpuResource,
+                                                      bitmap->width(),
+                                                      bitmap->height(),
+                                                      identity);
+    }
+    return nullptr;
+}
+#endif
diff --git a/viewer/src/viewer.cpp b/viewer/src/viewer.cpp
new file mode 100644
index 0000000..dc343fa
--- /dev/null
+++ b/viewer/src/viewer.cpp
@@ -0,0 +1,220 @@
+// Viewer & Rive
+#include "viewer/viewer.hpp"
+#include "viewer/viewer_content.hpp"
+#include "viewer/viewer_host.hpp"
+#include "rive/shapes/paint/color.hpp"
+
+// Graphics and UI abstraction
+#include "sokol_app.h"
+#include "sokol_gfx.h"
+#include "sokol_glue.h"
+#include "imgui.h"
+#include "util/sokol_imgui.h"
+
+// Std lib
+#include <stdio.h>
+#include <memory>
+#include <chrono>
+
+std::unique_ptr<ViewerHost> g_Host = ViewerHost::Make();
+std::unique_ptr<ViewerContent> g_Content = ViewerContent::TrimPath("");
+static struct
+{
+    sg_pass_action pass_action;
+} state;
+
+void displayStats();
+
+static const int backgroundColor = rive::colorARGB(255, 22, 22, 22);
+static std::chrono::time_point<std::chrono::high_resolution_clock> lastTime;
+static const float billion = 1000000000.0;
+
+static void init(void)
+{
+    sg_desc descriptor = {
+        .context = sapp_sgcontext(),
+        .buffer_pool_size = 1024,
+        .pipeline_pool_size = 1024,
+    };
+    sg_setup(&descriptor);
+    simgui_desc_t imguiDescriptor = {
+        .write_alpha_channel = true,
+    };
+    simgui_setup(&imguiDescriptor);
+
+    // If the host doesn't overwrite this (in init()), Sokol is in full control,
+    // so let's clear to our bg color.
+    state.pass_action = (sg_pass_action){
+        .colors[0] =
+            {
+                .action = SG_ACTION_CLEAR,
+                .value =
+                    {
+                        rive::colorRed(backgroundColor) / 255.0f,
+                        rive::colorGreen(backgroundColor) / 255.0f,
+                        rive::colorBlue(backgroundColor) / 255.0f,
+                        rive::colorOpacity(backgroundColor),
+                    },
+            },
+        .stencil =
+            {
+                .action = SG_ACTION_CLEAR,
+            },
+    };
+
+    lastTime = std::chrono::high_resolution_clock::now();
+
+    if (!g_Host->init(&state.pass_action, sapp_width(), sapp_height()))
+    {
+        fprintf(stderr, "failed to initialize host\n");
+        sapp_quit();
+    }
+}
+
+static void frame(void)
+{
+
+    auto newTime = std::chrono::high_resolution_clock::now();
+    auto dur =
+        std::chrono::duration_cast<std::chrono::nanoseconds>(newTime - lastTime).count() / billion;
+    lastTime = newTime;
+
+    g_Host->beforeDefaultPass(g_Content.get(), dur);
+
+    sg_begin_default_pass(&state.pass_action, sapp_width(), sapp_height());
+
+    g_Host->afterDefaultPass(g_Content.get(), dur);
+
+    simgui_frame_desc_t imguiDesc = {
+        .width = sapp_width(),
+        .height = sapp_height(),
+        .delta_time = sapp_frame_duration(),
+        .dpi_scale = sapp_dpi_scale(),
+    };
+    simgui_new_frame(&imguiDesc);
+
+    displayStats();
+
+    if (g_Content)
+    {
+        g_Content->handleImgui();
+    }
+    simgui_render();
+
+    sg_end_pass();
+    sg_commit();
+}
+
+static void cleanup(void)
+{
+    g_Content = nullptr;
+    g_Host = nullptr;
+
+    simgui_shutdown();
+    sg_shutdown();
+}
+
+static void event(const sapp_event* ev)
+{
+    simgui_handle_event(ev);
+
+    switch (ev->type)
+    {
+        case SAPP_EVENTTYPE_RESIZED:
+            if (g_Content)
+            {
+                g_Content->handleResize(ev->framebuffer_width, ev->framebuffer_height);
+            }
+            g_Host->handleResize(ev->framebuffer_width, ev->framebuffer_height);
+            break;
+        case SAPP_EVENTTYPE_FILES_DROPPED:
+        {
+            // Do this to make sure the graphics is bound.
+            bindGraphicsContext();
+
+            // get the number of files and their paths like this:
+            const int numDroppedFiles = sapp_get_num_dropped_files();
+            if (numDroppedFiles != 0)
+            {
+                const char* filename = sapp_get_dropped_file_path(numDroppedFiles - 1);
+                auto newContent = ViewerContent::findHandler(filename);
+                if (newContent)
+                {
+                    g_Content = std::move(newContent);
+                    g_Content->handleResize(ev->framebuffer_width, ev->framebuffer_height);
+                }
+                else
+                {
+                    fprintf(stderr, "No handler found for %s\n", filename);
+                }
+            }
+            break;
+        }
+        case SAPP_EVENTTYPE_MOUSE_DOWN:
+        case SAPP_EVENTTYPE_TOUCHES_BEGAN:
+            if (g_Content)
+            {
+                g_Content->handlePointerDown(ev->mouse_x, ev->mouse_y);
+            }
+            break;
+        case SAPP_EVENTTYPE_MOUSE_UP:
+        case SAPP_EVENTTYPE_TOUCHES_ENDED:
+            if (g_Content)
+            {
+                g_Content->handlePointerUp(ev->mouse_x, ev->mouse_y);
+                break;
+            }
+        case SAPP_EVENTTYPE_MOUSE_MOVE:
+        case SAPP_EVENTTYPE_TOUCHES_MOVED:
+            if (g_Content)
+            {
+                g_Content->handlePointerMove(ev->mouse_x, ev->mouse_y);
+            }
+            break;
+        case SAPP_EVENTTYPE_KEY_UP:
+            switch (ev->key_code)
+            {
+                case SAPP_KEYCODE_ESCAPE:
+                    sapp_quit();
+                    break;
+                case SAPP_KEYCODE_T:
+                    g_Content = ViewerContent::Text(".svg");
+                    break;
+                case SAPP_KEYCODE_P:
+                    g_Content = ViewerContent::TextPath("");
+                    break;
+                default:
+                    break;
+            }
+            break;
+        default:
+            break;
+    }
+}
+
+sapp_desc sokol_main(int argc, char* argv[])
+{
+    (void)argc;
+    (void)argv;
+
+    return (sapp_desc)
+    {
+        .init_cb = init, .frame_cb = frame, .cleanup_cb = cleanup, .event_cb = event,
+        .enable_dragndrop = true, .high_dpi = true,
+        .window_title = "Rive Viewer "
+#if defined(SOKOL_GLCORE33)
+                        "(OpenGL 3.3)",
+#elif defined(SOKOL_GLES2)
+                        "(OpenGL ES 2)",
+#elif defined(SOKOL_GLES3)
+                        "(OpenGL ES 3)",
+#elif defined(SOKOL_D3D11)
+                        "(D3D11)",
+#elif defined(SOKOL_METAL)
+                        "(Metal)",
+#elif defined(SOKOL_WGPU)
+                        "(WebGPU)",
+#endif
+        .width = 800, .height = 600, .icon.sokol_default = true, .gl_force_gles2 = true,
+    };
+}
\ No newline at end of file
diff --git a/viewer/src/viewer_content/image_content.cpp b/viewer/src/viewer_content/image_content.cpp
new file mode 100644
index 0000000..edca917
--- /dev/null
+++ b/viewer/src/viewer_content/image_content.cpp
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "viewer/viewer_content.hpp"
+#include "rive/factory.hpp"
+#include "rive/renderer.hpp"
+
+class ImageContent : public ViewerContent
+{
+    rive::rcp<rive::RenderImage> m_image;
+
+public:
+    ImageContent(rive::rcp<rive::RenderImage> image) { m_image = std::move(image); }
+
+    void handleDraw(rive::Renderer* renderer, double) override
+    {
+        renderer->drawImage(m_image.get(), rive::BlendMode::srcOver, 1);
+    }
+
+    void handleResize(int width, int height) override {}
+#ifndef RIVE_SKIP_IMGUI
+    void handleImgui() override {}
+#endif
+};
+
+std::unique_ptr<ViewerContent> ViewerContent::Image(const char filename[])
+{
+    auto bytes = LoadFile(filename);
+    auto image = RiveFactory()->decodeImage(bytes);
+    if (image)
+    {
+        return rivestd::make_unique<ImageContent>(std::move(image));
+    }
+    return nullptr;
+}
diff --git a/viewer/src/viewer_content/scene_content.cpp b/viewer/src/viewer_content/scene_content.cpp
new file mode 100644
index 0000000..ee43e2e
--- /dev/null
+++ b/viewer/src/viewer_content/scene_content.cpp
@@ -0,0 +1,399 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "rive/animation/linear_animation_instance.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/animation/state_machine_input_instance.hpp"
+#include "rive/animation/state_machine_number.hpp"
+#include "rive/animation/state_machine_bool.hpp"
+#include "rive/animation/state_machine_trigger.hpp"
+#include "rive/artboard.hpp"
+#include "rive/file.hpp"
+#include "rive/layout.hpp"
+#include "rive/math/aabb.hpp"
+#include "rive/assets/image_asset.hpp"
+#include "rive/viewmodel/viewmodel_instance.hpp"
+#include "viewer/viewer_content.hpp"
+#include "rive/relative_local_asset_loader.hpp"
+
+#ifdef RIVE_RENDERER_TESS
+#include "viewer/sample_tools/sample_atlas_packer.hpp"
+#endif
+
+constexpr int REQUEST_DEFAULT_SCENE = -1;
+
+class SceneContent : public ViewerContent
+{
+    // 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
+    std::vector<std::string> artboardNames;
+    std::vector<std::string> animationNames;
+    std::vector<std::string> stateMachineNames;
+
+    void loadArtboardNames()
+    {
+        if (m_File)
+        {
+            artboardNames.clear();
+            auto abCnt = m_File->artboardCount();
+
+            for (int i = 0; i < abCnt; i++)
+            {
+                auto abName = m_File->artboardNameAt(i);
+                artboardNames.push_back(abName);
+            }
+        }
+    }
+
+    void loadNames(const rive::Artboard* ab)
+    {
+        animationNames.clear();
+        stateMachineNames.clear();
+        if (ab)
+        {
+            for (size_t i = 0; i < ab->animationCount(); ++i)
+            {
+                animationNames.push_back(ab->animationNameAt(i));
+            }
+            for (size_t i = 0; i < ab->stateMachineCount(); ++i)
+            {
+                stateMachineNames.push_back(ab->stateMachineNameAt(i));
+            }
+        }
+    }
+
+    std::string m_Filename;
+    std::unique_ptr<rive::File> m_File;
+
+    std::unique_ptr<rive::ArtboardInstance> m_ArtboardInstance;
+    std::unique_ptr<rive::Scene> m_CurrentScene;
+    rive::ViewModelInstance* m_ViewModelInstance;
+    int m_ArtboardIndex = 0;
+    int m_AnimationIndex = 0;
+    int m_StateMachineIndex = -1;
+
+    int m_width = 0, m_height = 0;
+    rive::Mat2D m_InverseViewTransform;
+
+    void initArtboard(int index)
+    {
+        if (!m_File)
+            return;
+        loadArtboardNames();
+        m_ArtboardInstance = nullptr;
+
+        m_ArtboardIndex = (index == REQUEST_DEFAULT_SCENE) ? 0 : index;
+        m_ArtboardInstance = m_File->artboardAt(m_ArtboardIndex);
+        // m_ViewModelInstance = m_File->viewModelInstanceNamed("vm-3");
+        m_ViewModelInstance = m_File->createViewModelInstance(m_ArtboardInstance.get());
+        m_ArtboardInstance->dataContextFromInstance(m_ViewModelInstance);
+
+        m_ArtboardInstance->advance(0.0f);
+        loadNames(m_ArtboardInstance.get());
+
+        initStateMachine(REQUEST_DEFAULT_SCENE);
+    }
+
+    void initStateMachine(int index)
+    {
+        m_StateMachineIndex = -1;
+        m_AnimationIndex = -1;
+        m_CurrentScene = nullptr;
+
+        m_ArtboardInstance->advance(0.0f);
+
+        if (index < 0)
+        {
+            m_CurrentScene = m_ArtboardInstance->defaultStateMachine();
+            index = m_ArtboardInstance->defaultStateMachineIndex();
+        }
+        if (!m_CurrentScene)
+        {
+            if (index >= m_ArtboardInstance->stateMachineCount())
+            {
+                index = 0;
+            }
+            m_CurrentScene = m_ArtboardInstance->stateMachineAt(index);
+        }
+        if (!m_CurrentScene)
+        {
+            index = -1;
+            m_CurrentScene = m_ArtboardInstance->animationAt(0);
+            m_AnimationIndex = 0;
+        }
+        m_StateMachineIndex = index;
+
+        if (m_CurrentScene)
+        {
+            m_CurrentScene->inputCount();
+        }
+    }
+
+    void initAnimation(int index)
+    {
+        m_StateMachineIndex = -1;
+        m_AnimationIndex = -1;
+        m_CurrentScene = nullptr;
+
+        m_ArtboardInstance->advance(0.0f);
+
+        if (index >= 0 && index < m_ArtboardInstance->animationCount())
+        {
+            m_AnimationIndex = index;
+            m_CurrentScene = m_ArtboardInstance->animationAt(index);
+            m_CurrentScene->inputCount();
+        }
+    }
+
+public:
+    SceneContent(const char filename[], std::unique_ptr<rive::File> file) :
+        m_Filename(filename), m_File(std::move(file))
+    {
+        initArtboard(REQUEST_DEFAULT_SCENE);
+    }
+
+    void handlePointerMove(float x, float y) override
+    {
+        auto pointer = m_InverseViewTransform * rive::Vec2D(x, y);
+        if (m_CurrentScene)
+        {
+            m_CurrentScene->pointerMove(pointer);
+        }
+    }
+
+    void handlePointerDown(float x, float y) override
+    {
+        auto pointer = m_InverseViewTransform * rive::Vec2D(x, y);
+        if (m_CurrentScene)
+        {
+            m_CurrentScene->pointerDown(pointer);
+        }
+    }
+
+    void handlePointerUp(float x, float y) override
+    {
+        auto pointer = m_InverseViewTransform * rive::Vec2D(x, y);
+        if (m_CurrentScene)
+        {
+            m_CurrentScene->pointerUp(pointer);
+        }
+    }
+
+    void handleResize(int width, int height) override
+    {
+        m_width = width;
+        m_height = height;
+    }
+
+    void handleDraw(rive::Renderer* renderer, double elapsed) override
+    {
+        renderer->save();
+
+        auto viewTransform = rive::computeAlignment(rive::Fit::contain,
+                                                    rive::Alignment::center,
+                                                    rive::AABB(0, 0, m_width, m_height),
+                                                    m_ArtboardInstance->bounds());
+        renderer->transform(viewTransform);
+        // Store the inverse view so we can later go from screen to world.
+        m_InverseViewTransform = viewTransform.invertOrIdentity();
+
+        if (m_CurrentScene)
+        {
+            m_CurrentScene->advanceAndApply(elapsed);
+            m_CurrentScene->draw(renderer);
+        }
+        else
+        {
+            m_ArtboardInstance->draw(renderer); // we're just a still-frame file/artboard
+        }
+
+        renderer->restore();
+    }
+
+#ifndef RIVE_SKIP_IMGUI
+    void handleImgui() override
+    {
+        // For now the atlas packer only works with tess as it compiles in our
+        // Bitmap decoder.
+#ifdef RIVE_RENDERER_TESS
+        if (ImGui::BeginMainMenuBar())
+        {
+            if (ImGui::BeginMenu("Tools"))
+            {
+                if (ImGui::MenuItem("Build Atlas"))
+                {
+                    // Create an atlas packer.
+                    rive::SampleAtlasPacker atlasPacker(2048, 2048);
+
+                    // Have it pack the riv file, note that we need to re-load
+                    // the file as the packer internally sets up a custom
+                    // factory to process the images.
+                    auto rivFileBytes = LoadFile(m_Filename.c_str());
+                    atlasPacker.pack(rivFileBytes);
+
+                    // The packer now contains the new atlas(es) and metadata
+                    // (which image is in which atlas and the transform to apply
+                    // to the UV coordiantes). This is where you'd probably
+                    // serialize these results to new images and some metadata
+                    // format containing the atlas locations.
+
+                    // But for this demo, we're just going to pass this data on
+                    // to the asset resolver used to load the file with no
+                    // in-line images.
+
+                    // On that note, let's strip the images.
+                    rive::ImportResult stripResult;
+                    auto strippedBytes = rive::File::stripAssets(rivFileBytes,
+                                                                 {rive::ImageAsset::typeKey},
+                                                                 &stripResult);
+                    if (stripResult != rive::ImportResult::success)
+                    {
+                        printf("Failed to strip images\n");
+                        return;
+                    }
+
+                    // We let the atlas packer handle loading the riv file using
+                    // the previously generated assets. Note that we're loading
+                    // our riv file with the images stripped out of it.
+                    rive::ImportResult loadAtlasedResult;
+
+                    rive::SampleAtlasLoader resolver(&atlasPacker);
+                    if (auto file = rive::File::import(strippedBytes,
+                                                       RiveFactory(),
+                                                       &loadAtlasedResult,
+                                                       &resolver))
+                    {
+                        m_File = std::move(file);
+                        initArtboard(REQUEST_DEFAULT_SCENE);
+                    }
+                }
+                ImGui::EndMenu();
+            }
+            ImGui::EndMainMenuBar();
+        }
+#endif
+        if (m_ArtboardInstance != nullptr)
+        {
+            ImGui::Begin(m_Filename.c_str(), nullptr);
+            if (ImGui::ListBox(
+                    "Artboard",
+                    &m_ArtboardIndex,
+                    [](void* data, int index, const char** name) {
+                        auto& names = *static_cast<std::vector<std::string>*>(data);
+                        *name = names[index].c_str();
+                        return true;
+                    },
+                    &artboardNames,
+                    artboardNames.size(),
+                    4))
+            {
+                initArtboard(m_ArtboardIndex);
+            }
+            if (ImGui::ListBox(
+                    "Animations",
+                    &m_AnimationIndex,
+                    [](void* data, int index, const char** name) {
+                        auto& names = *static_cast<std::vector<std::string>*>(data);
+                        *name = names[index].c_str();
+                        return true;
+                    },
+                    &animationNames,
+                    animationNames.size(),
+                    4))
+            {
+                m_StateMachineIndex = -1;
+                initAnimation(m_AnimationIndex);
+            }
+            if (ImGui::ListBox(
+                    "State Machines",
+                    &m_StateMachineIndex,
+                    [](void* data, int index, const char** name) {
+                        auto& names = *static_cast<std::vector<std::string>*>(data);
+                        *name = names[index].c_str();
+                        return true;
+                    },
+                    &stateMachineNames,
+                    stateMachineNames.size(),
+                    4))
+            {
+                m_AnimationIndex = -1;
+                initStateMachine(m_StateMachineIndex);
+            }
+            if (m_CurrentScene != nullptr)
+            {
+
+                ImGui::Columns(2);
+                ImGui::SetColumnWidth(0, ImGui::GetWindowWidth() * 0.6666);
+
+                for (int i = 0; i < m_CurrentScene->inputCount(); i++)
+                {
+                    auto inputInstance = m_CurrentScene->input(i);
+
+                    if (inputInstance->input()->is<rive::StateMachineNumber>())
+                    {
+                        // ImGui requires names as id's, use ## to hide the
+                        // label but still give it an id.
+                        char label[256];
+                        snprintf(label, 256, "##%u", i);
+
+                        auto number = static_cast<rive::SMINumber*>(inputInstance);
+                        float v = number->value();
+                        ImGui::InputFloat(label, &v, 1.0f, 2.0f, "%.3f");
+                        number->value(v);
+                        ImGui::NextColumn();
+                    }
+                    else if (inputInstance->input()->is<rive::StateMachineTrigger>())
+                    {
+                        // ImGui requires names as id's, use ## to hide the
+                        // label but still give it an id.
+                        char label[256];
+                        snprintf(label, 256, "Fire##%u", i);
+                        if (ImGui::Button(label))
+                        {
+                            auto trigger = static_cast<rive::SMITrigger*>(inputInstance);
+                            trigger->fire();
+                        }
+                        ImGui::NextColumn();
+                    }
+                    else if (inputInstance->input()->is<rive::StateMachineBool>())
+                    {
+                        // ImGui requires names as id's, use ## to hide the
+                        // label but still give it an id.
+                        char label[256];
+                        snprintf(label, 256, "##%u", i);
+                        auto boolInput = static_cast<rive::SMIBool*>(inputInstance);
+                        bool value = boolInput->value();
+
+                        ImGui::Checkbox(label, &value);
+                        boolInput->value(value);
+                        ImGui::NextColumn();
+                    }
+                    ImGui::Text("%s", inputInstance->input()->name().c_str());
+                    ImGui::NextColumn();
+                }
+
+                ImGui::Columns(1);
+            }
+            ImGui::End();
+        }
+        else
+        {
+            ImGui::Text("Drop a .riv file to preview.");
+        }
+    }
+#endif
+};
+
+std::unique_ptr<ViewerContent> ViewerContent::Scene(const char filename[])
+{
+    auto bytes = LoadFile(filename);
+    rive::RelativeLocalAssetLoader loader(filename);
+    rive::ImportResult result;
+    if (auto file = rive::File::import(bytes, RiveFactory(), &result, &loader))
+    {
+        return rivestd::make_unique<SceneContent>(filename, std::move(file));
+    }
+    return nullptr;
+}
diff --git a/viewer/src/viewer_content/text_content.cpp b/viewer/src/viewer_content/text_content.cpp
new file mode 100644
index 0000000..3892d46
--- /dev/null
+++ b/viewer/src/viewer_content/text_content.cpp
@@ -0,0 +1,377 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "viewer/viewer_content.hpp"
+#include "rive/text/utf.hpp"
+
+#include "rive/math/raw_path.hpp"
+#include "rive/factory.hpp"
+#include "rive/refcnt.hpp"
+#include "rive/text_engine.hpp"
+#include <algorithm>
+
+using FontTextRuns = std::vector<rive::TextRun>;
+using FontGlyphRuns = rive::SimpleArray<rive::GlyphRun>;
+using FontFactory = rive::rcp<rive::Font> (*)(const rive::Span<const uint8_t>);
+
+static float drawrun(rive::Factory* factory,
+                     rive::Renderer* renderer,
+                     const rive::GlyphRun& run,
+                     unsigned startIndex,
+                     unsigned endIndex,
+                     rive::Vec2D origin)
+{
+    auto font = run.font.get();
+    const auto scale = rive::Mat2D::fromScale(run.size, run.size);
+    auto paint = factory->makeRenderPaint();
+    paint->color(0xFFFFFFFF);
+
+    float x = origin.x;
+    assert(startIndex >= 0 && endIndex <= run.glyphs.size());
+    int i, end, inc;
+    if (run.dir == rive::TextDirection::rtl)
+    {
+        i = endIndex - 1;
+        end = startIndex - 1;
+        inc = -1;
+    }
+    else
+    {
+        i = startIndex;
+        end = endIndex;
+        inc = 1;
+    }
+    while (i != end)
+    {
+        auto trans = rive::Mat2D::fromTranslate(x, origin.y);
+        x += run.advances[i];
+        auto rawpath = font->getPath(run.glyphs[i]);
+        rawpath.transformInPlace(trans * scale);
+        auto path = factory->makeRenderPath(rawpath, rive::FillRule::nonZero);
+        renderer->drawPath(path.get(), paint.get());
+        i += inc;
+    }
+    return x;
+}
+
+static float drawpara(rive::Factory* factory,
+                      rive::Renderer* renderer,
+                      const rive::Paragraph& paragraph,
+                      const rive::SimpleArray<rive::GlyphLine>& lines,
+                      rive::Vec2D origin)
+{
+
+    for (const auto& line : lines)
+    {
+
+        float x = line.startX + origin.x;
+        int runIndex, endRun, runInc;
+        if (paragraph.baseDirection == rive::TextDirection::rtl)
+        {
+            runIndex = line.endRunIndex;
+            endRun = line.startRunIndex - 1;
+            runInc = -1;
+        }
+        else
+        {
+            runIndex = line.startRunIndex;
+            endRun = line.endRunIndex + 1;
+            runInc = 1;
+        }
+        while (runIndex != endRun)
+        {
+            const auto& run = paragraph.runs[runIndex];
+            int startGIndex = runIndex == line.startRunIndex ? line.startGlyphIndex : 0;
+            int endGIndex = runIndex == line.endRunIndex ? line.endGlyphIndex : run.glyphs.size();
+
+            x = drawrun(factory,
+                        renderer,
+                        run,
+                        startGIndex,
+                        endGIndex,
+                        {x, origin.y + line.baseline});
+
+            runIndex += runInc;
+        }
+    }
+    return origin.y + lines.back().bottom;
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+std::vector<rive::rcp<rive::Font>> fallbackFonts;
+static rive::rcp<rive::Font> pickFallbackFont(rive::Span<const rive::Unichar> missing)
+{
+    size_t length = fallbackFonts.size();
+    for (size_t i = 0; i < length; i++)
+    {
+        auto font = fallbackFonts[i];
+        if (font->hasGlyph(missing))
+        {
+            return font;
+        }
+    }
+    return nullptr;
+}
+
+static rive::rcp<rive::RenderPath> make_line(rive::Factory* factory, rive::Vec2D a, rive::Vec2D b)
+{
+    rive::RawPath rawPath;
+    rawPath.move(a);
+    rawPath.line(b);
+    return factory->makeRenderPath(rawPath, rive::FillRule::nonZero);
+}
+
+static void draw_line(rive::Factory* factory, rive::Renderer* renderer, float x)
+{
+    auto paint = factory->makeRenderPaint();
+    paint->style(rive::RenderPaintStyle::stroke);
+    paint->thickness(1);
+    paint->color(0xFFFFFFFF);
+    auto path = make_line(factory, {x, 0}, {x, 1000});
+    renderer->drawPath(path.get(), paint.get());
+}
+
+static rive::TextRun append(std::vector<rive::Unichar>* unichars,
+                            rive::rcp<rive::Font> font,
+                            float size,
+                            float lineHeight,
+                            const char text[])
+{
+    const uint8_t* ptr = (const uint8_t*)text;
+    uint32_t n = 0;
+    while (*ptr)
+    {
+        unichars->push_back(rive::UTF::NextUTF8(&ptr));
+        n += 1;
+    }
+    return {std::move(font), size, lineHeight, 0.0f, n};
+}
+
+class TextContent : public ViewerContent
+{
+    std::vector<rive::Unichar> m_unichars;
+
+    rive::SimpleArray<rive::Paragraph> m_paragraphs;
+    rive::Mat2D m_xform;
+    float m_width = 300;
+    bool m_autoWidth = false;
+    int m_align = 0;
+
+    FontTextRuns make_truns(FontFactory fact)
+    {
+        auto loader = [fact](const char filename[]) -> rive::rcp<rive::Font> {
+            auto bytes = ViewerContent::LoadFile(filename);
+            if (bytes.size() == 0)
+            {
+                assert(false);
+                return nullptr;
+            }
+            return fact(bytes);
+        };
+
+        const char* fontFiles[] = {"../../../test/assets/RobotoFlex.ttf",
+                                   "../../../test/assets/Montserrat.ttf",
+                                   "../../../test/assets/IBMPlexSansArabic-Regular.ttf"};
+        // "../../../test/assets/NotoSansArabic-VariableFont_wdth,wght.ttf"};
+
+        auto font0 = loader(fontFiles[0]);
+        auto font1 = loader(fontFiles[1]);
+        auto font2 = loader(fontFiles[2]);
+        assert(font0);
+        assert(font1);
+        assert(font2);
+
+        fallbackFonts.push_back(font2);
+
+        rive::Font::Coord c1 = {'wght', 100.f}, c2 = {'wght', 800.f};
+
+        FontTextRuns truns;
+
+        // truns.push_back(
+        //     append(&m_unichars, font0->makeAtCoord(c2), 32, "No one ever left alive in "));
+        // truns.push_back(append(&m_unichars, font0->makeAtCoord(c2), 54, "nineteen hundred"));
+        // truns.push_back(append(&m_unichars, font0->makeAtCoord(c1), 30, "ne漢字asy"));
+
+        // truns.push_back(append(&m_unichars, font2, 30, " its the 
"));
+        // truns.push_back(append(&m_unichars, font2, 40, "cRown"));
+        // truns.push_back(append(&m_unichars, font2, 30, "a b c d"));
+
+        // truns.push_back(append(&m_unichars, font1->makeAtCoord(c1), 30, " that often"));
+        // truns.push_back(append(&m_unichars, font0, 30, " lies the head."));
+        // truns.push_back(append(&m_unichars, font0->makeAtCoord(c2), 60, "hi one two"));
+
+        // truns.push_back(append(&m_unichars, font2, 32.0f, "في 10-12 آذار 1997 بمدينة"));
+        // truns.push_back(append(&m_unichars, font2, 32.0f, "في 10-12 آذار 1997 بمدينة"));
+        truns.push_back(append(&m_unichars,
+                               font1,
+                               32.0f,
+                               21.0f,
+                               // clang-format off
+                               "this is some text
and here is "));
+        truns.push_back(append(&m_unichars,
+                               font2,
+                               32.0f,
+                               21.0f,
+                               // clang-format off
+                               "some"));
+        truns.push_back(append(&m_unichars,
+                               font1,
+                               32.0f,
+                               21.0f,
+                               // clang-format off
+                               " more"));
+                            //    "لمفاتيح ABC"));
+
+        // truns.push_back(append(&m_unichars,
+        //                        font1,
+        //                        28.0f,
+        //                        // clang-format off
+        //                        " DEF\n"));
+
+        // truns.push_back(append(&m_unichars,
+        //                        font0,
+        //                        32.0f,
+        //                        // clang-format off
+        //                        " one ever ff ffi left alive in "));
+
+        // truns.push_back(append(&m_unichars,
+        //                        font1,
+        //                        54.0f,
+        //                        // clang-format off
+        //                        "nineteen hundred"));
+
+        // truns.push_back(append(&m_unichars,
+        //                        font0,
+        //                        32.0f,
+        //                        // clang-format off
+        //                        "and eighty five"));
+        // TODO: test case from flutter causing assertion break
+
+        //    "لمفاتيح ABC DEF
في 10-12 آذار 1997 بمدينة\nabc def ghi jkl mnop\nلكن لا بد أن أوضح لك
+        //    أن كل"));
+        // "hello look\u2028here\nsecond paragraph"));
+
+        // clang-format on
+
+        // truns.push_back(append(&m_unichars, font2, 32.0f, "abc
def\nghijkl"));
+
+        // truns.push_back(append(&m_unichars, font2, 32.0f, "DEF
دنة"));
+
+        // truns.push_back(append(&m_unichars, font2, 32.0f, "AفيB"));
+
+        // truns.push_back(append(&m_unichars, font0, 42.0f, "OT\nHER\n"));
+        // truns.push_back(append(&m_unichars, font1, 62.0f, "VERY LARGE FONT HERE"));
+        // truns.push_back(
+        //     append(&m_unichars, font0, 52.0f, "one two three\n\n\nfour five six seven"));
+
+        // truns.push_back(append(&m_unichars, font0, 32.0f, "ab"));
+        // truns.push_back(append(&m_unichars, font0, 60.0f, "ee\n four"));
+
+        return truns;
+    }
+
+public:
+    TextContent()
+    {
+        fallbackFonts.clear();
+        rive::Font::gFallbackProc = pickFallbackFont;
+        auto truns = this->make_truns(ViewerContent::DecodeFont);
+        m_paragraphs = truns[0].font->shapeText(m_unichars, truns);
+
+        m_xform = rive::Mat2D::fromTranslate(10, 0) * rive::Mat2D::fromScale(3, 3);
+    }
+
+    void draw(rive::Renderer* renderer,
+              float width,
+              const rive::SimpleArray<rive::Paragraph>& paragraphs)
+    {
+
+        renderer->save();
+        renderer->transform(m_xform);
+        float y = 0.0f;
+        float paragraphWidth = m_autoWidth ? -1.0f : width;
+
+        rive::SimpleArray<rive::SimpleArray<rive::GlyphLine>>* lines =
+            new rive::SimpleArray<rive::SimpleArray<rive::GlyphLine>>(paragraphs.size());
+        rive::SimpleArray<rive::SimpleArray<rive::GlyphLine>>& linesRef = *lines;
+        size_t paragraphIndex = 0;
+
+        for (auto& para : paragraphs)
+        {
+            linesRef[paragraphIndex] =
+                rive::GlyphLine::BreakLines(para.runs, m_autoWidth ? -1.0f : width);
+
+            if (m_autoWidth)
+            {
+                paragraphWidth =
+                    std::max(paragraphWidth,
+                             rive::GlyphLine::ComputeMaxWidth(linesRef[paragraphIndex], para.runs));
+            }
+            paragraphIndex++;
+        }
+        paragraphIndex = 0;
+        for (auto& para : paragraphs)
+        {
+            rive::SimpleArray<rive::GlyphLine>& lines = linesRef[paragraphIndex];
+            rive::GlyphLine::ComputeLineSpacing(paragraphIndex == 0,
+                                                lines,
+                                                para.runs,
+                                                paragraphWidth,
+                                                (rive::TextAlign)m_align);
+            y = drawpara(RiveFactory(), renderer, para, lines, {0, y}) + 20.0f;
+            paragraphIndex++;
+        }
+        if (!m_autoWidth)
+        {
+            draw_line(RiveFactory(), renderer, width);
+        }
+
+        renderer->restore();
+    }
+
+    void handleDraw(rive::Renderer* renderer, double) override
+    {
+        this->draw(renderer, m_width, m_paragraphs);
+    }
+
+    void handleResize(int width, int height) override {}
+#ifndef RIVE_SKIP_IMGUI
+    void handleImgui() override
+    {
+        const char* alignOptions[] = {"left", "right", "center"};
+        ImGui::Begin("text", nullptr);
+        ImGui::SliderFloat("Width", &m_width, 1, 400);
+        ImGui::Checkbox("Autowidth", &m_autoWidth);
+        ImGui::Combo("combo", &m_align, alignOptions, IM_ARRAYSIZE(alignOptions));
+        ImGui::End();
+    }
+#endif
+};
+
+static bool ends_width(const char str[], const char suffix[])
+{
+    size_t ln = strlen(str);
+    size_t lx = strlen(suffix);
+    if (lx > ln)
+    {
+        return false;
+    }
+    for (size_t i = 0; i < lx; ++i)
+    {
+        if (str[ln - lx + i] != suffix[i])
+        {
+            return false;
+        }
+    }
+    return true;
+}
+
+std::unique_ptr<ViewerContent> ViewerContent::Text(const char filename[])
+{
+    if (ends_width(filename, ".svg"))
+    {
+        return rivestd::make_unique<TextContent>();
+    }
+    return nullptr;
+}
diff --git a/viewer/src/viewer_content/textpath_content.cpp b/viewer/src/viewer_content/textpath_content.cpp
new file mode 100644
index 0000000..9b05375
--- /dev/null
+++ b/viewer/src/viewer_content/textpath_content.cpp
@@ -0,0 +1,411 @@
+
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "viewer/viewer_content.hpp"
+#include "rive/text/utf.hpp"
+
+#include "rive/math/raw_path.hpp"
+#include "rive/refcnt.hpp"
+#include "rive/factory.hpp"
+#include "rive/text_engine.hpp"
+#include "rive/math/contour_measure.hpp"
+
+using namespace rive;
+#if 0
+using FontTextRuns = std::vector<TextRun>;
+using FontGlyphRuns = rive::SimpleArray<GlyphRun>;
+using FontFactory = rcp<Font> (*)(const Span<const uint8_t>);
+
+template <typename Handler> void visit(const Span<GlyphRun>& gruns, Vec2D origin, Handler proc)
+{
+    for (const auto& gr : gruns)
+    {
+        for (size_t i = 0; i < gr.glyphs.size(); ++i)
+        {
+            auto path = gr.font->getPath(gr.glyphs[i]);
+            auto mx = Mat2D::fromTranslate(origin.x + gr.xpos[i], origin.y) *
+                      Mat2D::fromScale(gr.size, gr.size);
+            path.transformInPlace(mx);
+            proc(path);
+        }
+    }
+}
+
+static Vec2D ave(Vec2D a, Vec2D b) { return (a + b) * 0.5f; }
+
+static RawPath make_quad_path(Span<const Vec2D> pts)
+{
+    const int N = pts.size();
+    RawPath path;
+    if (N >= 2)
+    {
+        path.move(pts[0]);
+        if (N == 2)
+        {
+            path.line(pts[1]);
+        }
+        else if (N == 3)
+        {
+            path.quad(pts[1], pts[2]);
+        }
+        else
+        {
+            for (int i = 1; i < N - 2; ++i)
+            {
+                path.quad(pts[i], ave(pts[i], pts[i + 1]));
+            }
+            path.quad(pts[N - 2], pts[N - 1]);
+        }
+    }
+    return path;
+}
+
+static void warp_in_place(ContourMeasure* meas, RawPath* path)
+{
+    for (auto& pt : path->points())
+    {
+        pt = meas->warp(pt);
+    }
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+
+static rcp<RenderPath> make_rpath(RawPath& path)
+{
+    return ViewerContent::RiveFactory()->makeRenderPath(path, FillRule::nonZero);
+}
+
+static void stroke_path(Renderer* renderer, RawPath& path, float size, ColorInt color)
+{
+    auto paint = ViewerContent::RiveFactory()->makeRenderPaint();
+    paint->color(color);
+    paint->thickness(size);
+    paint->style(RenderPaintStyle::stroke);
+    renderer->drawPath(make_rpath(path).get(), paint.get());
+}
+
+static void fill_rect(Renderer* renderer, const AABB& r, RenderPaint* paint)
+{
+    RawPath rp;
+    rp.addRect(r);
+    renderer->drawPath(make_rpath(rp).get(), paint);
+}
+
+static void fill_point(Renderer* renderer, Vec2D p, float r, RenderPaint* paint)
+{
+    fill_rect(renderer, {p.x - r, p.y - r, p.x + r, p.y + r}, paint);
+}
+
+static TextRun append(std::vector<Unichar>* unichars, rcp<Font> font, float size, const char text[])
+{
+    const uint8_t* ptr = (const uint8_t*)text;
+    uint32_t n = 0;
+    while (*ptr)
+    {
+        unichars->push_back(rive::UTF::NextUTF8(&ptr));
+        n += 1;
+    }
+    return {std::move(font), size, n};
+}
+
+class TextPathContent : public ViewerContent
+{
+    std::vector<Unichar> m_unichars;
+    FontGlyphRuns m_gruns;
+    rcp<RenderPaint> m_paint;
+    AABB m_gbounds;
+
+    std::vector<Vec2D> m_pathpts;
+    Vec2D m_lastPt = {0, 0};
+    int m_trackingIndex = -1;
+    Mat2D m_trans;
+
+    Mat2D m_oneLineXform;
+    bool m_trackingOneLine = false;
+    float m_oneLineX = 0;
+    float m_flareRadius = 50;
+
+    float m_alignment = 0, m_scaleY = 1, m_offsetY = 0,
+          m_windowWidth = 1, // %
+        m_windowOffset = 0;  // %
+
+    FontTextRuns make_truns(FontFactory fact)
+    {
+        auto loader = [fact](const char filename[]) -> rcp<Font> {
+            auto bytes = ViewerContent::LoadFile(filename);
+            if (bytes.size() == 0)
+            {
+                assert(false);
+                return nullptr;
+            }
+            return fact(bytes);
+        };
+
+        const char* fontFiles[] = {
+            "../../../test/assets/RobotoFlex.ttf",
+            "../../../test/assets/LibreBodoni-Italic-VariableFont_wght.ttf",
+        };
+
+        auto font0 = loader(fontFiles[0]);
+        auto font1 = loader(fontFiles[1]);
+        assert(font0);
+        assert(font1);
+
+        Font::Coord c1 = {'wght', 100.f}, c2 = {'wght', 800.f};
+
+        FontTextRuns truns;
+
+        truns.push_back(append(&m_unichars, font0->makeAtCoord(c2), 60, "U"));
+        truns.push_back(append(&m_unichars, font0->makeAtCoord(c1), 30, "ne漢字asy"));
+        truns.push_back(append(&m_unichars, font1, 30, " fits the crown"));
+        truns.push_back(append(&m_unichars, font1->makeAtCoord(c1), 30, " that often"));
+        truns.push_back(append(&m_unichars, font0, 30, " lies the head."));
+
+        return truns;
+    }
+
+public:
+    TextPathContent()
+    {
+        auto compute_bounds = [](const rive::SimpleArray<GlyphRun>& gruns) {
+            AABB bounds = {};
+            for (const auto& gr : gruns)
+            {
+                bounds.minY = std::min(bounds.minY, gr.font->lineMetrics().ascent * gr.size);
+                bounds.maxY = std::max(bounds.maxY, gr.font->lineMetrics().descent * gr.size);
+            }
+            bounds.minX = gruns.front().xpos.front();
+            bounds.maxX = gruns.back().xpos.back();
+            printf("%g %g %g %g\n", bounds.left(), bounds.top(), bounds.right(), bounds.bottom());
+            return bounds;
+        };
+
+        auto truns = this->make_truns(ViewerContent::DecodeFont);
+
+        m_gruns = truns[0].font->shapeText(m_unichars, truns);
+
+        m_gbounds = compute_bounds(m_gruns);
+        m_oneLineXform = Mat2D::fromScale(2.5, 2.5) * Mat2D::fromTranslate(20, 80);
+
+        m_paint = ViewerContent::RiveFactory()->makeRenderPaint();
+        m_paint->color(0xFFFFFFFF);
+
+        m_pathpts.push_back({20, 300});
+        m_pathpts.push_back({220, 100});
+        m_pathpts.push_back({420, 500});
+        m_pathpts.push_back({620, 100});
+        m_pathpts.push_back({820, 300});
+
+        m_trans = Mat2D::fromTranslate(200, 200) * Mat2D::fromScale(2, 2);
+    }
+
+    void draw_warp(Renderer* renderer, RawPath& warp)
+    {
+        stroke_path(renderer, warp, 0.5, 0xFF00FF00);
+
+        auto paint = ViewerContent::RiveFactory()->makeRenderPaint();
+        paint->color(0xFF008800);
+        const float r = 4;
+        for (auto p : m_pathpts)
+        {
+            fill_point(renderer, p, r, paint.get());
+        }
+    }
+
+    static size_t count_glyphs(const FontGlyphRuns& gruns)
+    {
+        size_t n = 0;
+        for (const auto& gr : gruns)
+        {
+            n += gr.glyphs.size();
+        }
+        return n;
+    }
+
+    void modify(float amount) { m_paint->color(0xFFFFFFFF); }
+
+    void draw(Renderer* renderer, const FontGlyphRuns& gruns)
+    {
+        auto get_path = [this](const GlyphRun& run, int index, float dx) {
+            auto path = run.font->getPath(run.glyphs[index]);
+            path.transformInPlace(Mat2D::fromTranslate(run.xpos[index] + dx, m_offsetY) *
+                                  Mat2D::fromScale(run.size, run.size * m_scaleY));
+            return path;
+        };
+
+        renderer->save();
+        renderer->transform(m_trans);
+
+        RawPath warp = make_quad_path(m_pathpts);
+        this->draw_warp(renderer, warp);
+
+        auto meas = ContourMeasureIter(&warp).next();
+
+        const float warpLength = meas->length();
+        const float textLength = gruns.back().xpos.back();
+        const float offset = (warpLength - textLength) * m_alignment;
+
+        const size_t glyphCount = count_glyphs(gruns);
+        size_t glyphIndex = 0;
+        float windowEnd = m_windowOffset + m_windowWidth;
+
+        for (const auto& gr : gruns)
+        {
+            for (size_t i = 0; i < gr.glyphs.size(); ++i)
+            {
+                float percent = glyphIndex / (float)(glyphCount - 1);
+                float amount = (percent >= m_windowOffset && percent <= windowEnd);
+
+                float scaleY = m_scaleY;
+                m_paint->color(0xFF666666);
+                m_paint->style(RenderPaintStyle::fill);
+                if (amount > 0)
+                {
+                    this->modify(amount);
+                }
+
+                auto path = get_path(gr, i, offset);
+                warp_in_place(meas.get(), &path);
+                renderer->drawPath(make_rpath(path).get(), m_paint.get());
+                glyphIndex += 1;
+                m_scaleY = scaleY;
+            }
+        }
+        renderer->restore();
+    }
+
+    void drawOneLine(Renderer* renderer)
+    {
+        auto paint = ViewerContent::RiveFactory()->makeRenderPaint();
+        paint->color(0xFF88FFFF);
+
+        if (m_trackingOneLine)
+        {
+            float mx = m_oneLineX / m_gbounds.width();
+            const ColorInt colors[] = {0xFF88FFFF, 0xFF88FFFF, 0xFFFFFFFF, 0xFF88FFFF, 0xFF88FFFF};
+            const float stops[] = {0, mx / 2, mx, (1 + mx) / 2, 1};
+            paint->shader(ViewerContent::RiveFactory()->makeLinearGradient(m_gbounds.left(),
+                                                                           0,
+                                                                           m_gbounds.right(),
+                                                                           0,
+                                                                           colors,
+                                                                           stops,
+                                                                           5));
+        }
+
+        struct EaseWindow
+        {
+            float center, radius;
+
+            float map(float x) const
+            {
+                float dist = std::abs(center - x);
+                if (dist > radius)
+                {
+                    return 0;
+                }
+                float t = (radius - dist) / radius;
+                return t * t * (3 - 2 * t);
+            }
+        };
+
+        auto wrap_path = [](const RawPath& src, float x, float rad) {
+            return src.morph([x, rad](Vec2D p) {
+                Vec2D newpt = p;
+                newpt.y = p.y * 4 + 18;
+
+                const float t = EaseWindow{x, rad}.map(p.x);
+                return Vec2D::lerp(p, newpt, t);
+            });
+        };
+
+        visit(m_gruns, {0, 0}, [&](RawPath& rp) {
+            RawPath* ptr = &rp;
+            RawPath storage;
+            if (m_trackingOneLine)
+            {
+                storage = wrap_path(rp, m_oneLineX, m_flareRadius);
+                ptr = &storage;
+            }
+            renderer->drawPath(make_rpath(*ptr).get(), paint.get());
+        });
+    }
+
+    void handleDraw(rive::Renderer* renderer, double) override
+    {
+        renderer->save();
+        this->draw(renderer, m_gruns);
+        renderer->restore();
+
+        renderer->save();
+        renderer->transform(m_oneLineXform);
+        this->drawOneLine(renderer);
+        renderer->restore();
+    }
+
+    void handlePointerMove(float x, float y) override
+    {
+        auto contains = [](const AABB& r, Vec2D p) {
+            return r.left() <= p.x && p.x < r.right() && r.top() <= p.y && p.y < r.bottom();
+        };
+
+        // are we on onLine?
+        {
+            m_trackingOneLine = false;
+            auto pos = m_oneLineXform.invertOrIdentity() * Vec2D{x, y};
+            if (contains(m_gbounds.inset(-8, 0), pos))
+            {
+                m_trackingOneLine = true;
+                m_oneLineX = pos.x;
+                return;
+            }
+        }
+
+        // are we on the path?
+        m_lastPt = m_trans.invertOrIdentity() * Vec2D{x, y};
+        if (m_trackingIndex >= 0)
+        {
+            m_pathpts[m_trackingIndex] = m_lastPt;
+        }
+    }
+    void handlePointerDown(float x, float y) override
+    {
+        auto close_to = [](Vec2D a, Vec2D b) { return Vec2D::distance(a, b) <= 10; };
+        for (size_t i = 0; i < m_pathpts.size(); ++i)
+        {
+            if (close_to(m_lastPt, m_pathpts[i]))
+            {
+                m_trackingIndex = i;
+                break;
+            }
+        }
+    }
+
+    void handlePointerUp(float x, float y) override { m_trackingIndex = -1; }
+
+    void handleResize(int width, int height) override {}
+
+#ifndef RIVE_SKIP_IMGUI
+    void handleImgui() override
+    {
+        ImGui::Begin("path", nullptr);
+        ImGui::SliderFloat("Alignment", &m_alignment, -3, 4);
+        ImGui::SliderFloat("Scale Y", &m_scaleY, 0.25f, 3.0f);
+        ImGui::SliderFloat("Offset Y", &m_offsetY, -100, 100);
+        ImGui::SliderFloat("Window Offset", &m_windowOffset, -1.1f, 1.1f);
+        ImGui::SliderFloat("Window Width", &m_windowWidth, 0, 1.2f);
+        ImGui::SliderFloat("Flare radius", &m_flareRadius, 10, 100);
+        ImGui::End();
+    }
+#endif
+};
+
+std::unique_ptr<ViewerContent> ViewerContent::TextPath(const char filename[])
+{
+    return rivestd::make_unique<TextPathContent>();
+}
+#else
+std::unique_ptr<ViewerContent> ViewerContent::TextPath(const char filename[]) { return nullptr; }
+#endif
diff --git a/viewer/src/viewer_content/trimpath_content.cpp b/viewer/src/viewer_content/trimpath_content.cpp
new file mode 100644
index 0000000..d4ffb3d
--- /dev/null
+++ b/viewer/src/viewer_content/trimpath_content.cpp
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "viewer/viewer_content.hpp"
+#include "rive/factory.hpp"
+#include "rive/renderer.hpp"
+#include "rive/math/contour_measure.hpp"
+
+using namespace rive;
+
+static Vec2D ave(Vec2D a, Vec2D b) { return (a + b) * 0.5f; }
+
+static RawPath make_quad_path(Span<const Vec2D> pts)
+{
+    const int N = pts.size();
+    RawPath path;
+    if (N >= 2)
+    {
+        path.move(pts[0]);
+        if (N == 2)
+        {
+            path.line(pts[1]);
+        }
+        else if (N == 3)
+        {
+            path.quad(pts[1], pts[2]);
+        }
+        else
+        {
+            for (int i = 1; i < N - 2; ++i)
+            {
+                path.quad(pts[i], ave(pts[i], pts[i + 1]));
+            }
+            path.quad(pts[N - 2], pts[N - 1]);
+        }
+    }
+    return path;
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+
+static rcp<RenderPath> make_rpath(RawPath& path)
+{
+    return ViewerContent::RiveFactory()->makeRenderPath(path, FillRule::nonZero);
+}
+
+static void stroke_path(Renderer* renderer, RawPath& path, float size, ColorInt color)
+{
+    auto paint = ViewerContent::RiveFactory()->makeRenderPaint();
+    paint->color(color);
+    paint->thickness(size);
+    paint->style(RenderPaintStyle::stroke);
+    renderer->drawPath(make_rpath(path).get(), paint.get());
+}
+
+static void fill_rect(Renderer* renderer, const AABB& r, RenderPaint* paint)
+{
+    RawPath rp;
+    rp.addRect(r);
+    renderer->drawPath(make_rpath(rp).get(), paint);
+}
+
+static void fill_point(Renderer* renderer, Vec2D p, float r, RenderPaint* paint)
+{
+    fill_rect(renderer, {p.x - r, p.y - r, p.x + r, p.y + r}, paint);
+}
+
+static RawPath trim(ContourMeasure* cm, float startT, float endT)
+{
+    // start and end are 0...1
+    auto startD = startT * cm->length();
+    auto endD = endT * cm->length();
+    if (startD > endD)
+    {
+        std::swap(startD, endD);
+    }
+
+    RawPath path;
+    cm->getSegment(startD, endD, &path, true);
+    return path;
+}
+
+class TrimPathContent : public ViewerContent
+{
+    std::vector<Vec2D> m_pathpts;
+    int m_trackingIndex = -1;
+
+    float m_trimFrom = 0, m_trimTo = 1;
+
+public:
+    TrimPathContent()
+    {
+        m_pathpts.push_back({20, 300});
+        m_pathpts.push_back({220, 100});
+        m_pathpts.push_back({420, 500});
+        m_pathpts.push_back({620, 100});
+        m_pathpts.push_back({820, 300});
+    }
+
+    void handleDraw(rive::Renderer* renderer, double) override
+    {
+        auto path = make_quad_path(m_pathpts);
+
+        RawPath cubicpath;
+        cubicpath.move(m_pathpts[0]);
+        cubicpath.cubic(m_pathpts[1], m_pathpts[2], m_pathpts[3]);
+        cubicpath.line(m_pathpts[4]);
+
+        RawPath* ps[] = {&path, &cubicpath};
+
+        renderer->save();
+        for (auto p : ps)
+        {
+            renderer->save();
+
+            auto cm = ContourMeasureIter(p, false).next();
+            auto p1 = trim(cm.get(), m_trimFrom, m_trimTo);
+            stroke_path(renderer, p1, 20, 0xFFFF0000);
+
+            stroke_path(renderer, *p, 4, 0xFFFFFFFF);
+
+            renderer->restore();
+            renderer->translate(0, 500);
+        }
+        renderer->restore();
+
+        auto paint = ViewerContent::RiveFactory()->makeRenderPaint();
+        paint->color(0xFF008800);
+        const float r = 6;
+        for (auto p : m_pathpts)
+        {
+            fill_point(renderer, p, r, paint.get());
+        }
+    }
+
+    void handlePointerMove(float x, float y) override
+    {
+        if (m_trackingIndex >= 0)
+        {
+            m_pathpts[m_trackingIndex] = Vec2D{x, y};
+        }
+    }
+    void handlePointerDown(float x, float y) override
+    {
+        auto pt = Vec2D{x, y};
+        auto close_to = [](Vec2D a, Vec2D b) { return Vec2D::distance(a, b) <= 10; };
+        for (size_t i = 0; i < m_pathpts.size(); ++i)
+        {
+            if (close_to(pt, m_pathpts[i]))
+            {
+                m_trackingIndex = i;
+                break;
+            }
+        }
+    }
+
+    void handlePointerUp(float x, float y) override { m_trackingIndex = -1; }
+
+    void handleResize(int width, int height) override {}
+
+#ifndef RIVE_SKIP_IMGUI
+    void handleImgui() override
+    {
+        ImGui::Begin("trim", nullptr);
+        ImGui::SliderFloat("From", &m_trimFrom, 0, 1);
+        ImGui::SliderFloat("To", &m_trimTo, 0, 1);
+        ImGui::End();
+    }
+#endif
+};
+
+std::unique_ptr<ViewerContent> ViewerContent::TrimPath(const char[])
+{
+    return rivestd::make_unique<TrimPathContent>();
+}
diff --git a/viewer/src/viewer_content/viewer_content.cpp b/viewer/src/viewer_content/viewer_content.cpp
new file mode 100644
index 0000000..e41da51
--- /dev/null
+++ b/viewer/src/viewer_content/viewer_content.cpp
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2022 Rive
+ */
+
+#include "viewer/viewer_content.hpp"
+#include <vector>
+
+ViewerContent::~ViewerContent() {}
+
+const char* gCounterNames[] = {
+    "file",
+    "artboard",
+    "animation",
+    "machine",
+    "buffer",
+    "path",
+    "paint",
+    "shader",
+    "image",
+};
+
+std::vector<uint8_t> ViewerContent::LoadFile(const char filename[])
+{
+    std::vector<uint8_t> bytes;
+
+    FILE* fp = fopen(filename, "rb");
+    if (!fp)
+    {
+        fprintf(stderr, "Can't find file: %s\n", filename);
+        return bytes;
+    }
+
+    fseek(fp, 0, SEEK_END);
+    size_t size = ftell(fp);
+    fseek(fp, 0, SEEK_SET);
+
+    bytes.resize(size);
+    size_t bytesRead = fread(bytes.data(), 1, size, fp);
+    fclose(fp);
+
+    if (bytesRead != size)
+    {
+        fprintf(stderr, "Failed to read all of %s\n", filename);
+        bytes.resize(0);
+    }
+    return bytes;
+}
+
+#ifndef RIVE_SKIP_IMGUI
+#include "viewer/viewer_host.hpp"
+
+rive::Factory* ViewerContent::RiveFactory() { return ViewerHost::Factory(); }
+#endif
+
+#include "rive/text/font_hb.hpp"
+rive::rcp<rive::Font> ViewerContent::DecodeFont(rive::Span<const uint8_t> span)
+{
+    return HBFont::Decode(span);
+}